From 30a2537eaa8aed46c40f0fd4c921e4d38c003df8 Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Mon, 6 Jul 2026 13:31:37 +0200 Subject: [PATCH] =?UTF-8?q?feat(app):=20Stitch=20paywall=20=E2=80=94=20sin?= =?UTF-8?q?gle=20plan=20card=20with=20free-trial=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- app/profile/billing.tsx | 532 ++++++++++++---------------------------- 1 file changed, 158 insertions(+), 374 deletions(-) diff --git a/app/profile/billing.tsx b/app/profile/billing.tsx index 433adb8..4f6c3c5 100644 --- a/app/profile/billing.tsx +++ b/app/profile/billing.tsx @@ -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, useWindowDimensions } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, ImageBackground, Platform, Switch } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useRouter, useLocalSearchParams } from 'expo-router'; @@ -175,6 +175,20 @@ const getBillingCopy = (language: Language) => { 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', + paywallEyebrow: 'GreenLens Pro', + paywallHeadline: 'Unbegrenzter Zugriff', + paywallSub: 'Unbegrenzte Scans, Health-Checks und dein persönlicher Pflegeplan.', + planCardTitle: 'GreenLens Pro', + planCardBody: 'Unbegrenzte KI-Scans, Gesundheitsdiagnose, 7-Tage-Rettungspläne, 100 Credits/Monat', + planCardPriceTrial: (price: string) => `7 Tage gratis, dann ${price}/Jahr`, + planCardPriceMonthly: (price: string) => `${price}/Monat`, + trialToggleLabel: 'Gratis-Test aktiviert', + dueTodayTrial: 'Fällig heute — 7 Tage gratis', + dueTodayAmount: '0,00 €', + dueLater: (date: string) => `Fällig am ${date}`, + ctaTrial: 'Gratis testen', + ctaMonthly: 'Jetzt starten', + cancelAnytime: 'Jederzeit kündbar', }; } else if (language === 'es') { return { @@ -249,6 +263,20 @@ const getBillingCopy = (language: Language) => { 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', + paywallEyebrow: 'GreenLens Pro', + paywallHeadline: 'Acceso ilimitado', + paywallSub: 'Escaneos ilimitados, chequeos de salud y tu plan de cuidados personal.', + planCardTitle: 'GreenLens Pro', + planCardBody: 'Escaneos IA ilimitados, diagnóstico de salud, planes de rescate de 7 días, 100 créditos/mes', + planCardPriceTrial: (price: string) => `7 días gratis, luego ${price}/año`, + planCardPriceMonthly: (price: string) => `${price}/mes`, + trialToggleLabel: 'Prueba gratis activada', + dueTodayTrial: 'Hoy — 7 días gratis', + dueTodayAmount: '0,00 €', + dueLater: (date: string) => `El ${date}`, + ctaTrial: 'Probar gratis', + ctaMonthly: 'Empezar ahora', + cancelAnytime: 'Cancela cuando quieras', }; } return { @@ -323,6 +351,20 @@ const getBillingCopy = (language: Language) => { autoRenewMonthly: 'Auto-renews monthly. Cancel anytime in iOS Settings.', 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.', + planCardTitle: 'GreenLens Pro', + planCardBody: 'Unlimited AI scans, health diagnosis, 7-day rescue plans, 100 credits/month', + planCardPriceTrial: (price: string) => `Free for 7 days, then ${price}/year`, + planCardPriceMonthly: (price: string) => `${price}/month`, + trialToggleLabel: 'Free Trial Enabled', + dueTodayTrial: 'Due today — 7 days free', + dueTodayAmount: '€0.00', + dueLater: (date: string) => `Due ${date}`, + ctaTrial: 'Try Free', + ctaMonthly: 'Start Now', + cancelAnytime: 'Cancel Anytime', }; }; @@ -338,8 +380,6 @@ export default function BillingScreen() { 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); @@ -407,8 +447,16 @@ export default function BillingScreen() { }; }, [isExpoGo]); + const trialEnabled = selectedPaywallPlan === 'yearly'; + useEffect(() => { - try { posthog.capture('paywall_viewed', { plan_id: planId }); } catch {} + try { + posthog.capture('paywall_viewed', { + plan_id: planId, + context: onboardingContext ? 'onboarding' : 'in_app', + trial_enabled: trialEnabled, + }); + } catch {} if (showPaywallPlans) { try { posthog.capture('hard_paywall_viewed', { @@ -417,18 +465,19 @@ export default function BillingScreen() { }); } catch {} } - }, [posthog, planId, session?.serverUserId, showPaywallPlans]); + }, [posthog, planId, session?.serverUserId, showPaywallPlans, onboardingContext, trialEnabled]); 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 trialEndDate = useMemo(() => { + const date = new Date(); + date.setDate(date.getDate() + 7); + const locale = language === 'de' ? 'de-DE' : language === 'es' ? 'es-ES' : 'en-US'; + return date.toLocaleDateString(locale, { day: 'numeric', month: 'long' }); + }, [language]); const topupLabels = useMemo(() => ({ topup_small: topupProducts.topup_small ? `${TOPUP_CREDITS_BY_PRODUCT.topup_small} Credits - ${topupProducts.topup_small.priceString}` : copy.topupSmall, @@ -638,125 +687,89 @@ export default function BillingScreen() { imageStyle={styles.hardPaywallHeroImage} resizeMode="cover" > - + + + {copy.restorePurchases} + - - - - - - - - - - {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} - - - - )} - + + {copy.paywallEyebrow.toUpperCase()} + {copy.paywallHeadline} + {copy.paywallSub} - {storeError ? ( - {storeError} - ) : null} - {paywallFooterLabel} - - Linking.openURL('https://greenlenspro.com/privacy')}> - Privacy Policy + + {copy.planCardTitle} + {copy.planCardBody} + + + {trialEnabled ? copy.planCardPriceTrial(yearlyPrice) : copy.planCardPriceMonthly(monthlyPrice)} + + + + + {copy.trialToggleLabel} + setSelectedPaywallPlan(next ? 'yearly' : 'weekly')} + trackColor={{ true: colors.primary, false: colors.border }} + thumbColor="#FFFFFF" + /> + + + {trialEnabled ? ( + + + + {copy.dueTodayTrial} + {copy.dueTodayAmount} + + + + + {copy.dueLater(trialEndDate)} + {yearlyPrice} + + + ) : null} + + {storeError ? ( + {storeError} + ) : null} + + handlePurchase(trialEnabled ? 'yearly_pro' : 'monthly_pro')} + disabled={isUpdating || !storeReady || Boolean(storeError)} + activeOpacity={0.86} + > + {isUpdating || !storeReady ? ( + + ) : ( + + {trialEnabled ? copy.ctaTrial : copy.ctaMonthly} + + )} - · - Linking.openURL('https://greenlenspro.com/terms')}> - Terms of Use - - - - {copy.restorePurchases} - + + + Privacy | Terms + {copy.cancelAnytime} + + @@ -1142,10 +1155,6 @@ const styles = StyleSheet.create({ }, hardPaywallSafe: { flex: 1, - justifyContent: 'space-between', - }, - hardPaywallSafeCompact: { - justifyContent: 'flex-end', }, heroTopBar: { flexDirection: 'row', @@ -1170,256 +1179,42 @@ const styles = StyleSheet.create({ 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', + paywallSheet: { + flex: 1, borderTopLeftRadius: 30, borderTopRightRadius: 30, - paddingHorizontal: 20, - paddingTop: 9, - paddingBottom: 18, + overflow: 'hidden', zIndex: 5, }, - hardPaywallSheetCompact: { - paddingHorizontal: 18, - paddingTop: 8, - paddingBottom: 16, - }, sheetHandle: { alignSelf: 'center', width: 42, height: 5, borderRadius: 999, - backgroundColor: '#C7C8C3', + marginTop: 10, 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, - }, + paywallBody: { paddingHorizontal: 22, paddingTop: 10, paddingBottom: 24 }, + 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' }, + paywallFooterText: { fontSize: 12, fontWeight: '600' }, safeArea: { flex: 1 }, header: { flexDirection: 'row', alignItems: 'center', padding: 16 }, backButton: { width: 40, height: 40, justifyContent: 'center' }, @@ -1768,17 +1563,6 @@ const styles = StyleSheet.create({ 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,