App copy
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, ImageBackground, Platform, Switch, useWindowDimensions } from 'react-native';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, Platform, useWindowDimensions } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
@@ -26,7 +26,6 @@ type SubscriptionPackages = Partial<Record<SubscriptionProductId, PurchasesPacka
|
||||
type TopupProducts = Partial<Record<TopupProductId, PurchasesStoreProduct>>;
|
||||
type PaywallPlanId = 'monthly' | 'yearly';
|
||||
|
||||
const PAYWALL_BACKGROUND = require('../../assets/paywall_scan_background.png');
|
||||
|
||||
const TOPUP_CREDITS_BY_PRODUCT: Record<TopupProductId, number> = {
|
||||
topup_small: 30,
|
||||
@@ -121,27 +120,35 @@ const getBillingCopy = (language: Language) => {
|
||||
freePlanPrice: '0 EUR / Monat',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4,99 € / Monat',
|
||||
// Nackter Betrag: planCardPriceTrial/-Monthly haengen die Periode selbst an.
|
||||
proPlanPriceBare: '4,99 €',
|
||||
proYearlyPlanPriceBare: '39,99 €',
|
||||
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',
|
||||
'100 Scans und Nachfragen jeden Monat',
|
||||
'Ursachen-Ranking mit Angabe, wie sicher wir sind',
|
||||
'Konkrete Prüfschritte statt allgemeiner Pflegetipps',
|
||||
'7-Tage-Plan pro Problem, plus Nachfrage danach',
|
||||
'Komplette Historie & Galerie',
|
||||
'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 €',
|
||||
topupSmall: '30 Credits - 2,99 €',
|
||||
topupMedium: '100 Credits - 6,99 €',
|
||||
topupLarge: '250 Credits - 12,99 €',
|
||||
topupBestValue: 'BESTES ANGEBOT',
|
||||
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',
|
||||
pauseTitle: 'Dann pausier es doch einfach.',
|
||||
pauseText: 'Wir setzen dein Abo für 3 Monate aus. Deine Pflanzen, deine Historie und deine Pläne bleiben erhalten - es wird nichts abgebucht.',
|
||||
pauseAccept: '3 Monate pausieren',
|
||||
pauseDecline: 'Nein, trotzdem kündigen',
|
||||
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).',
|
||||
@@ -153,14 +160,55 @@ const getBillingCopy = (language: Language) => {
|
||||
autoRenewYearly: 'Verlängert sich jährlich automatisch. Jederzeit über iOS-Einstellungen kündbar.',
|
||||
manageInSettings: 'In iOS-Einstellungen verwalten',
|
||||
paywallEyebrow: 'GreenLens Pro',
|
||||
paywallHeadline: 'Unbegrenzter Zugriff',
|
||||
paywallSub: 'Unbegrenzte Scans, Health-Checks und dein persönlicher Pflegeplan.',
|
||||
// "Unbegrenzt" ist ersatzlos gestrichen: es stand im direkten
|
||||
// Widerspruch zu "100 Credits/Monat" drei Zeilen weiter unten.
|
||||
paywallHeadline: 'Nicht mehr raten, was deiner Pflanze fehlt.',
|
||||
// Kurz gehalten: die Details stehen direkt darunter in den Bullets.
|
||||
// Sub und Bullets doppelt zu bespielen kostet Aufmerksamkeit und
|
||||
// liest sich, als traute man dem eigenen Argument nicht.
|
||||
paywallSub: 'Ursache, Prüfschritte, Sofortmaßnahme - und ein Check nach 7 Tagen.',
|
||||
planCardTitle: 'GreenLens Pro',
|
||||
planCardBody: 'Unbegrenzte KI-Scans, Gesundheitsdiagnose, 7-Tage-Rettungspläne, 100 Credits/Monat',
|
||||
// "Gesundheitsdiagnose" -> "Ursachen-Analyse": eine Diagnose koennen
|
||||
// wir nicht versprechen, eine gewichtete Ursachenliste schon.
|
||||
planCardBody: '100 Scans & Nachfragen pro Monat',
|
||||
paywallBullets: [
|
||||
'Ursachen-Ranking - inklusive dem, was unsicher ist',
|
||||
'Konkrete Prüfschritte statt allgemeiner Pflegetipps',
|
||||
'7-Tage-Plan, plus Nachfrage, ob es wirkt',
|
||||
'Komplette Historie deiner Pflanzen',
|
||||
],
|
||||
// Belief 5 explizit: der Vergleich gegen die kostenlose Alternative
|
||||
// gehoert an die Stelle, an der die Kaufentscheidung faellt.
|
||||
paywallCompare: 'Kostenlose Tools sagen dir, wie deine Pflanze heißt. Sie sagen dir nicht, was zu prüfen ist, was zu tun ist oder ob es gewirkt hat.',
|
||||
badgeMostPopular: 'BELIEBT',
|
||||
badgeFlexible: 'FLEXIBEL',
|
||||
planYearlyName: 'Jährlich - 7 Tage gratis',
|
||||
planMonthlyName: 'Monatlich',
|
||||
saveBadge: '33 % SPAREN',
|
||||
perYear: '/ Jahr',
|
||||
perMonth: '/ Monat',
|
||||
breaksDownTo: 'Macht nur',
|
||||
// Nur der Wert, kein ganzer Satz: das Label links sagt bereits
|
||||
// "Macht nur" - der Satz "entspricht X pro Monat" doppelte das und
|
||||
// lief aus der Zeile.
|
||||
equivalentValue: (price: string) => `${price} / Monat`,
|
||||
faqQ1: '"Kostenlose Tools gibt es doch."',
|
||||
faqQ2: '"Eine andere App lag selbstbewusst falsch."',
|
||||
faqA2: 'Wir zeigen dir zu jeder Ursache, wie sicher wir sind. Unter 65 % sagen wir das deutlich - und nennen dir die Prüfschritte, mit denen du es selbst bestätigst oder ausschließt.',
|
||||
faqQ3: '"Und wenn es nicht hilft?"',
|
||||
faqA3: 'Nach 7 Tagen fragen wir nach. Wird es nicht besser, passen wir den Plan an. Kündigen geht in 2 Taps über die iOS-Einstellungen - wir fragen kurz nach dem Grund, das war’s.',
|
||||
faqScope: 'Enthalten: 100 Scans und Nachfragen pro Monat, komplette Historie deiner Pflanzen.',
|
||||
altMonthly: (price: string) => `oder ${price} monatlich, ohne Testphase`,
|
||||
altYearly: (price: string) => `oder ${price} jährlich - mit 7 Tagen gratis`,
|
||||
dueSummary: (today: string, later: string, date: string) =>
|
||||
`Heute fällig: ${today} - dann ${later} am ${date}`,
|
||||
detailsToggle: 'Fragen, die du jetzt vielleicht hast',
|
||||
trialReminder: 'Wir erinnern dich 2 Tage vor Ablauf der Testphase.',
|
||||
cancelPath: 'Kündigen in 2 Taps über die iOS-Einstellungen - Anleitung findest du in der App.',
|
||||
planCardPriceTrial: (price: string) => `7 Tage gratis, dann ${price}/Jahr`,
|
||||
planCardPriceMonthly: (price: string) => `${price}/Monat`,
|
||||
trialToggleLabel: '7 Tage gratis testen',
|
||||
dueTodayTrial: 'Fällig heute — 7 Tage gratis',
|
||||
dueTodayTrial: 'Fällig heute - 7 Tage gratis',
|
||||
dueTodayAmount: '0,00 €',
|
||||
dueLater: (date: string) => `Fällig am ${date}`,
|
||||
ctaTrial: 'Gratis testen',
|
||||
@@ -186,27 +234,34 @@ const getBillingCopy = (language: Language) => {
|
||||
freePlanPrice: '0 EUR / Mes',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4.99 EUR / Mes',
|
||||
proPlanPriceBare: '4,99 €',
|
||||
proYearlyPlanPriceBare: '39,99 €',
|
||||
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',
|
||||
'100 escaneos y consultas cada mes',
|
||||
'Ranking de causas indicando qué tan seguros estamos',
|
||||
'Pasos concretos de revisión, no consejos genéricos',
|
||||
'Plan de 7 días por problema, más el seguimiento',
|
||||
'Historial y galería completos',
|
||||
'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 €',
|
||||
topupSmall: '30 Créditos - 2,99 €',
|
||||
topupMedium: '100 Créditos - 6,99 €',
|
||||
topupLarge: '250 Créditos - 12,99 €',
|
||||
topupBestValue: 'MEJOR OFERTA',
|
||||
cancelTitle: 'Lamentamos verte ir',
|
||||
cancelQuestion: '¿Podemos saber por qué cancelas?',
|
||||
reasonTooExpensive: 'Es muy caro',
|
||||
reasonNotUsing: 'No lo uso suficiente',
|
||||
pauseTitle: 'Entonces mejor ponlo en pausa.',
|
||||
pauseText: 'Suspendemos tu suscripción 3 meses. Tus plantas, tu historial y tus planes se conservan - no se cobra nada.',
|
||||
pauseAccept: 'Pausar 3 meses',
|
||||
pauseDecline: 'No, cancelar igualmente',
|
||||
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).',
|
||||
@@ -218,14 +273,43 @@ const getBillingCopy = (language: Language) => {
|
||||
autoRenewYearly: 'Se renueva anualmente de forma automática. Cancela cuando quieras en Ajustes de iOS.',
|
||||
manageInSettings: 'Administrar en Ajustes de iOS',
|
||||
paywallEyebrow: 'GreenLens Pro',
|
||||
paywallHeadline: 'Acceso ilimitado',
|
||||
paywallSub: 'Escaneos ilimitados, chequeos de salud y tu plan de cuidados personal.',
|
||||
paywallHeadline: 'Deja de adivinar qué le pasa a tu planta.',
|
||||
paywallSub: 'Causa, pasos de revisión, acción inmediata - y una comprobación a los 7 días.',
|
||||
planCardTitle: 'GreenLens Pro',
|
||||
planCardBody: 'Escaneos IA ilimitados, diagnóstico de salud, planes de rescate de 7 días, 100 créditos/mes',
|
||||
planCardBody: '100 escaneos y consultas al mes',
|
||||
paywallBullets: [
|
||||
'Ranking de causas - incluyendo lo que es incierto',
|
||||
'Pasos concretos de revisión, no consejos genéricos',
|
||||
'Plan de 7 días, más la pregunta de si funciona',
|
||||
'Historial completo de tus plantas',
|
||||
],
|
||||
paywallCompare: 'Las herramientas gratuitas te dicen cómo se llama tu planta. No te dicen qué revisar, qué hacer ni si funcionó.',
|
||||
badgeMostPopular: 'POPULAR',
|
||||
badgeFlexible: 'FLEXIBLE',
|
||||
planYearlyName: 'Anual - 7 días gratis',
|
||||
planMonthlyName: 'Mensual',
|
||||
saveBadge: 'AHORRA 33 %',
|
||||
perYear: '/ año',
|
||||
perMonth: '/ mes',
|
||||
breaksDownTo: 'Sale a solo',
|
||||
equivalentValue: (price: string) => `${price} / mes`,
|
||||
faqQ1: '"Pero hay herramientas gratuitas."',
|
||||
faqQ2: '"Otra app se equivocó con mucha seguridad."',
|
||||
faqA2: 'Te mostramos qué tan seguros estamos de cada causa. Por debajo del 65 % te lo decimos claramente, y te damos los pasos de revisión para confirmarlo o descartarlo tú mismo.',
|
||||
faqQ3: '"¿Y si no funciona?"',
|
||||
faqA3: 'A los 7 días preguntamos. Si no mejora, ajustamos el plan. Cancelar son 2 toques desde los ajustes de iOS - te preguntamos el motivo, y ya está.',
|
||||
faqScope: 'Incluye: 100 escaneos y consultas al mes, historial completo de tus plantas.',
|
||||
altMonthly: (price: string) => `o ${price} al mes, sin prueba gratuita`,
|
||||
altYearly: (price: string) => `o ${price} al año - con 7 días gratis`,
|
||||
dueSummary: (today: string, later: string, date: string) =>
|
||||
`Hoy pagas: ${today} - luego ${later} el ${date}`,
|
||||
detailsToggle: 'Preguntas que quizá tengas ahora',
|
||||
trialReminder: 'Te avisamos 2 días antes de que termine la prueba.',
|
||||
cancelPath: 'Cancela en 2 toques desde los Ajustes de iOS - te explicamos cómo en la app.',
|
||||
planCardPriceTrial: (price: string) => `7 días gratis, luego ${price}/año`,
|
||||
planCardPriceMonthly: (price: string) => `${price}/mes`,
|
||||
trialToggleLabel: 'Probar 7 días gratis',
|
||||
dueTodayTrial: 'Hoy — 7 días gratis',
|
||||
dueTodayTrial: 'Hoy - 7 días gratis',
|
||||
dueTodayAmount: '0,00 €',
|
||||
dueLater: (date: string) => `El ${date}`,
|
||||
ctaTrial: 'Probar gratis',
|
||||
@@ -251,27 +335,34 @@ const getBillingCopy = (language: Language) => {
|
||||
freePlanPrice: '0 EUR / Month',
|
||||
proPlanName: 'Pro',
|
||||
proPlanPrice: '4.99 EUR / Month',
|
||||
proPlanPriceBare: '€4.99',
|
||||
proYearlyPlanPriceBare: '€39.99',
|
||||
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',
|
||||
'100 scans and follow-ups every month',
|
||||
'A ranking of causes, with how confident we are',
|
||||
'Concrete things to check, not generic care tips',
|
||||
'A 7-day plan per problem, plus the follow-up',
|
||||
'Complete history & gallery',
|
||||
'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',
|
||||
topupSmall: '30 Credits - €2.99',
|
||||
topupMedium: '100 Credits - €6.99',
|
||||
topupLarge: '250 Credits - €12.99',
|
||||
topupBestValue: 'BEST VALUE',
|
||||
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',
|
||||
pauseTitle: 'Then just pause it.',
|
||||
pauseText: 'We put your subscription on hold for 3 months. Your plants, history and plans stay - nothing gets charged.',
|
||||
pauseAccept: 'Pause for 3 months',
|
||||
pauseDecline: 'No, cancel anyway',
|
||||
reasonOther: 'Other reason',
|
||||
offerTitle: 'A gift for you!',
|
||||
offerText: 'Stay with us and get your next month for just €2.49 (50% off).',
|
||||
@@ -283,14 +374,43 @@ const getBillingCopy = (language: Language) => {
|
||||
autoRenewYearly: 'Auto-renews annually. Cancel anytime in iOS Settings.',
|
||||
manageInSettings: 'Manage in iOS Settings',
|
||||
paywallEyebrow: 'GreenLens Pro',
|
||||
paywallHeadline: 'Get Unlimited Access',
|
||||
paywallSub: 'Unlimited scans, health checks and your personal care plan.',
|
||||
paywallHeadline: 'Stop guessing what your plant needs.',
|
||||
paywallSub: 'Cause, what to check, what to do now - and a follow-up after 7 days.',
|
||||
planCardTitle: 'GreenLens Pro',
|
||||
planCardBody: 'Unlimited AI scans, health diagnosis, 7-day rescue plans, 100 credits/month',
|
||||
planCardBody: '100 scans & follow-ups per month',
|
||||
paywallBullets: [
|
||||
'A ranking of causes - including what is uncertain',
|
||||
'Concrete things to check, not generic care tips',
|
||||
'A 7-day plan, plus a follow-up on whether it works',
|
||||
'Your complete plant history',
|
||||
],
|
||||
paywallCompare: 'Free tools tell you what your plant is called. They do not tell you what to check, what to do, or whether it worked.',
|
||||
badgeMostPopular: 'MOST POPULAR',
|
||||
badgeFlexible: 'FLEXIBLE',
|
||||
planYearlyName: 'Yearly - 7 days free',
|
||||
planMonthlyName: 'Monthly',
|
||||
saveBadge: 'SAVE 33%',
|
||||
perYear: '/ year',
|
||||
perMonth: '/ month',
|
||||
breaksDownTo: 'Breaks down to just',
|
||||
equivalentValue: (price: string) => `${price} / month`,
|
||||
faqQ1: '"But free tools exist."',
|
||||
faqQ2: '"Another app was confidently wrong."',
|
||||
faqA2: 'We show you how confident we are for every cause. Below 65% we say so plainly - and give you the checks to confirm or rule it out yourself.',
|
||||
faqQ3: '"What if it does not help?"',
|
||||
faqA3: 'After 7 days we ask. If it is not improving, we adjust the plan. Cancelling takes 2 taps in iOS settings - we ask why, that is it.',
|
||||
faqScope: 'Included: 100 scans and follow-ups per month, your complete plant history.',
|
||||
altMonthly: (price: string) => `or ${price} monthly, without trial`,
|
||||
altYearly: (price: string) => `or ${price} yearly - with 7 days free`,
|
||||
dueSummary: (today: string, later: string, date: string) =>
|
||||
`Due today: ${today} - then ${later} on ${date}`,
|
||||
detailsToggle: 'Questions you might have right now',
|
||||
trialReminder: 'We remind you 2 days before the trial ends.',
|
||||
cancelPath: 'Cancel in 2 taps via iOS Settings - we show you how inside the app.',
|
||||
planCardPriceTrial: (price: string) => `Free for 7 days, then ${price}/year`,
|
||||
planCardPriceMonthly: (price: string) => `${price}/month`,
|
||||
trialToggleLabel: 'Try 7 days free',
|
||||
dueTodayTrial: 'Due today — 7 days free',
|
||||
dueTodayTrial: 'Due today - 7 days free',
|
||||
dueTodayAmount: '€0.00',
|
||||
dueLater: (date: string) => `Due ${date}`,
|
||||
ctaTrial: 'Try Free',
|
||||
@@ -325,6 +445,13 @@ export default function BillingScreen() {
|
||||
|
||||
// Cancel Flow State
|
||||
const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none');
|
||||
const [cancelReason, setCancelReason] = useState<string | null>(null);
|
||||
// Argumente eingeklappt: die Paywall passt so ohne Scrollen auf einen Screen.
|
||||
// Wer mehr wissen will, klappt auf - wer kaufen will, wird nicht aufgehalten.
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
// Wer die App zu selten nutzt, hat kein Preisproblem - ein Rabatt geht dort
|
||||
// ins Leere und verschenkt Marge. Pause rettet die Beziehung stattdessen.
|
||||
const isPauseOffer = cancelReason === 'not_using';
|
||||
|
||||
const planId = billingSummary?.entitlement?.plan || 'free';
|
||||
const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? 0);
|
||||
@@ -404,8 +531,9 @@ export default function BillingScreen() {
|
||||
const monthlyPackage = subscriptionPackages.monthly_pro;
|
||||
const yearlyPackage = subscriptionPackages.yearly_pro;
|
||||
|
||||
const monthlyPrice = monthlyPackage?.product.priceString ?? copy.proPlanPrice;
|
||||
const yearlyPrice = yearlyPackage?.product.priceString ?? copy.proYearlyPlanPrice;
|
||||
// Fallback ohne Periode - sonst entsteht "4.99 EUR / Month/month".
|
||||
const monthlyPrice = monthlyPackage?.product.priceString ?? copy.proPlanPriceBare;
|
||||
const yearlyPrice = yearlyPackage?.product.priceString ?? copy.proYearlyPlanPriceBare;
|
||||
const trialEndDate = useMemo(() => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 7);
|
||||
@@ -496,7 +624,7 @@ export default function BillingScreen() {
|
||||
posthog.capture('purchase_initiated', { product_id: productId });
|
||||
try {
|
||||
if (isExpoGo) {
|
||||
// ExpoGo has no native RevenueCat — use simulation for development only
|
||||
// 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, [
|
||||
@@ -558,7 +686,7 @@ export default function BillingScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
// RevenueCat error code 7 = PRODUCT_ALREADY_PURCHASED — the Apple ID already
|
||||
// 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<string, unknown>).code : undefined;
|
||||
@@ -606,6 +734,7 @@ export default function BillingScreen() {
|
||||
try {
|
||||
await simulateWebhookEvent('entitlement_revoked');
|
||||
setCancelStep('none');
|
||||
setCancelReason(null);
|
||||
setSubModalVisible(false);
|
||||
} catch (e) {
|
||||
console.error('Downgrade failed', e);
|
||||
@@ -614,109 +743,230 @@ export default function BillingScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
// Jahrespreis -> Monatsaequivalent, mit Waehrungssymbol aus dem Store-Preis.
|
||||
const yearlyMonthlyEquivalent = (() => {
|
||||
const numeric = parseFloat(yearlyPrice.replace(/[^0-9.,]/g, '').replace(',', '.'));
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) return null;
|
||||
const symbol = yearlyPrice.replace(/[0-9.,\s]/g, '') || '€';
|
||||
const perMonth = (numeric / 12).toFixed(2).replace('.', language === 'en' ? '.' : ',');
|
||||
return `${perMonth} ${symbol}`.trim();
|
||||
})();
|
||||
|
||||
if (showPaywallPlans) {
|
||||
return (
|
||||
<View style={styles.hardPaywallScreen}>
|
||||
<ImageBackground
|
||||
source={PAYWALL_BACKGROUND}
|
||||
style={styles.hardPaywallHero}
|
||||
imageStyle={styles.hardPaywallHeroImage}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<View style={[styles.hardPaywallScreen, { backgroundColor: colors.background }]}>
|
||||
<View style={styles.hardPaywallPlain}>
|
||||
<SafeAreaView style={styles.hardPaywallSafe} edges={['top']}>
|
||||
<View style={styles.heroTopBar}>
|
||||
<TouchableOpacity onPress={handleBack} style={styles.heroIconButton}>
|
||||
<Ionicons name="close" size={24} color="#FFFFFF" />
|
||||
<TouchableOpacity onPress={handleBack} style={[styles.heroIconButton, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={handleRestore} disabled={isUpdating}>
|
||||
<Text style={styles.heroRestoreText}>{copy.restorePurchases}</Text>
|
||||
<Text style={[styles.heroRestoreText, { color: colors.textSecondary }]}>{copy.restorePurchases}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.paywallSheet, { backgroundColor: colors.background, marginTop: compact ? 110 : 190 }]}>
|
||||
<View style={[styles.paywallSheet, { backgroundColor: colors.background, marginTop: 0 }]}>
|
||||
<ScrollView
|
||||
style={styles.paywallScroll}
|
||||
contentContainerStyle={styles.paywallBody}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.sheetHandle, { backgroundColor: colors.border }]} />
|
||||
<Text style={[styles.paywallEyebrow, { color: colors.primary }]}>{copy.paywallEyebrow.toUpperCase()}</Text>
|
||||
<Text style={[styles.paywallHeadline, { color: colors.text }]}>{copy.paywallHeadline}</Text>
|
||||
<Text style={[styles.paywallSub, { color: colors.textSecondary }]}>{copy.paywallSub}</Text>
|
||||
|
||||
<View style={[styles.planCard, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Text style={[styles.planCardTitle, { color: colors.text }]}>{copy.planCardTitle}</Text>
|
||||
<Text style={[styles.planCardBody, { color: colors.textSecondary }]}>{copy.planCardBody}</Text>
|
||||
<View style={[styles.planCardDivider, { backgroundColor: colors.border }]} />
|
||||
<Text style={[styles.planCardPrice, { color: colors.text }]}>
|
||||
{trialEnabled ? copy.planCardPriceTrial(yearlyPrice) : copy.planCardPriceMonthly(monthlyPrice)}
|
||||
</Text>
|
||||
{/* Feature-Bullets ueber der Angebotskarte: kurz, drei
|
||||
Stueck, ohne Kaestchen - sie sind Kontext, nicht die
|
||||
Entscheidung. */}
|
||||
<View style={styles.offerBullets}>
|
||||
{copy.paywallBullets.slice(0, 3).map((bullet) => (
|
||||
<View key={bullet} style={styles.offerBulletRow}>
|
||||
<Ionicons name="checkmark-circle-outline" size={19} color={colors.primary} />
|
||||
<Text style={[styles.offerBulletText, { color: colors.textSecondary }]}>{bullet}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={[styles.trialToggleRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.trialToggleLabel, { color: colors.text }]}>{copy.trialToggleLabel}</Text>
|
||||
<Switch
|
||||
value={trialEnabled}
|
||||
onValueChange={(next) => setSelectedPaywallPlan(next ? 'yearly' : 'monthly')}
|
||||
trackColor={{ true: colors.primary, false: colors.border }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
{/* EINE Angebotskarte statt Toggle + Preiskarte. Der Plan,
|
||||
der Preis und der Kaufbutton stehen zusammen, die
|
||||
Alternative darunter als Textlink. Damit entfaellt der
|
||||
Switch, der die Preisaenderung nie sichtbar gemacht hat. */}
|
||||
<View
|
||||
style={[
|
||||
styles.offerCardOuter,
|
||||
{
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
borderColor: colors.primary,
|
||||
// Dezenter Glow statt Leuchtrahmen.
|
||||
shadowColor: colors.primary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={[styles.offerBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={[styles.offerBadgeText, { color: colors.onPrimary }]}>
|
||||
{trialEnabled ? copy.badgeMostPopular : copy.badgeFlexible}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.offerHeaderRow}>
|
||||
<View style={styles.offerHeaderLeft}>
|
||||
<Text style={[styles.offerPlanName, { color: colors.text }]}>
|
||||
{trialEnabled ? copy.planYearlyName : copy.planMonthlyName}
|
||||
</Text>
|
||||
{trialEnabled ? (
|
||||
<Text style={[styles.offerSaveText, { color: colors.primary }]}>{copy.saveBadge}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.offerHeaderRight}>
|
||||
<Text style={[styles.offerPrice, { color: colors.text }]}>
|
||||
{trialEnabled ? yearlyPrice : monthlyPrice}
|
||||
</Text>
|
||||
<Text style={[styles.offerPricePeriod, { color: colors.textMuted }]}>
|
||||
{trialEnabled ? copy.perYear : copy.perMonth}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{trialEnabled && yearlyMonthlyEquivalent ? (
|
||||
<View style={[styles.offerBreakdown, { backgroundColor: colors.surface }]}>
|
||||
<Text style={[styles.offerBreakdownLabel, { color: colors.textSecondary }]}>
|
||||
{copy.breaksDownTo}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.offerBreakdownValue, { color: colors.text }]}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
>
|
||||
{copy.equivalentValue(yearlyMonthlyEquivalent)}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.offerCta,
|
||||
{ backgroundColor: colors.primary },
|
||||
(!storeReady || isUpdating || Boolean(storeError)) && styles.disabledPlanCard,
|
||||
]}
|
||||
onPress={() => handlePurchase(trialEnabled ? 'yearly_pro' : 'monthly_pro')}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
activeOpacity={0.86}
|
||||
>
|
||||
{isUpdating || !storeReady ? (
|
||||
<ActivityIndicator color={colors.onPrimary} />
|
||||
) : (
|
||||
<>
|
||||
<Text style={[styles.offerCtaText, { color: colors.onPrimary }]}>
|
||||
{trialEnabled ? copy.ctaTrial : copy.ctaMonthly}
|
||||
</Text>
|
||||
<Ionicons name="arrow-forward" size={19} color={colors.onPrimary} />
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Planwechsel als Textlink - kein Schalter, sondern eine
|
||||
sichtbar formulierte Alternative inkl. Preis. */}
|
||||
<TouchableOpacity
|
||||
onPress={() => setSelectedPaywallPlan(trialEnabled ? 'monthly' : 'yearly')}
|
||||
style={styles.offerAltLink}
|
||||
>
|
||||
<Text style={[styles.offerAltLinkText, { color: colors.textSecondary }]}>
|
||||
{trialEnabled
|
||||
? copy.altMonthly(monthlyPrice)
|
||||
: copy.altYearly(yearlyPrice)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{trialEnabled ? (
|
||||
<View style={styles.dueTimeline}>
|
||||
<View style={styles.dueRow}>
|
||||
<View style={[styles.dueDot, { backgroundColor: colors.primary }]} />
|
||||
<Text style={[styles.dueLabel, { color: colors.primary }]}>{copy.dueTodayTrial}</Text>
|
||||
<Text style={[styles.dueAmount, { color: colors.text }]}>{copy.dueTodayAmount}</Text>
|
||||
</View>
|
||||
<View style={[styles.dueLine, { backgroundColor: colors.border }]} />
|
||||
<View style={styles.dueRow}>
|
||||
<View style={[styles.dueDot, { backgroundColor: colors.border }]} />
|
||||
<Text style={[styles.dueLabel, { color: colors.textSecondary }]}>{copy.dueLater(trialEndDate)}</Text>
|
||||
<Text style={[styles.dueAmount, { color: colors.text }]}>{yearlyPrice}</Text>
|
||||
</View>
|
||||
<View style={[styles.dueBanner, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons name="calendar-outline" size={16} color={colors.primary} />
|
||||
<Text style={[styles.dueBannerText, { color: colors.textSecondary }]}>
|
||||
{copy.dueSummary(copy.dueTodayAmount, yearlyPrice, trialEndDate)}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{storeError ? (
|
||||
<Text style={[styles.paywallFooterText, { color: colors.danger, marginBottom: 8 }]}>{storeError}</Text>
|
||||
) : null}
|
||||
|
||||
{/* Aufklappbar: eingeklappt bleibt die Kaufentscheidung
|
||||
ueber der Falz, aufgeklappt stehen die Argumente
|
||||
vollstaendig da. Beides ohne zweiten Screen. */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.paywallCta,
|
||||
{ backgroundColor: colors.primary },
|
||||
(!storeReady || isUpdating || Boolean(storeError)) && styles.disabledPlanCard,
|
||||
]}
|
||||
onPress={() => handlePurchase(trialEnabled ? 'yearly_pro' : 'monthly_pro')}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
activeOpacity={0.86}
|
||||
style={[styles.detailsToggle, { borderColor: colors.border }]}
|
||||
onPress={() => {
|
||||
setDetailsOpen((open) => {
|
||||
if (!open) posthog.capture('paywall_details_expanded');
|
||||
return !open;
|
||||
});
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isUpdating || !storeReady ? (
|
||||
<ActivityIndicator color={colors.onPrimary} />
|
||||
) : (
|
||||
<Text style={[styles.paywallCtaText, { color: colors.onPrimary }]}>
|
||||
{trialEnabled ? copy.ctaTrial : copy.ctaMonthly}
|
||||
</Text>
|
||||
)}
|
||||
<Text style={[styles.detailsToggleLabel, { color: colors.text }]}>{copy.detailsToggle}</Text>
|
||||
<Ionicons
|
||||
name={detailsOpen ? 'chevron-up' : 'chevron-down'}
|
||||
size={18}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.paywallFooter}>
|
||||
<View style={styles.paywallFooterLinks}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
|
||||
<Text style={[styles.paywallFooterText, { color: colors.textMuted }]}>Privacy</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.paywallFooterText, { color: colors.textMuted }]}> | </Text>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/terms')}>
|
||||
<Text style={[styles.paywallFooterText, { color: colors.textMuted }]}>Terms</Text>
|
||||
</TouchableOpacity>
|
||||
{detailsOpen ? (
|
||||
/* Bewusst KEINE Feature-Wiederholung: die drei
|
||||
wichtigsten Punkte stehen bereits oben. Hier
|
||||
stehen stattdessen die drei dokumentierten
|
||||
Einwaende aus dem Offer Brief - Gratis-Alternativen,
|
||||
selbstbewusst falsche Konkurrenz-Apps, und was
|
||||
passiert, wenn es nicht wirkt. */
|
||||
<View style={styles.faqBlock}>
|
||||
<View style={styles.faqItem}>
|
||||
<Text style={[styles.faqQuestion, { color: colors.text }]}>{copy.faqQ1}</Text>
|
||||
<Text style={[styles.faqAnswer, { color: colors.textSecondary }]}>{copy.paywallCompare}</Text>
|
||||
</View>
|
||||
<View style={styles.faqItem}>
|
||||
<Text style={[styles.faqQuestion, { color: colors.text }]}>{copy.faqQ2}</Text>
|
||||
<Text style={[styles.faqAnswer, { color: colors.textSecondary }]}>{copy.faqA2}</Text>
|
||||
</View>
|
||||
<View style={styles.faqItem}>
|
||||
<Text style={[styles.faqQuestion, { color: colors.text }]}>{copy.faqQ3}</Text>
|
||||
<Text style={[styles.faqAnswer, { color: colors.textSecondary }]}>{copy.faqA3}</Text>
|
||||
</View>
|
||||
<Text style={[styles.faqScope, { color: colors.textMuted }]}>{copy.faqScope}</Text>
|
||||
</View>
|
||||
<Text style={[styles.paywallFooterText, { color: colors.textMuted }]}>{copy.cancelAnytime}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
</ScrollView>
|
||||
|
||||
{/* Nur noch Rechtstexte: der Kaufbutton sitzt in der
|
||||
Angebotskarte, direkt neben Plan und Preis. Ein zweiter
|
||||
CTA in einer Leiste waere eine doppelte Aufforderung
|
||||
zur selben Handlung. */}
|
||||
<View style={[styles.paywallActionBar, { backgroundColor: colors.background, borderTopColor: colors.border }]}>
|
||||
{storeError ? (
|
||||
<Text style={[styles.paywallFooterText, styles.paywallFooterCentered, { color: colors.danger, marginBottom: 6 }]}>
|
||||
{storeError}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{/* Belief 6: Kuendigung und Verlaengerung stehen sichtbar
|
||||
vor der Entscheidung, nicht im Kleingedruckten. */}
|
||||
<Text style={[styles.paywallFooterText, styles.paywallFooterCentered, { color: colors.textMuted }]}>
|
||||
{trialEnabled ? `${copy.cancelAnytime} · ${copy.trialReminder}` : copy.cancelAnytime}
|
||||
</Text>
|
||||
<Text style={[styles.paywallFooterText, styles.paywallFooterCentered, { color: colors.textMuted }]}>
|
||||
{copy.cancelPath}
|
||||
</Text>
|
||||
<View style={styles.paywallFooterLinks}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
|
||||
<Text style={[styles.paywallFooterText, { color: colors.textMuted }]}>Privacy</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.paywallFooterText, { color: colors.textMuted }]}> | </Text>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/terms')}>
|
||||
<Text style={[styles.paywallFooterText, { color: colors.textMuted }]}>Terms</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -835,11 +1085,12 @@ export default function BillingScreen() {
|
||||
<View style={[styles.modalContent, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={[styles.modalTitle, { color: colors.text }]}>
|
||||
{cancelStep === 'survey' ? copy.cancelTitle : cancelStep === 'offer' ? copy.offerTitle : copy.subscriptionTitle}
|
||||
{cancelStep === 'survey' ? copy.cancelTitle : cancelStep === 'offer' ? (isPauseOffer ? copy.pauseTitle : copy.offerTitle) : copy.subscriptionTitle}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setSubModalVisible(false);
|
||||
setCancelStep('none');
|
||||
setCancelReason(null);
|
||||
}}>
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
@@ -953,6 +1204,9 @@ export default function BillingScreen() {
|
||||
key={reason.id}
|
||||
style={[styles.reasonOption, { borderColor: colors.border }]}
|
||||
onPress={() => {
|
||||
// Grund merken: ein Rabatt hilft nur bei "zu teuer".
|
||||
// Wer zu selten nutzt, hat kein Preisproblem.
|
||||
setCancelReason(reason.id);
|
||||
setCancelStep('offer');
|
||||
}}
|
||||
>
|
||||
@@ -969,20 +1223,29 @@ export default function BillingScreen() {
|
||||
<View style={styles.cancelFlowContainer}>
|
||||
<View style={[styles.offerCard, { backgroundColor: colors.primarySoft }]}>
|
||||
<View style={[styles.offerIconWrap, { backgroundColor: colors.primary }]}>
|
||||
<Ionicons name="gift" size={28} color="#fff" />
|
||||
<Ionicons name={isPauseOffer ? 'pause' : 'gift'} size={28} color="#fff" />
|
||||
</View>
|
||||
<Text style={[styles.offerText, { color: colors.text }]}>{copy.offerText}</Text>
|
||||
<Text style={[styles.offerText, { color: colors.text }]}>
|
||||
{isPauseOffer ? copy.pauseText : copy.offerText}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.offerAcceptBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={() => {
|
||||
// Handle applying discount here (future implementation)
|
||||
Alert.alert('Erfolg', 'Rabatt angewendet! (Mock)');
|
||||
posthog.capture('cancel_save_offer_accepted', {
|
||||
reason: cancelReason,
|
||||
offer: isPauseOffer ? 'pause_3_months' : 'discount_50',
|
||||
});
|
||||
// TODO(billing): Pause bzw. Rabatt am Store-Abo umsetzen.
|
||||
Alert.alert('OK', isPauseOffer ? 'Abo pausiert (Mock)' : 'Rabatt angewendet (Mock)');
|
||||
setCancelStep('none');
|
||||
setCancelReason(null);
|
||||
setSubModalVisible(false);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.offerAcceptBtnText}>{copy.offerAccept}</Text>
|
||||
<Text style={styles.offerAcceptBtnText}>
|
||||
{isPauseOffer ? copy.pauseAccept : copy.offerAccept}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -991,7 +1254,9 @@ export default function BillingScreen() {
|
||||
onPress={finalizeCancel}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<Text style={[styles.offerDeclineBtnText, { color: colors.textMuted }]}>{copy.offerDecline}</Text>
|
||||
<Text style={[styles.offerDeclineBtnText, { color: colors.textMuted }]}>
|
||||
{isPauseOffer ? copy.pauseDecline : copy.offerDecline}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
@@ -1008,12 +1273,6 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: '#101411',
|
||||
},
|
||||
hardPaywallHero: {
|
||||
flex: 1,
|
||||
},
|
||||
hardPaywallHeroImage: {
|
||||
transform: [{ translateY: -38 }, { scale: 1.06 }],
|
||||
},
|
||||
hardPaywallSafe: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -1040,43 +1299,108 @@ const styles = StyleSheet.create({
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 3,
|
||||
},
|
||||
hardPaywallPlain: { flex: 1 },
|
||||
paywallSheet: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 30,
|
||||
borderTopRightRadius: 30,
|
||||
overflow: 'hidden',
|
||||
zIndex: 5,
|
||||
},
|
||||
sheetHandle: {
|
||||
alignSelf: 'center',
|
||||
width: 42,
|
||||
height: 5,
|
||||
borderRadius: 999,
|
||||
marginTop: 10,
|
||||
marginBottom: 8,
|
||||
},
|
||||
paywallBody: { paddingHorizontal: 22, paddingTop: 10, paddingBottom: 24 },
|
||||
paywallScroll: { flex: 1 },
|
||||
paywallBody: { paddingHorizontal: 22, paddingTop: 10, paddingBottom: 16 },
|
||||
paywallEyebrow: { fontSize: 12, fontWeight: '900', letterSpacing: 1.4, textAlign: 'center', marginBottom: 6 },
|
||||
paywallHeadline: { fontSize: 32, fontWeight: '900', textAlign: 'center', marginBottom: 6 },
|
||||
paywallSub: { fontSize: 15, lineHeight: 21, textAlign: 'center', marginBottom: 18 },
|
||||
planCard: { borderRadius: 16, padding: 18, marginBottom: 14 },
|
||||
planCardTitle: { fontSize: 19, fontWeight: '800', marginBottom: 6 },
|
||||
planCardBody: { fontSize: 14, lineHeight: 20 },
|
||||
planCardDivider: { height: StyleSheet.hairlineWidth, marginVertical: 12 },
|
||||
planCardPrice: { fontSize: 15, fontWeight: '800' },
|
||||
trialToggleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderRadius: 14, borderWidth: 1, paddingHorizontal: 16, paddingVertical: 12, marginBottom: 16 },
|
||||
trialToggleLabel: { fontSize: 15, fontWeight: '800' },
|
||||
dueTimeline: { marginBottom: 18, paddingHorizontal: 4 },
|
||||
dueRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
dueDot: { width: 10, height: 10, borderRadius: 5 },
|
||||
dueLine: { width: 2, height: 18, marginLeft: 4, marginVertical: 2 },
|
||||
dueLabel: { flex: 1, fontSize: 14, fontWeight: '700' },
|
||||
dueAmount: { fontSize: 14, fontWeight: '800' },
|
||||
paywallCta: { height: 58, borderRadius: 14, alignItems: 'center', justifyContent: 'center', marginBottom: 12 },
|
||||
paywallCtaText: { fontSize: 18, fontWeight: '800' },
|
||||
paywallFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
paywallFooterLinks: { flexDirection: 'row', alignItems: 'center' },
|
||||
// Bullets ueber der Karte: reiner Kontext, bewusst ohne eigenen Container.
|
||||
offerBullets: { gap: 12, marginBottom: 22, paddingHorizontal: 4 },
|
||||
offerBulletRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 11 },
|
||||
offerBulletText: { flex: 1, fontSize: 14.5, lineHeight: 20, fontWeight: '500' },
|
||||
|
||||
// Angebotskarte. Der Glow ist bewusst zurueckhaltend: opacity 0.18 statt
|
||||
// eines Leuchtrahmens, damit die Karte sich abhebt ohne zu schreien.
|
||||
offerCardOuter: {
|
||||
borderRadius: 22,
|
||||
borderWidth: 1.5,
|
||||
paddingHorizontal: 18,
|
||||
paddingTop: 26,
|
||||
paddingBottom: 16,
|
||||
marginBottom: 16,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.42,
|
||||
shadowRadius: 26,
|
||||
elevation: 10,
|
||||
},
|
||||
offerBadge: {
|
||||
position: 'absolute',
|
||||
top: -12,
|
||||
alignSelf: 'center',
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginHorizontal: 'auto',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 999,
|
||||
maxWidth: 160,
|
||||
},
|
||||
offerBadgeText: { fontSize: 11, fontWeight: '900', letterSpacing: 0.8, textAlign: 'center' },
|
||||
offerHeaderRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 },
|
||||
offerHeaderLeft: { flex: 1, gap: 3 },
|
||||
offerHeaderRight: { alignItems: 'flex-end' },
|
||||
offerPlanName: { fontSize: 17, fontWeight: '800', lineHeight: 22 },
|
||||
offerSaveText: { fontSize: 12.5, fontWeight: '800', letterSpacing: 0.4 },
|
||||
offerPrice: { fontSize: 25, fontWeight: '900', letterSpacing: -0.4 },
|
||||
offerPricePeriod: { fontSize: 12.5, fontWeight: '600' },
|
||||
offerBreakdown: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 11,
|
||||
marginTop: 16,
|
||||
gap: 10,
|
||||
},
|
||||
offerBreakdownLabel: { fontSize: 13.5, fontWeight: '600' },
|
||||
offerBreakdownValue: { fontSize: 15, fontWeight: '800' },
|
||||
offerCta: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 9,
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
marginTop: 16,
|
||||
},
|
||||
offerCtaText: { fontSize: 17.5, fontWeight: '800' },
|
||||
offerAltLink: { alignItems: 'center', paddingVertical: 13 },
|
||||
offerAltLinkText: { fontSize: 13.5, fontWeight: '600', textDecorationLine: 'underline' },
|
||||
dueBanner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 9,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
dueBannerText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '600' },
|
||||
detailsToggle: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderRadius: 14, borderWidth: 1, paddingHorizontal: 16, paddingVertical: 13, marginBottom: 14 },
|
||||
detailsToggleLabel: { fontSize: 14.5, fontWeight: '700' },
|
||||
faqBlock: { gap: 16, marginBottom: 18, paddingHorizontal: 2 },
|
||||
faqItem: { gap: 5 },
|
||||
faqQuestion: { fontSize: 14.5, fontWeight: '800', fontStyle: 'italic' },
|
||||
faqAnswer: { fontSize: 13.5, lineHeight: 19, fontWeight: '500' },
|
||||
faqScope: { fontSize: 12.5, lineHeight: 17, fontWeight: '600', marginTop: 2 },
|
||||
paywallCompare: { borderLeftWidth: 3, paddingLeft: 12, paddingVertical: 4, marginBottom: 16 },
|
||||
paywallCompareText: { fontSize: 13.5, lineHeight: 19, fontWeight: '500' },
|
||||
paywallBullets: { gap: 10, marginBottom: 18, paddingHorizontal: 2 },
|
||||
paywallBulletRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9 },
|
||||
paywallBulletText: { flex: 1, fontSize: 14, lineHeight: 19, fontWeight: '600' },
|
||||
// Der Preis war mit 15px kleiner als der Fliesstext. Bei einem Avatar, dessen
|
||||
// Kernangst Abo-Intransparenz ist, muss der Preis das Auffaelligste im Block sein.
|
||||
paywallActionBar: { paddingHorizontal: 22, paddingTop: 14, paddingBottom: 18, borderTopWidth: 1, gap: 6 },
|
||||
paywallFooterLinks: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
||||
paywallFooterText: { fontSize: 12, fontWeight: '600' },
|
||||
paywallFooterCentered: { textAlign: 'center' },
|
||||
safeArea: { flex: 1 },
|
||||
header: { flexDirection: 'row', alignItems: 'center', padding: 16 },
|
||||
backButton: { width: 40, height: 40, justifyContent: 'center' },
|
||||
|
||||
@@ -5,9 +5,10 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
import { AuthService } from '../../services/authService';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
import { AuthService } from '../../services/authService';
|
||||
import { resetToFreshInstall } from '../../services/devReset';
|
||||
|
||||
const getDataCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
@@ -115,23 +116,45 @@ export default function DataScreen() {
|
||||
]);
|
||||
};
|
||||
|
||||
// Nur im Dev-Build: setzt die App auf "frisch installiert" zurueck. Noetig,
|
||||
// weil ein Expo-Reload weder Keychain noch SQLite loescht - man landet sonst
|
||||
// immer wieder direkt im Dashboard statt im Onboarding.
|
||||
const handleDevReset = () => {
|
||||
Alert.alert(
|
||||
'Onboarding neu starten?',
|
||||
'Session, Onboarding-Status und A/B-Zuweisung werden zurückgesetzt. Du landest wieder auf dem Welcome-Screen.',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{
|
||||
text: 'Zurücksetzen',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await resetToFreshInstall();
|
||||
await signOut();
|
||||
router.replace('/onboarding');
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
Alert.alert(copy.deleteConfirmTitle, copy.deleteConfirmMessage, [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: copy.deleteActionBtn,
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await AuthService.deleteAccount();
|
||||
await signOut();
|
||||
router.replace('/onboarding');
|
||||
} catch {
|
||||
Alert.alert(copy.genericErrorTitle, copy.genericErrorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
text: copy.deleteActionBtn,
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await AuthService.deleteAccount();
|
||||
await signOut();
|
||||
router.replace('/onboarding');
|
||||
} catch {
|
||||
Alert.alert(copy.genericErrorTitle, copy.genericErrorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -177,6 +200,23 @@ export default function DataScreen() {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{__DEV__ ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionRow, { backgroundColor: colors.cardBg, borderColor: colors.border }]}
|
||||
onPress={handleDevReset}
|
||||
>
|
||||
<View style={styles.actionIcon}>
|
||||
<Ionicons name="refresh-circle-outline" size={24} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.actionTextContainer}>
|
||||
<Text style={[styles.actionTitle, { color: colors.text }]}>Onboarding neu starten (DEV)</Text>
|
||||
<Text style={[styles.actionHint, { color: `${colors.text}80` }]}>
|
||||
Setzt Session, Onboarding-Status und A/B-Variante zurück.
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
<View style={{ marginTop: 24 }}>
|
||||
<TouchableOpacity style={[styles.actionRow, { backgroundColor: '#FF3B3015', borderColor: '#FF3B3050', marginBottom: 0 }]} onPress={handleDeleteAccount}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: '#FF3B3020' }]}>
|
||||
|
||||
Reference in New Issue
Block a user