import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, ImageBackground, Platform, useWindowDimensions } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useFocusEffect } from '@react-navigation/native'; import Constants from 'expo-constants'; import Purchases, { LOG_LEVEL, PACKAGE_TYPE, PRODUCT_CATEGORY, PurchasesOffering, PurchasesPackage, PurchasesStoreProduct, } from 'react-native-purchases'; import { useApp } from '../../context/AppContext'; import { useSafeAnalytics } from '../../services/analytics'; import { useColors } from '../../constants/Colors'; import { ThemeBackdrop } from '../../components/ThemeBackdrop'; import { Language } from '../../types'; import { PurchaseProductId } from '../../services/backend/contracts'; type SubscriptionProductId = 'monthly_pro' | 'yearly_pro'; type TopupProductId = Extract; type SubscriptionPackages = Partial>; type TopupProducts = Partial>; type PaywallPlanId = 'weekly' | 'yearly'; const PAYWALL_BACKGROUND = require('../../assets/paywall_scan_background.png'); const TOPUP_CREDITS_BY_PRODUCT: Record = { 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, productId: SubscriptionProductId, expectedPackageType: PACKAGE_TYPE, ) => { return ( pkg.product.identifier === productId || pkg.identifier === productId || pkg.packageType === expectedPackageType ); }; const resolveSubscriptionPackages = (offering: PurchasesOffering | null): SubscriptionPackages => { if (!offering) { return {}; } const availablePackages = [ offering.monthly, offering.annual, ...offering.availablePackages, ].filter((value): value is PurchasesPackage => Boolean(value)); return { monthly_pro: availablePackages.find((pkg) => isMatchingPackage(pkg, 'monthly_pro', PACKAGE_TYPE.MONTHLY)), yearly_pro: availablePackages.find((pkg) => isMatchingPackage(pkg, 'yearly_pro', PACKAGE_TYPE.ANNUAL)), }; }; const summarizeOfferingPackages = (offering: PurchasesOffering | null) => { if (!offering) { return { identifier: null, packages: [] as Array> }; } return { identifier: offering.identifier, packages: offering.availablePackages.map((pkg) => ({ identifier: pkg.identifier, packageType: pkg.packageType, productIdentifier: pkg.product.identifier, priceString: pkg.product.priceString, })), }; }; let revenueCatConfigured = false; const ensureRevenueCatConfigured = () => { if (revenueCatConfigured || Constants.appOwnership === 'expo') { return; } Purchases.setLogLevel(LOG_LEVEL.WARN); const iosApiKey = process.env.EXPO_PUBLIC_REVENUECAT_IOS_API_KEY || 'appl_hrSpsuUuVstbHhYIDnOqYxPOnmR'; const androidApiKey = process.env.EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY || 'goog_placeholder'; if (Platform.OS === 'ios') { Purchases.configure({ apiKey: iosApiKey }); } else if (Platform.OS === 'android') { Purchases.configure({ apiKey: androidApiKey }); } revenueCatConfigured = true; }; const getBillingCopy = (language: Language) => { if (language === 'de') { return { title: 'Abo und Credits', planLabel: 'Aktueller Plan', planFree: 'Free', planPro: 'Pro', creditsAvailableLabel: 'Verfügbare Credits', manageSubscription: 'Abo verwalten', subscriptionTitle: 'Abos', subscriptionHint: 'Wähle ein Abo und schalte stärkere KI-Scans sowie mehr Credits frei.', 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', hardPaywallTitle: 'Pflanze scannen. Richtig pflegen.', hardPaywallHint: 'GreenLens erkennt Art, Pflegebedarf und mögliche Probleme in Sekunden.', identifiedChip: 'Monstera erkannt', benefitUnlimitedScans: 'Unbegrenzte Scans', benefitCarePlan: 'Pflegeplan + Erinnerungen', weeklyPlanName: 'Monatlich', yearlyPlanName: 'Jährlich', weeklyPriceFallback: '5,99 € / Monat', yearlyPriceFallback: '39,99 € / Jahr', bestOffer: 'Bestes Angebot', noPaymentToday: 'Keine Zahlung heute · jederzeit kündbar', monthlyFooter: 'Monatlich kündbar · kein Gratis-Test', 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', continueWithoutPro: 'Ohne Pro fortfahren', perYear: '/ Jahr', perMonth: '/ Monat', freePlanName: 'Free', freePlanPrice: '0 EUR / Monat', proPlanName: 'Pro', proPlanPrice: '4,99 € / Monat', proBadgeText: 'EMPFOHLEN', proYearlyPlanName: 'Pro', proYearlyPlanPrice: '39,99 € / Jahr', proYearlyBadgeText: 'SPAREN', proBenefits: [ '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', 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', reasonNotUsing: 'Ich nutze die App zu selten', reasonOther: 'Ein anderer Grund', offerTitle: 'Ein Geschenk für dich!', offerText: 'Bleib dabei und erhalte den nächsten Monat für nur 2,49 € (50% Rabatt).', offerAccept: 'Rabatt sichern', offerDecline: 'Nein, Kündigung fortsetzen', confirmCancelBtn: 'Jetzt kündigen', restorePurchases: 'Käufe wiederherstellen', autoRenewMonthly: 'Verlängert sich monatlich automatisch. Jederzeit über iOS-Einstellungen kündbar.', autoRenewYearly: 'Verlängert sich jährlich automatisch. Jederzeit über iOS-Einstellungen kündbar.', manageInSettings: 'In iOS-Einstellungen verwalten', }; } else if (language === 'es') { return { title: 'Suscripción y Créditos', planLabel: 'Plan Actual', planFree: 'Gratis', planPro: 'Pro', creditsAvailableLabel: 'Créditos Disponibles', manageSubscription: 'Administrar Suscripción', subscriptionTitle: 'Suscripciones', subscriptionHint: 'Elige un plan y desbloquea escaneos con IA más potentes y más créditos.', 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', hardPaywallTitle: 'Escanea plantas. Cuídalas bien.', hardPaywallHint: 'GreenLens identifica la especie, los cuidados y posibles problemas en segundos.', identifiedChip: 'Monstera identificada', benefitUnlimitedScans: 'Escaneos ilimitados', benefitCarePlan: 'Plan de cuidado + recordatorios', weeklyPlanName: 'Mensual', yearlyPlanName: 'Anual', weeklyPriceFallback: '5,99 € / mes', yearlyPriceFallback: '39,99 € / año', bestOffer: 'Mejor oferta', noPaymentToday: 'Sin pago hoy · cancela cuando quieras', monthlyFooter: 'Cancela mensualmente · sin prueba 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', continueWithoutPro: 'Continuar sin Pro', perYear: '/ ano', perMonth: '/ mes', freePlanName: 'Gratis', freePlanPrice: '0 EUR / Mes', proPlanName: 'Pro', proPlanPrice: '4.99 EUR / Mes', proBadgeText: 'RECOMENDADO', proYearlyPlanName: 'Pro', proYearlyPlanPrice: '39.99 EUR / Año', proYearlyBadgeText: 'AHORRAR', proBenefits: [ '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', 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', reasonNotUsing: 'No lo uso suficiente', reasonOther: 'Otra razón', offerTitle: '¡Un regalo para ti!', offerText: 'Quédate y obtén el próximo mes por solo 2,49 € (50% de descuento).', offerAccept: 'Aceptar descuento', offerDecline: 'No, continuar cancelando', confirmCancelBtn: 'Cancelar ahora', restorePurchases: 'Restaurar Compras', autoRenewMonthly: 'Se renueva mensualmente de forma automática. Cancela cuando quieras en Ajustes de iOS.', autoRenewYearly: 'Se renueva anualmente de forma automática. Cancela cuando quieras en Ajustes de iOS.', manageInSettings: 'Administrar en Ajustes de iOS', }; } return { title: 'Billing & Credits', planLabel: 'Current Plan', planFree: 'Free', planPro: 'Pro', creditsAvailableLabel: 'Available Credits', manageSubscription: 'Manage Subscription', subscriptionTitle: 'Subscriptions', subscriptionHint: 'Choose a plan to unlock stronger AI scans and more credits.', 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', hardPaywallTitle: 'Scan plants. Care for them right.', hardPaywallHint: 'GreenLens identifies the species, care needs, and possible problems in seconds.', identifiedChip: 'Monstera identified', benefitUnlimitedScans: 'Unlimited scans', benefitCarePlan: 'Care plan + reminders', weeklyPlanName: 'Monthly', yearlyPlanName: 'Yearly', weeklyPriceFallback: '$4.99 / month', yearlyPriceFallback: '$39.99 / year', bestOffer: 'Best offer', noPaymentToday: 'No payment today · cancel anytime', monthlyFooter: 'Cancel monthly · no 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', continueWithoutPro: 'Continue without Pro', perYear: '/ year', perMonth: '/ month', freePlanName: 'Free', freePlanPrice: '0 EUR / Month', proPlanName: 'Pro', proPlanPrice: '4.99 EUR / Month', proBadgeText: 'RECOMMENDED', proYearlyPlanName: 'Pro', proYearlyPlanPrice: '39.99 EUR / Year', proYearlyBadgeText: 'SAVE', proBenefits: [ '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', 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', reasonNotUsing: 'I don\'t use it enough', reasonOther: 'Other reason', offerTitle: 'A gift for you!', offerText: 'Stay with us and get your next month for just €2.49 (50% off).', offerAccept: 'Claim discount', offerDecline: 'No, continue cancelling', confirmCancelBtn: 'Cancel now', restorePurchases: 'Restore Purchases', autoRenewMonthly: 'Auto-renews monthly. Cancel anytime in iOS Settings.', autoRenewYearly: 'Auto-renews annually. Cancel anytime in iOS Settings.', manageInSettings: 'Manage in iOS Settings', }; }; export default function BillingScreen() { const router = useRouter(); const { isDarkMode, language, billingSummary, isLoadingBilling, simulatePurchase, simulateWebhookEvent, syncRevenueCatState, colorPalette, session } = useApp(); const colors = useColors(isDarkMode, colorPalette); const posthog = useSafeAnalytics(); const copy = getBillingCopy(language); const isExpoGo = Constants.appOwnership === 'expo'; const { height: windowHeight } = useWindowDimensions(); const compactPaywall = windowHeight < 760; const [subModalVisible, setSubModalVisible] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [storeReady, setStoreReady] = useState(isExpoGo); const [storeError, setStoreError] = useState(null); const [subscriptionPackages, setSubscriptionPackages] = useState({}); const [topupProducts, setTopupProducts] = useState({}); const [selectedPaywallPlan, setSelectedPaywallPlan] = useState('yearly'); // Cancel Flow State const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none'); const planId = billingSummary?.entitlement?.plan || 'free'; const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? 0); const showPaywallPlans = !session || (!isLoadingBilling && planId !== 'pro'); useEffect(() => { let cancelled = false; const loadStoreProducts = async () => { if (isExpoGo) { setStoreReady(true); return; } try { ensureRevenueCatConfigured(); const [offerings, topups] = await Promise.all([ Purchases.getOfferings(), Purchases.getProducts(['topup_small', 'topup_medium', 'topup_large'], PRODUCT_CATEGORY.NON_SUBSCRIPTION), ]); if (cancelled) return; const currentOffering = offerings.current; const resolvedPackages = resolveSubscriptionPackages(currentOffering); if (!resolvedPackages.monthly_pro || !resolvedPackages.yearly_pro) { console.warn('[Billing] RevenueCat offering missing expected subscription packages', summarizeOfferingPackages(currentOffering)); } setSubscriptionPackages(resolvedPackages); setTopupProducts({ topup_small: topups.find((product) => product.identifier === 'topup_small'), topup_medium: topups.find((product) => product.identifier === 'topup_medium'), topup_large: topups.find((product) => product.identifier === 'topup_large'), }); setStoreError(null); } catch (error) { console.warn('Failed to load RevenueCat products', error); if (!cancelled) { setStoreError('Purchases are temporarily unavailable. Please try again later.'); } } finally { if (!cancelled) { setStoreReady(true); } } }; loadStoreProducts(); return () => { cancelled = true; }; }, [isExpoGo]); useEffect(() => { try { posthog.capture('paywall_viewed', { plan_id: planId }); } catch {} if (showPaywallPlans) { try { posthog.capture('hard_paywall_viewed', { plan_id: planId, authenticated: Boolean(session), }); } catch {} } }, [posthog, planId, session?.serverUserId, showPaywallPlans]); const monthlyPackage = subscriptionPackages.monthly_pro; const yearlyPackage = subscriptionPackages.yearly_pro; const monthlyPrice = monthlyPackage?.product.priceString ?? copy.proPlanPrice; const yearlyPrice = yearlyPackage?.product.priceString ?? copy.proYearlyPlanPrice; const weeklyDisplayPrice = copy.weeklyPriceFallback; const yearlyDisplayPrice = copy.yearlyPriceFallback; const selectedProductId: SubscriptionProductId = selectedPaywallPlan === 'yearly' ? 'yearly_pro' : 'monthly_pro'; const paywallCtaLabel = selectedPaywallPlan === 'yearly' ? copy.startTrial : copy.monthlyCta; const paywallFooterLabel = selectedPaywallPlan === 'yearly' ? copy.noPaymentToday : copy.monthlyFooter; const topupLabels = useMemo(() => ({ 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 handleBack = useCallback(() => { if (showPaywallPlans) { router.replace('/onboarding'); return; } if (router.canGoBack()) { router.back(); return; } router.replace('/(tabs)'); }, [router, showPaywallPlans]); useFocusEffect( useCallback(() => { const subscription = BackHandler.addEventListener('hardwareBackPress', () => { if (!showPaywallPlans) { return false; } router.replace('/onboarding'); return true; }); return () => subscription.remove(); }, [router, showPaywallPlans]), ); 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 }); setSubModalVisible(false); router.replace('/(tabs)'); } 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; } if (!isExpoGo && storeError) { Alert.alert('Purchases unavailable', storeError); return; } setIsUpdating(true); posthog.capture('purchase_initiated', { product_id: productId }); try { if (isExpoGo) { // ExpoGo has no native RevenueCat — use simulation for development only setIsUpdating(false); if (productId === 'monthly_pro' || productId === 'yearly_pro') { Alert.alert(copy.expoGoPurchaseTitle, copy.expoGoPurchaseMessage, [ { text: copy.continueWithoutPro, style: 'cancel' }, { text: copy.expoGoSimulate, onPress: () => completeExpoGoSimulation(productId) }, ]); return; } await completeExpoGoSimulation(productId); return; } else { ensureRevenueCatConfigured(); if (productId === 'monthly_pro' || productId === 'yearly_pro') { if (planId === 'pro') { await openAppleSubscriptions(); setSubModalVisible(false); return; } const selectedPackage = productId === 'monthly_pro' ? monthlyPackage : yearlyPackage; const latestOffering = !selectedPackage ? await Purchases.getOfferings().then((offerings) => offerings.current) : null; if (!selectedPackage) { console.warn('[Billing] Purchase blocked because subscription package was not resolved', { productId, offering: summarizeOfferingPackages(latestOffering), }); throw new Error('Abo-Paket konnte nicht geladen werden. Bitte RevenueCat Offering prüfen.'); } const purchaseResult = await Purchases.purchasePackage(selectedPackage); // Apply RevenueCat entitlement locally and let backend sync finish in the background. const customerInfo = (purchaseResult as { customerInfo?: unknown }).customerInfo ?? await Purchases.getCustomerInfo(); void syncRevenueCatState(customerInfo as any, 'subscription_purchase'); posthog.capture('subscription_started', { product_id: productId }); posthog.capture('trial_started', { product_id: productId }); setSubModalVisible(false); setTimeout(() => router.replace('/(tabs)'), 0); return; } else { const selectedProduct = topupProducts[productId]; if (!selectedProduct) { throw new Error('Top-up Produkt konnte nicht geladen werden. Bitte Store-Produkt IDs prüfen.'); } 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); } 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 }); 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; // the current account stays free. The user can restore via "Käufe wiederherstellen". const rcErrorCode = typeof e === 'object' && e !== null ? (e as Record).code : undefined; if (rcErrorCode === 7) { setSubModalVisible(false); return; } console.error('Payment failed', e); posthog.capture('purchase_failed', { product_id: productId, error: msg }); Alert.alert('Unerwarteter Fehler', msg); } finally { setIsUpdating(false); } }; const handleRestore = async () => { setIsUpdating(true); try { if (!isExpoGo) { ensureRevenueCatConfigured(); const customerInfo = await Purchases.restorePurchases(); await syncRevenueCatState(customerInfo as any, 'restore'); } Alert.alert(copy.restorePurchases, '✓'); } catch (e) { Alert.alert('Error', e instanceof Error ? e.message : String(e)); } finally { setIsUpdating(false); } }; const handleDowngrade = async () => { if (planId === 'free') return; if (!isExpoGo) { await openAppleSubscriptions(); return; } // Expo Go / dev only: simulate cancel flow setCancelStep('survey'); }; const finalizeCancel = async () => { setIsUpdating(true); try { await simulateWebhookEvent('entitlement_revoked'); setCancelStep('none'); setSubModalVisible(false); } catch (e) { console.error('Downgrade failed', e); } finally { setIsUpdating(false); } }; if (showPaywallPlans) { return ( {copy.identifiedChip} {copy.hardPaywallTitle} {copy.hardPaywallHint} {[ { icon: 'infinite-outline' as const, title: copy.benefitUnlimitedScans }, { icon: 'calendar-outline' as const, title: copy.benefitCarePlan }, ].map((benefit) => ( {benefit.title} ))} {[ { id: 'weekly' as PaywallPlanId, name: copy.weeklyPlanName, price: weeklyDisplayPrice }, { id: 'yearly' as PaywallPlanId, name: copy.yearlyPlanName, price: yearlyDisplayPrice, badge: copy.bestOffer }, ].map((plan) => { const selected = selectedPaywallPlan === plan.id; return ( setSelectedPaywallPlan(plan.id)} activeOpacity={0.88} > {selected ? : null} {plan.name} {plan.price} {plan.badge ? ( {plan.badge} ) : null} ); })} handlePurchase(selectedProductId)} disabled={isUpdating || !storeReady || Boolean(storeError)} activeOpacity={0.9} > {isUpdating || !storeReady ? ( ) : ( <> {paywallCtaLabel} )} {storeError ? ( {storeError} ) : null} {paywallFooterLabel} Linking.openURL('https://greenlenspro.com/privacy')}> Privacy Policy · Linking.openURL('https://greenlenspro.com/terms')}> Terms of Use {copy.restorePurchases} ); } return ( {copy.title} {isLoadingBilling && session ? ( ) : ( <> {session && planId === 'pro' && ( {copy.planLabel} {planId === 'pro' ? copy.planPro : copy.planFree} setSubModalVisible(true)} > {copy.manageSubscription} {copy.creditsAvailableLabel} {credits} )} {showPaywallPlans && ( {copy.paywallTitle} {copy.paywallHint} {copy.proBenefits.slice(0, 3).map((benefit, index) => ( {benefit} ))} handlePurchase('yearly_pro')} disabled={isUpdating || !storeReady || Boolean(storeError)} activeOpacity={0.9} > GreenLens Pro {copy.saveLabel} {copy.yearlyTrialBadge} {yearlyPrice} {copy.perYear} {copy.yearlySubline} {copy.startTrial} handlePurchase('monthly_pro')} disabled={isUpdating || !storeReady || Boolean(storeError)} activeOpacity={0.9} > Monatlich {copy.monthlySubline} {copy.monthlyBadge} {monthlyPrice} {copy.perMonth} {!storeReady ? ( ) : ( <> {copy.monthlyCta} )} Linking.openURL('https://greenlenspro.com/privacy')}> Privacy Policy · Linking.openURL('https://greenlenspro.com/terms')}> Terms of Use {copy.restorePurchases} )} {session && planId === 'pro' && !isExpoGo ? ( {copy.topupTitle} {copy.topupHint} {([ { id: 'topup_small' as PurchaseProductId, label: topupLabels.topup_small }, { id: 'topup_medium' as PurchaseProductId, label: topupLabels.topup_medium, badge: copy.topupBestValue }, { id: 'topup_large' as PurchaseProductId, label: topupLabels.topup_large }, ] as { id: PurchaseProductId; label: string; badge?: string }[]).map((pack) => ( handlePurchase(pack.id)} disabled={isUpdating || !storeReady || Boolean(storeError)} > {isUpdating ? '...' : pack.label} {pack.badge && ( {pack.badge} )} ))} Linking.openURL('https://greenlenspro.com/privacy')}> Privacy Policy · Linking.openURL('https://greenlenspro.com/terms')}> Terms of Use {copy.restorePurchases} ) : null} )} setSubModalVisible(false)}> {cancelStep === 'survey' ? copy.cancelTitle : cancelStep === 'offer' ? copy.offerTitle : copy.subscriptionTitle} { setSubModalVisible(false); setCancelStep('none'); }}> {cancelStep === 'none' ? ( <> {copy.subscriptionHint} {copy.freePlanName} {copy.freePlanPrice} {planId === 'free' && } handlePurchase('monthly_pro')} disabled={isUpdating || !storeReady || Boolean(storeError)} > {copy.proPlanName} {copy.proBadgeText} {monthlyPrice} {copy.autoRenewMonthly} {copy.proBenefits.map((b, i) => ( {b} ))} {planId === 'pro' && } handlePurchase('yearly_pro')} disabled={isUpdating || !storeReady || Boolean(storeError)} > {copy.proYearlyPlanName} {copy.proYearlyBadgeText} {yearlyPrice} {copy.autoRenewYearly} {copy.proBenefits.map((b, i) => ( {b} ))} {planId === 'pro' && } Linking.openURL('https://greenlenspro.com/privacy')}> Privacy Policy · Linking.openURL('https://greenlenspro.com/terms')}> Terms of Use {copy.restorePurchases} ) : cancelStep === 'survey' ? ( {copy.cancelQuestion} {[ { id: 'expensive', label: copy.reasonTooExpensive, icon: 'cash-outline' }, { id: 'not_using', label: copy.reasonNotUsing, icon: 'calendar-outline' }, { id: 'other', label: copy.reasonOther, icon: 'ellipsis-horizontal-outline' }, ].map((reason) => ( { setCancelStep('offer'); }} > {reason.label} ))} ) : ( {copy.offerText} { // Handle applying discount here (future implementation) Alert.alert('Erfolg', 'Rabatt angewendet! (Mock)'); setCancelStep('none'); setSubModalVisible(false); }} > {copy.offerAccept} {copy.offerDecline} )} {(isUpdating || (!storeReady && cancelStep === 'none')) && } ); } const styles = StyleSheet.create({ hardPaywallScreen: { flex: 1, backgroundColor: '#101411', }, hardPaywallHero: { flex: 1, }, hardPaywallHeroImage: { transform: [{ translateY: -38 }, { scale: 1.06 }], }, hardPaywallSafe: { flex: 1, justifyContent: 'space-between', }, hardPaywallSafeCompact: { justifyContent: 'flex-end', }, heroTopBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingTop: 4, }, heroIconButton: { width: 42, height: 42, borderRadius: 21, alignItems: 'center', justifyContent: 'center', backgroundColor: '#00000066', }, heroRestoreText: { color: '#FFFFFF', fontSize: 13, fontWeight: '700', textShadowColor: '#00000066', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 3, }, scanFrameOverlay: { position: 'absolute', top: '14%', left: 54, right: 54, height: '28%', }, scanFrameOverlayCompact: { display: 'none', }, scanCorner: { position: 'absolute', width: 44, height: 44, borderColor: '#FFFFFF', }, scanCornerTopLeft: { top: 0, left: 0, borderTopWidth: 4, borderLeftWidth: 4, borderTopLeftRadius: 18, }, scanCornerTopRight: { top: 0, right: 0, borderTopWidth: 4, borderRightWidth: 4, borderTopRightRadius: 18, }, scanCornerBottomLeft: { bottom: 0, left: 0, borderBottomWidth: 4, borderLeftWidth: 4, borderBottomLeftRadius: 18, }, scanCornerBottomRight: { bottom: 0, right: 0, borderBottomWidth: 4, borderRightWidth: 4, borderBottomRightRadius: 18, }, identifiedChip: { position: 'absolute', alignSelf: 'center', top: '43%', zIndex: 4, flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 18, paddingHorizontal: 16, paddingVertical: 8, backgroundColor: '#16351FCC', }, identifiedChipCompact: { display: 'none', }, identifiedChipText: { color: '#FFFFFF', fontSize: 15, fontWeight: '800', }, hardPaywallSheet: { backgroundColor: '#FAFAF5', borderTopLeftRadius: 30, borderTopRightRadius: 30, paddingHorizontal: 20, paddingTop: 9, paddingBottom: 18, zIndex: 5, }, hardPaywallSheetCompact: { paddingHorizontal: 18, paddingTop: 8, paddingBottom: 16, }, sheetHandle: { alignSelf: 'center', width: 42, height: 5, borderRadius: 999, backgroundColor: '#C7C8C3', marginBottom: 8, }, sheetHandleCompact: { marginBottom: 8, }, hardPaywallTitle: { color: '#101411', fontSize: 23, lineHeight: 28, fontWeight: '900', textAlign: 'center', marginBottom: 5, }, hardPaywallTitleCompact: { fontSize: 22, lineHeight: 26, }, hardPaywallHint: { color: '#676C66', fontSize: 12, lineHeight: 17, textAlign: 'center', marginBottom: 10, }, hardPaywallHintCompact: { display: 'none', }, hardBenefits: { gap: 5, marginBottom: 7, }, hardBenefitsCompact: { marginBottom: 10, }, hardBenefitRow: { flexDirection: 'row', alignItems: 'center', gap: 12, }, hardBenefitRowCompact: { gap: 9, }, hardBenefitIcon: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center', backgroundColor: '#E3EFD9', }, hardBenefitIconCompact: { width: 32, height: 32, borderRadius: 16, }, hardBenefitText: { color: '#101411', fontSize: 13, fontWeight: '800', }, hardBenefitTextCompact: { fontSize: 13, }, hardPlanRow: { flexDirection: 'row', gap: 10, marginBottom: 7, }, hardPlanCard: { flex: 1, minHeight: 76, borderRadius: 14, borderWidth: 1.5, borderColor: '#D8DAD3', padding: 9, flexDirection: 'row', gap: 10, backgroundColor: '#FFFFFF', }, hardPlanCardCompact: { minHeight: 76, padding: 9, gap: 8, }, hardPlanCardSelected: { borderColor: '#1F5B34', borderWidth: 2, backgroundColor: '#FBFFF8', }, planRadio: { width: 20, height: 20, borderRadius: 11, borderWidth: 2, borderColor: '#8C918A', alignItems: 'center', justifyContent: 'center', marginTop: 2, }, planRadioSelected: { backgroundColor: '#1F5B34', borderColor: '#1F5B34', }, hardPlanName: { color: '#101411', fontSize: 14, fontWeight: '900', marginBottom: 4, }, hardPlanNameCompact: { fontSize: 13, marginBottom: 2, }, hardPlanPrice: { color: '#626862', fontSize: 11, fontWeight: '700', lineHeight: 18, }, hardPlanPriceCompact: { fontSize: 11, lineHeight: 15, }, bestOfferBadge: { alignSelf: 'flex-start', backgroundColor: '#DDEBCF', borderRadius: 999, paddingHorizontal: 8, paddingVertical: 3, marginTop: 5, }, bestOfferText: { color: '#1F5B34', fontSize: 10, fontWeight: '900', }, hardPaywallCta: { minHeight: 50, borderRadius: 14, backgroundColor: '#1F5B34', alignItems: 'center', justifyContent: 'center', flexDirection: 'row', gap: 12, marginBottom: 5, }, hardPaywallCtaCompact: { minHeight: 48, marginBottom: 8, }, hardPaywallCtaText: { color: '#FFFFFF', fontSize: 15, fontWeight: '900', }, hardPaywallCtaTextCompact: { fontSize: 14, }, hardPaywallFooter: { color: '#777C75', fontSize: 12, fontWeight: '600', textAlign: 'center', marginBottom: 5, }, safeArea: { flex: 1 }, header: { flexDirection: 'row', alignItems: 'center', padding: 16 }, backButton: { width: 40, height: 40, justifyContent: 'center' }, title: { flex: 1, fontSize: 20, fontWeight: '700', textAlign: 'center' }, scrollContent: { padding: 16, gap: 16 }, card: { padding: 16, borderRadius: 16, borderWidth: StyleSheet.hairlineWidth, }, sectionTitle: { fontSize: 14, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 8, }, row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, value: { fontSize: 18, fontWeight: '600', }, manageBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, }, manageBtnText: { color: '#fff', fontSize: 14, fontWeight: '600', }, 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', paddingVertical: 12, borderRadius: 12, borderWidth: 2, gap: 8, }, topupText: { fontSize: 16, fontWeight: '600', }, modalOverlay: { flex: 1, backgroundColor: '#00000080', justifyContent: 'flex-end', }, modalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, borderTopWidth: StyleSheet.hairlineWidth, paddingBottom: 40, }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, }, modalTitle: { fontSize: 20, fontWeight: '700', }, modalHint: { fontSize: 14, marginBottom: 24, }, plansContainer: { gap: 12, }, planOption: { padding: 16, borderRadius: 12, borderWidth: 2, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, planName: { fontSize: 18, fontWeight: '600', }, planPrice: { fontSize: 14, }, planHeaderRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 2, }, proBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, }, proBadgeText: { color: '#fff', fontSize: 10, fontWeight: '800', }, proBenefits: { marginTop: 12, gap: 6, }, benefitRow: { flexDirection: 'row', alignItems: 'center', gap: 6, }, benefitText: { fontSize: 12, fontWeight: '500', }, cancelFlowContainer: { marginTop: 8, }, cancelHint: { fontSize: 15, marginBottom: 16, }, reasonList: { gap: 12, }, reasonOption: { flexDirection: 'row', alignItems: 'center', padding: 16, borderWidth: 1, borderRadius: 12, }, reasonIcon: { width: 36, height: 36, borderRadius: 18, justifyContent: 'center', alignItems: 'center', marginRight: 12, }, reasonText: { flex: 1, fontSize: 16, fontWeight: '500', }, offerCard: { borderRadius: 16, padding: 24, alignItems: 'center', marginBottom: 16, }, offerIconWrap: { width: 56, height: 56, borderRadius: 28, justifyContent: 'center', alignItems: 'center', marginBottom: 16, }, offerText: { fontSize: 16, textAlign: 'center', lineHeight: 24, marginBottom: 24, fontWeight: '500', }, offerAcceptBtn: { paddingHorizontal: 24, paddingVertical: 14, borderRadius: 24, width: '100%', alignItems: 'center', }, offerAcceptBtnText: { color: '#fff', fontSize: 16, fontWeight: '700', }, offerDeclineBtn: { paddingVertical: 12, alignItems: 'center', }, offerDeclineBtnText: { fontSize: 15, fontWeight: '500', }, 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, }, disabledPlanCard: { opacity: 0.72, }, 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, flexShrink: 0, }, secondaryBadgeText: { fontSize: 10, fontWeight: '800', }, monthlyCtaText: { fontSize: 13, fontWeight: '800', }, monthlyCtaButton: { minHeight: 42, borderRadius: 12, borderWidth: 1, marginTop: 12, paddingHorizontal: 14, paddingVertical: 10, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, }, guestSubscribeBtn: { marginTop: 14, paddingVertical: 12, borderRadius: 10, alignItems: 'center', }, legalLinksRow: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 16, }, legalLink: { fontSize: 12, fontWeight: '500', textDecorationLine: 'underline', }, legalSep: { fontSize: 12, }, restoreBtn: { alignItems: 'center', paddingVertical: 8, }, hardRestoreBtn: { alignItems: 'center', paddingVertical: 6, marginTop: 4, }, hardRestoreText: { color: '#7A8079', fontSize: 12, fontWeight: '500', textDecorationLine: 'underline', }, autoRenewText: { fontSize: 11, marginTop: 2, marginBottom: 4, }, });