Hard paywall
This commit is contained in:
@@ -19,9 +19,19 @@ import { Language } from '../../types';
|
||||
import { PurchaseProductId } from '../../services/backend/contracts';
|
||||
|
||||
type SubscriptionProductId = 'monthly_pro' | 'yearly_pro';
|
||||
type TopupProductId = Extract<PurchaseProductId, 'topup_small' | 'topup_medium' | 'topup_large'>;
|
||||
type SubscriptionPackages = Partial<Record<SubscriptionProductId, PurchasesPackage>>;
|
||||
type TopupProducts = Partial<Record<TopupProductId, PurchasesStoreProduct>>;
|
||||
type TopupProductId = Extract<PurchaseProductId, 'topup_small' | 'topup_medium' | 'topup_large'>;
|
||||
type SubscriptionPackages = Partial<Record<SubscriptionProductId, PurchasesPackage>>;
|
||||
type TopupProducts = Partial<Record<TopupProductId, PurchasesStoreProduct>>;
|
||||
|
||||
const TOPUP_CREDITS_BY_PRODUCT: Record<TopupProductId, number> = {
|
||||
topup_small: 30,
|
||||
topup_medium: 100,
|
||||
topup_large: 250,
|
||||
};
|
||||
|
||||
const isTopupProductId = (productId: PurchaseProductId): productId is TopupProductId => (
|
||||
productId === 'topup_small' || productId === 'topup_medium' || productId === 'topup_large'
|
||||
);
|
||||
|
||||
const isMatchingPackage = (
|
||||
pkg: PurchasesPackage,
|
||||
@@ -79,7 +89,22 @@ const getBillingCopy = (language: Language) => {
|
||||
manageSubscription: 'Abo verwalten',
|
||||
subscriptionTitle: 'Abos',
|
||||
subscriptionHint: 'Wähle ein Abo und schalte stärkere KI-Scans sowie mehr Credits frei.',
|
||||
freePlanName: 'Free',
|
||||
paywallTitle: 'Vollstaendige Diagnose freischalten',
|
||||
paywallHint: 'Starte Pro fuer echte GPT-5.4 Scans, deinen 7-Tage-Rettungsplan und 100 Credits fuer AI-Scans und Follow-ups.',
|
||||
startTrial: '7 Tage kostenlos testen',
|
||||
monthlyCta: 'Monatlich starten',
|
||||
yearlyCta: 'Jaehrlich starten',
|
||||
yearlyTrialBadge: '7 TAGE GRATIS',
|
||||
monthlyBadge: 'FLEXIBEL',
|
||||
yearlySubline: 'Danach 39,99 EUR/Jahr. Jederzeit kuendbar.',
|
||||
monthlySubline: '4,99 EUR/Monat. Ohne Jahresbindung.',
|
||||
saveLabel: 'Bester Wert',
|
||||
expoGoPurchaseTitle: 'Kauf nur im Dev Build',
|
||||
expoGoPurchaseMessage: 'Expo Go kann keine Apple- oder RevenueCat-Kaufmaske anzeigen. Im Development Build oder TestFlight erscheint hier der echte 7-Tage-Trial. Fuer lokale Tests kannst du Pro simulieren.',
|
||||
expoGoSimulate: 'Pro simulieren',
|
||||
perYear: '/ Jahr',
|
||||
perMonth: '/ Monat',
|
||||
freePlanName: 'Free',
|
||||
freePlanPrice: '0 EUR / Monat',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4,99 € / Monat',
|
||||
@@ -88,17 +113,20 @@ const getBillingCopy = (language: Language) => {
|
||||
proYearlyPlanPrice: '39,99 € / Jahr',
|
||||
proYearlyBadgeText: 'SPAREN',
|
||||
proBenefits: [
|
||||
'250 Credits jeden Monat',
|
||||
'100 Credits für AI-Scans und Follow-ups jeden Monat',
|
||||
'Pro-Scans mit GPT-5.4',
|
||||
'Unbegrenzte Historie & Galerie',
|
||||
'KI-Pflanzendoktor inklusive',
|
||||
'Priorisierter Support'
|
||||
],
|
||||
topupTitle: 'Credits Aufladen',
|
||||
topupSmall: '25 Credits – 1,99 €',
|
||||
topupMedium: '120 Credits – 6,99 €',
|
||||
topupLarge: '300 Credits – 12,99 €',
|
||||
topupBestValue: 'BESTES ANGEBOT',
|
||||
topupHint: 'Für aktive Pro-Nutzer, wenn die Monatscredits nicht reichen.',
|
||||
topupSmall: '30 Credits – 2,99 €',
|
||||
topupMedium: '100 Credits – 6,99 €',
|
||||
topupLarge: '250 Credits – 12,99 €',
|
||||
topupBestValue: 'BESTES ANGEBOT',
|
||||
topupRequiresProTitle: 'Pro erforderlich',
|
||||
topupRequiresProMessage: 'Top-ups sind für aktive Pro-Nutzer gedacht. Starte Pro, um zusätzliche Credits zu kaufen.',
|
||||
cancelTitle: 'Schade, dass du gehst',
|
||||
cancelQuestion: 'Dürfen wir fragen, warum du kündigst?',
|
||||
reasonTooExpensive: 'Es ist mir zu teuer',
|
||||
@@ -124,7 +152,22 @@ const getBillingCopy = (language: Language) => {
|
||||
manageSubscription: 'Administrar Suscripción',
|
||||
subscriptionTitle: 'Suscripciones',
|
||||
subscriptionHint: 'Elige un plan y desbloquea escaneos con IA más potentes y más créditos.',
|
||||
freePlanName: 'Gratis',
|
||||
paywallTitle: 'Desbloquear diagnostico completo',
|
||||
paywallHint: 'Inicia Pro para escaneos reales con GPT-5.4, tu plan de rescate de 7 dias y 100 creditos para escaneos IA y seguimientos.',
|
||||
startTrial: 'Probar 7 dias gratis',
|
||||
monthlyCta: 'Empezar mensual',
|
||||
yearlyCta: 'Empezar anual',
|
||||
yearlyTrialBadge: '7 DIAS GRATIS',
|
||||
monthlyBadge: 'FLEXIBLE',
|
||||
yearlySubline: 'Despues 39.99 EUR/ano. Cancela cuando quieras.',
|
||||
monthlySubline: '4.99 EUR/mes. Sin compromiso anual.',
|
||||
saveLabel: 'Mejor valor',
|
||||
expoGoPurchaseTitle: 'Compra solo en Dev Build',
|
||||
expoGoPurchaseMessage: 'Expo Go no puede mostrar la compra nativa de Apple o RevenueCat. En Development Build o TestFlight aparecera el trial real de 7 dias. Para pruebas locales puedes simular Pro.',
|
||||
expoGoSimulate: 'Simular Pro',
|
||||
perYear: '/ ano',
|
||||
perMonth: '/ mes',
|
||||
freePlanName: 'Gratis',
|
||||
freePlanPrice: '0 EUR / Mes',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4.99 EUR / Mes',
|
||||
@@ -133,17 +176,20 @@ const getBillingCopy = (language: Language) => {
|
||||
proYearlyPlanPrice: '39.99 EUR / Año',
|
||||
proYearlyBadgeText: 'AHORRAR',
|
||||
proBenefits: [
|
||||
'250 créditos cada mes',
|
||||
'100 créditos para escaneos IA y seguimientos cada mes',
|
||||
'Escaneos Pro con GPT-5.4',
|
||||
'Historial y galería ilimitados',
|
||||
'Doctor de plantas de IA incluido',
|
||||
'Soporte prioritario'
|
||||
],
|
||||
topupTitle: 'Recargar Créditos',
|
||||
topupSmall: '25 Créditos – 1,99 €',
|
||||
topupMedium: '120 Créditos – 6,99 €',
|
||||
topupLarge: '300 Créditos – 12,99 €',
|
||||
topupBestValue: 'MEJOR OFERTA',
|
||||
topupHint: 'Para usuarios Pro activos cuando los créditos mensuales no alcanzan.',
|
||||
topupSmall: '30 Créditos – 2,99 €',
|
||||
topupMedium: '100 Créditos – 6,99 €',
|
||||
topupLarge: '250 Créditos – 12,99 €',
|
||||
topupBestValue: 'MEJOR OFERTA',
|
||||
topupRequiresProTitle: 'Pro requerido',
|
||||
topupRequiresProMessage: 'Los top-ups son para usuarios Pro activos. Inicia Pro para comprar créditos adicionales.',
|
||||
cancelTitle: 'Lamentamos verte ir',
|
||||
cancelQuestion: '¿Podemos saber por qué cancelas?',
|
||||
reasonTooExpensive: 'Es muy caro',
|
||||
@@ -169,7 +215,22 @@ const getBillingCopy = (language: Language) => {
|
||||
manageSubscription: 'Manage Subscription',
|
||||
subscriptionTitle: 'Subscriptions',
|
||||
subscriptionHint: 'Choose a plan to unlock stronger AI scans and more credits.',
|
||||
freePlanName: 'Free',
|
||||
paywallTitle: 'Unlock the full diagnosis',
|
||||
paywallHint: 'Start Pro for real GPT-5.4 scans, your 7-day rescue plan, and 100 credits for AI scans and follow-ups.',
|
||||
startTrial: 'Start 7-day free trial',
|
||||
monthlyCta: 'Start monthly',
|
||||
yearlyCta: 'Start yearly',
|
||||
yearlyTrialBadge: '7 DAYS FREE',
|
||||
monthlyBadge: 'FLEXIBLE',
|
||||
yearlySubline: 'Then EUR 39.99/year. Cancel anytime.',
|
||||
monthlySubline: 'EUR 4.99/month. No annual commitment.',
|
||||
saveLabel: 'Best value',
|
||||
expoGoPurchaseTitle: 'Purchase requires a dev build',
|
||||
expoGoPurchaseMessage: 'Expo Go cannot show the native Apple or RevenueCat purchase sheet. In a Development Build or TestFlight this opens the real 7-day trial. For local testing you can simulate Pro.',
|
||||
expoGoSimulate: 'Simulate Pro',
|
||||
perYear: '/ year',
|
||||
perMonth: '/ month',
|
||||
freePlanName: 'Free',
|
||||
freePlanPrice: '0 EUR / Month',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4.99 EUR / Month',
|
||||
@@ -178,17 +239,20 @@ const getBillingCopy = (language: Language) => {
|
||||
proYearlyPlanPrice: '39.99 EUR / Year',
|
||||
proYearlyBadgeText: 'SAVE',
|
||||
proBenefits: [
|
||||
'250 credits every month',
|
||||
'100 credits for AI scans and follow-ups every month',
|
||||
'Pro scans with GPT-5.4',
|
||||
'Unlimited history & gallery',
|
||||
'AI Plant Doctor included',
|
||||
'Priority support'
|
||||
],
|
||||
topupTitle: 'Topup Credits',
|
||||
topupSmall: '25 Credits – €1.99',
|
||||
topupMedium: '120 Credits – €6.99',
|
||||
topupLarge: '300 Credits – €12.99',
|
||||
topupBestValue: 'BEST VALUE',
|
||||
topupHint: 'For active Pro users when monthly credits are not enough.',
|
||||
topupSmall: '30 Credits – €2.99',
|
||||
topupMedium: '100 Credits – €6.99',
|
||||
topupLarge: '250 Credits – €12.99',
|
||||
topupBestValue: 'BEST VALUE',
|
||||
topupRequiresProTitle: 'Pro required',
|
||||
topupRequiresProMessage: 'Top-ups are for active Pro users. Start Pro to buy extra credits.',
|
||||
cancelTitle: 'Sorry to see you go',
|
||||
cancelQuestion: 'May we ask why you are cancelling?',
|
||||
reasonTooExpensive: 'It is too expensive',
|
||||
@@ -225,8 +289,9 @@ export default function BillingScreen() {
|
||||
// Cancel Flow State
|
||||
const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none');
|
||||
|
||||
const planId = billingSummary?.entitlement?.plan || 'free';
|
||||
const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? '--');
|
||||
const planId = billingSummary?.entitlement?.plan || 'free';
|
||||
const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? '--');
|
||||
const showPaywallPlans = !session || planId !== 'pro';
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -274,9 +339,15 @@ export default function BillingScreen() {
|
||||
};
|
||||
}, [isExpoGo]);
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('paywall_viewed', { plan_id: planId });
|
||||
}, [posthog, planId]);
|
||||
useEffect(() => {
|
||||
posthog.capture('paywall_viewed', { plan_id: planId });
|
||||
if (showPaywallPlans) {
|
||||
posthog.capture('hard_paywall_viewed', {
|
||||
plan_id: planId,
|
||||
authenticated: Boolean(session),
|
||||
});
|
||||
}
|
||||
}, [posthog, planId, session?.serverUserId, showPaywallPlans]);
|
||||
|
||||
const monthlyPackage = subscriptionPackages.monthly_pro;
|
||||
const yearlyPackage = subscriptionPackages.yearly_pro;
|
||||
@@ -285,22 +356,60 @@ export default function BillingScreen() {
|
||||
const yearlyPrice = yearlyPackage?.product.priceString ?? copy.proYearlyPlanPrice;
|
||||
|
||||
const topupLabels = useMemo(() => ({
|
||||
topup_small: topupProducts.topup_small ? `25 Credits - ${topupProducts.topup_small.priceString}` : copy.topupSmall,
|
||||
topup_medium: topupProducts.topup_medium ? `120 Credits - ${topupProducts.topup_medium.priceString}` : copy.topupMedium,
|
||||
topup_large: topupProducts.topup_large ? `300 Credits - ${topupProducts.topup_large.priceString}` : copy.topupLarge,
|
||||
topup_small: topupProducts.topup_small ? `${TOPUP_CREDITS_BY_PRODUCT.topup_small} Credits - ${topupProducts.topup_small.priceString}` : copy.topupSmall,
|
||||
topup_medium: topupProducts.topup_medium ? `${TOPUP_CREDITS_BY_PRODUCT.topup_medium} Credits - ${topupProducts.topup_medium.priceString}` : copy.topupMedium,
|
||||
topup_large: topupProducts.topup_large ? `${TOPUP_CREDITS_BY_PRODUCT.topup_large} Credits - ${topupProducts.topup_large.priceString}` : copy.topupLarge,
|
||||
}), [copy.topupLarge, copy.topupMedium, copy.topupSmall, topupProducts.topup_large, topupProducts.topup_medium, topupProducts.topup_small]);
|
||||
|
||||
const openAppleSubscriptions = async () => {
|
||||
await Linking.openURL('itms-apps://apps.apple.com/account/subscriptions');
|
||||
};
|
||||
|
||||
const handlePurchase = async (productId: PurchaseProductId) => {
|
||||
setIsUpdating(true);
|
||||
posthog.capture('purchase_initiated', { product_id: productId });
|
||||
const openAppleSubscriptions = async () => {
|
||||
await Linking.openURL('itms-apps://apps.apple.com/account/subscriptions');
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (showPaywallPlans) {
|
||||
router.replace(session ? '/scanner' : '/onboarding');
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
const completeExpoGoSimulation = async (productId: PurchaseProductId) => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await simulatePurchase(productId);
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
posthog.capture('subscription_started', { product_id: productId, simulated: true });
|
||||
posthog.capture('trial_started', { product_id: productId, simulated: true });
|
||||
} else {
|
||||
posthog.capture('topup_purchased', { product_id: productId, simulated: true });
|
||||
}
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurchase = async (productId: PurchaseProductId) => {
|
||||
if (isTopupProductId(productId) && planId !== 'pro') {
|
||||
Alert.alert(copy.topupRequiresProTitle, copy.topupRequiresProMessage, [
|
||||
{ text: copy.manageSubscription, onPress: () => setSubModalVisible(true) },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdating(true);
|
||||
posthog.capture('purchase_initiated', { product_id: productId });
|
||||
try {
|
||||
if (isExpoGo) {
|
||||
// ExpoGo has no native RevenueCat — use simulation for development only
|
||||
await simulatePurchase(productId);
|
||||
setIsUpdating(false);
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
Alert.alert(copy.expoGoPurchaseTitle, copy.expoGoPurchaseMessage, [
|
||||
{ text: 'OK', style: 'cancel' },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
await completeExpoGoSimulation(productId);
|
||||
return;
|
||||
} else {
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
if (planId === 'pro') {
|
||||
@@ -323,8 +432,7 @@ export default function BillingScreen() {
|
||||
// Derive plan locally from RevenueCat — backend sync via webhook comes later (Step 3)
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
await syncRevenueCatState(customerInfo as any, 'subscription_purchase');
|
||||
posthog.capture('subscription_started', { product_id: productId });
|
||||
} else {
|
||||
} else {
|
||||
const selectedProduct = topupProducts[productId];
|
||||
if (!selectedProduct) {
|
||||
throw new Error('Top-up Produkt konnte nicht geladen werden. Bitte Store-Produkt IDs prüfen.');
|
||||
@@ -332,18 +440,24 @@ export default function BillingScreen() {
|
||||
await Purchases.purchaseStoreProduct(selectedProduct);
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
await syncRevenueCatState(customerInfo as any, 'topup_purchase');
|
||||
posthog.capture('topup_purchased', { product_id: productId });
|
||||
}
|
||||
}
|
||||
setSubModalVisible(false);
|
||||
}
|
||||
}
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
posthog.capture('subscription_started', { product_id: productId });
|
||||
posthog.capture('trial_started', { product_id: productId });
|
||||
} else {
|
||||
posthog.capture('topup_purchased', { product_id: productId });
|
||||
}
|
||||
setSubModalVisible(false);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
const userCancelled = typeof e === 'object' && e !== null && 'userCancelled' in e && Boolean((e as { userCancelled?: boolean }).userCancelled);
|
||||
|
||||
if (userCancelled) {
|
||||
posthog.capture('purchase_cancelled', { product_id: productId });
|
||||
return;
|
||||
}
|
||||
if (userCancelled) {
|
||||
posthog.capture('purchase_cancelled', { product_id: productId });
|
||||
posthog.capture('paywall_purchase_cancelled', { product_id: productId });
|
||||
return;
|
||||
}
|
||||
|
||||
// RevenueCat error code 7 = PRODUCT_ALREADY_PURCHASED — the Apple ID already
|
||||
// owns this subscription on a different GreenLens account. Silently dismiss;
|
||||
@@ -405,7 +519,7 @@ export default function BillingScreen() {
|
||||
<ThemeBackdrop colors={colors} />
|
||||
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
||||
<TouchableOpacity onPress={handleBack} style={styles.backButton}>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
|
||||
@@ -417,7 +531,7 @@ export default function BillingScreen() {
|
||||
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 40 }} />
|
||||
) : (
|
||||
<>
|
||||
{session && (
|
||||
{session && planId === 'pro' && (
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.planLabel}</Text>
|
||||
<View style={[styles.row, { marginBottom: 16 }]}>
|
||||
@@ -436,66 +550,73 @@ export default function BillingScreen() {
|
||||
<Text style={[styles.creditsValue, { color: colors.text }]}>{credits}</Text>
|
||||
</View>
|
||||
)}
|
||||
{!session && (
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>Subscription Plans</Text>
|
||||
<Text style={[styles.modalHint, { color: colors.text + '80', marginBottom: 16 }]}>
|
||||
Choose a plan to unlock AI plant scans and care features.
|
||||
</Text>
|
||||
{showPaywallPlans && (
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.paywallTitle, { color: colors.text }]}>{copy.paywallTitle}</Text>
|
||||
<Text style={[styles.modalHint, { color: colors.text + '80', marginBottom: 16 }]}>
|
||||
{copy.paywallHint}
|
||||
</Text>
|
||||
|
||||
{/* Monthly */}
|
||||
<View style={[styles.guestPlanCard, { borderColor: colors.primary, backgroundColor: colors.primary + '10' }]}>
|
||||
<View style={styles.guestPlanHeader}>
|
||||
<Text style={[styles.guestPlanName, { color: colors.text }]}>GreenLens Pro</Text>
|
||||
<View style={[styles.proBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.proBadgeText}>MONTHLY</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.guestPlanPrice, { color: colors.text }]}>{monthlyPrice}</Text>
|
||||
<Text style={[styles.guestPlanRenew, { color: colors.textMuted }]}>{copy.autoRenewMonthly}</Text>
|
||||
<View style={{ gap: 4, marginTop: 8 }}>
|
||||
{copy.proBenefits.map((b, i) => (
|
||||
<View key={i} style={styles.benefitRow}>
|
||||
<Ionicons name="checkmark" size={14} color={colors.primary} />
|
||||
<Text style={[styles.benefitText, { color: colors.textSecondary }]}>{b}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.guestSubscribeBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
>
|
||||
<Text style={styles.manageBtnText}>Subscribe Monthly</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Yearly */}
|
||||
<View style={[styles.guestPlanCard, { borderColor: colors.border, marginTop: 12 }]}>
|
||||
<View style={styles.guestPlanHeader}>
|
||||
<Text style={[styles.guestPlanName, { color: colors.text }]}>GreenLens Pro</Text>
|
||||
<View style={[styles.proBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.proBadgeText}>YEARLY</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.guestPlanPrice, { color: colors.text }]}>{yearlyPrice}</Text>
|
||||
<Text style={[styles.guestPlanRenew, { color: colors.textMuted }]}>{copy.autoRenewYearly}</Text>
|
||||
<View style={{ gap: 4, marginTop: 8 }}>
|
||||
{copy.proBenefits.map((b, i) => (
|
||||
<View key={i} style={styles.benefitRow}>
|
||||
<Ionicons name="checkmark" size={14} color={colors.primary} />
|
||||
<Text style={[styles.benefitText, { color: colors.textSecondary }]}>{b}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.guestSubscribeBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={() => handlePurchase('yearly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
>
|
||||
<Text style={styles.manageBtnText}>Subscribe Yearly</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.paywallValueRows}>
|
||||
{copy.proBenefits.slice(0, 3).map((benefit, index) => (
|
||||
<View key={benefit} style={[styles.paywallValueRow, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons
|
||||
name={index === 0 ? 'scan-outline' : index === 1 ? 'medkit-outline' : 'calendar-outline'}
|
||||
size={17}
|
||||
color={colors.primary}
|
||||
/>
|
||||
<Text style={[styles.paywallValueText, { color: colors.text }]}>{benefit}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.paywallPlanCardPrimary, { borderColor: colors.primary, backgroundColor: colors.primary + '12' }]}
|
||||
onPress={() => handlePurchase('yearly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<View style={styles.planTopRow}>
|
||||
<View>
|
||||
<Text style={[styles.guestPlanName, { color: colors.text }]}>GreenLens Pro</Text>
|
||||
<Text style={[styles.planSubline, { color: colors.textMuted }]}>{copy.saveLabel}</Text>
|
||||
</View>
|
||||
<View style={[styles.proBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={styles.proBadgeText}>{copy.yearlyTrialBadge}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.priceRow}>
|
||||
<Text style={[styles.guestPlanPrice, { color: colors.text }]}>{yearlyPrice}</Text>
|
||||
<Text style={[styles.planTerm, { color: colors.textMuted }]}>{copy.perYear}</Text>
|
||||
</View>
|
||||
<Text style={[styles.guestPlanRenew, { color: colors.textMuted }]}>{copy.yearlySubline}</Text>
|
||||
<View style={[styles.trialCallout, { backgroundColor: colors.primarySoft }]}>
|
||||
<Ionicons name="sparkles-outline" size={16} color={colors.primary} />
|
||||
<Text style={[styles.trialCalloutText, { color: colors.text }]}>{copy.startTrial}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.paywallPlanCardSecondary, { borderColor: colors.border }]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<View style={styles.planTopRow}>
|
||||
<View>
|
||||
<Text style={[styles.guestPlanName, { color: colors.text }]}>Monatlich</Text>
|
||||
<Text style={[styles.planSubline, { color: colors.textMuted }]}>{copy.monthlySubline}</Text>
|
||||
</View>
|
||||
<View style={[styles.secondaryBadge, { borderColor: colors.borderStrong }]}>
|
||||
<Text style={[styles.secondaryBadgeText, { color: colors.textSecondary }]}>{copy.monthlyBadge}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.priceRow}>
|
||||
<Text style={[styles.guestPlanPrice, { color: colors.text }]}>{monthlyPrice}</Text>
|
||||
<Text style={[styles.planTerm, { color: colors.textMuted }]}>{copy.perMonth}</Text>
|
||||
</View>
|
||||
<Text style={[styles.monthlyCtaText, { color: colors.primary }]}>{copy.monthlyCta}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={[styles.legalLinksRow, { marginTop: 16 }]}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
|
||||
@@ -512,9 +633,11 @@ export default function BillingScreen() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.topupTitle}</Text>
|
||||
<View style={{ gap: 10, marginTop: 8 }}>
|
||||
{session && planId === 'pro' && !isExpoGo ? (
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.topupTitle}</Text>
|
||||
<Text style={[styles.modalHint, { color: colors.text + '80', marginBottom: 8 }]}>{copy.topupHint}</Text>
|
||||
<View style={{ gap: 10, marginTop: 8 }}>
|
||||
{([
|
||||
{ id: 'topup_small' as PurchaseProductId, label: topupLabels.topup_small },
|
||||
{ id: 'topup_medium' as PurchaseProductId, label: topupLabels.topup_medium, badge: copy.topupBestValue },
|
||||
@@ -560,10 +683,11 @@ export default function BillingScreen() {
|
||||
<Text style={[styles.legalLink, { color: colors.primary }]}>Terms of Use</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.restoreBtn} onPress={handleRestore} disabled={isUpdating}>
|
||||
<Text style={[styles.legalLink, { color: colors.textMuted }]}>{copy.restorePurchases}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.restoreBtn} onPress={handleRestore} disabled={isUpdating}>
|
||||
<Text style={[styles.legalLink, { color: colors.textMuted }]}>{copy.restorePurchases}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
@@ -779,11 +903,36 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
creditsValue: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
},
|
||||
topupBtn: {
|
||||
creditsValue: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
},
|
||||
paywallTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
lineHeight: 30,
|
||||
marginBottom: 8,
|
||||
},
|
||||
paywallValueRows: {
|
||||
gap: 8,
|
||||
marginBottom: 14,
|
||||
},
|
||||
paywallValueRow: {
|
||||
minHeight: 42,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 9,
|
||||
},
|
||||
paywallValueText: {
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
lineHeight: 18,
|
||||
},
|
||||
topupBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -940,29 +1089,93 @@ const styles = StyleSheet.create({
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
guestPlanCard: {
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
},
|
||||
guestPlanHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
guestPlanName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
guestPlanPrice: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
marginBottom: 2,
|
||||
},
|
||||
guestPlanRenew: {
|
||||
fontSize: 12,
|
||||
},
|
||||
guestPlanCard: {
|
||||
borderWidth: 2,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
},
|
||||
paywallPlanCardPrimary: {
|
||||
borderWidth: 2,
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
marginTop: 2,
|
||||
},
|
||||
paywallPlanCardSecondary: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
marginTop: 10,
|
||||
},
|
||||
guestPlanHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
planTopRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
gap: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
guestPlanName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
planSubline: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 2,
|
||||
},
|
||||
priceRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
gap: 6,
|
||||
},
|
||||
guestPlanPrice: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
marginBottom: 2,
|
||||
},
|
||||
planTerm: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
guestPlanRenew: {
|
||||
fontSize: 12,
|
||||
lineHeight: 17,
|
||||
},
|
||||
trialCallout: {
|
||||
minHeight: 46,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
marginTop: 14,
|
||||
},
|
||||
trialCalloutText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '800',
|
||||
},
|
||||
secondaryBadge: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
},
|
||||
secondaryBadgeText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
},
|
||||
monthlyCtaText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
marginTop: 8,
|
||||
},
|
||||
guestSubscribeBtn: {
|
||||
marginTop: 14,
|
||||
paddingVertical: 12,
|
||||
|
||||
Reference in New Issue
Block a user