fix(app): free-tier billing management view, loading guard, dead paywall code cleanup

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Timo Knuth
2026-07-06 13:47:56 +02:00
parent 91dddc5137
commit 84624e50cf

View File

@@ -112,34 +112,11 @@ const getBillingCopy = (language: Language) => {
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',
@@ -200,34 +177,11 @@ const getBillingCopy = (language: Language) => {
manageSubscription: 'Administrar Suscripción',
subscriptionTitle: 'Suscripciones',
subscriptionHint: 'Elige un plan y desbloquea escaneos con IA más potentes y más créditos.',
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',
@@ -288,34 +242,11 @@ const getBillingCopy = (language: Language) => {
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',
@@ -394,7 +325,7 @@ export default function BillingScreen() {
const planId = billingSummary?.entitlement?.plan || 'free';
const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? 0);
const showPaywallPlans = (!session || paywallRequested) && planId !== 'pro';
const showPaywallPlans = (!session || paywallRequested) && (!isLoadingBilling || !session) && planId !== 'pro';
useEffect(() => {
let cancelled = false;
@@ -802,122 +733,39 @@ export default function BillingScreen() {
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 40 }} />
) : (
<>
{session && planId === 'pro' && (
{session && (
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.planLabel}</Text>
<View style={[styles.row, { marginBottom: 16 }]}>
<Text style={[styles.value, { color: colors.text }]}>
{planId === 'pro' ? copy.planPro : copy.planFree}
</Text>
<TouchableOpacity
style={[styles.manageBtn, { backgroundColor: colors.primary }]}
onPress={() => setSubModalVisible(true)}
>
<Text style={styles.manageBtnText}>{copy.manageSubscription}</Text>
</TouchableOpacity>
{planId === 'pro' && (
<TouchableOpacity
style={[styles.manageBtn, { backgroundColor: colors.primary }]}
onPress={() => setSubModalVisible(true)}
>
<Text style={styles.manageBtnText}>{copy.manageSubscription}</Text>
</TouchableOpacity>
)}
</View>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.creditsAvailableLabel}</Text>
<Text style={[styles.creditsValue, { color: colors.text }]}>{credits}</Text>
{planId !== 'pro' && (
<TouchableOpacity
style={[styles.upgradeCta, { backgroundColor: colors.primary }]}
onPress={() => router.push('/profile/billing?view=paywall')}
activeOpacity={0.86}
>
<Ionicons name="sparkles-outline" size={18} color={colors.onPrimary} />
<Text style={[styles.upgradeCtaText, { color: colors.onPrimary }]}>{copy.startTrial}</Text>
</TouchableOpacity>
)}
</View>
)}
{showPaywallPlans && (
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
<Text style={[styles.paywallTitle, { color: colors.text }]}>{copy.paywallTitle}</Text>
<Text style={[styles.modalHint, { color: colors.text + '80', marginBottom: 16 }]}>
{copy.paywallHint}
</Text>
<View style={styles.paywallValueRows}>
{copy.proBenefits.slice(0, 3).map((benefit, index) => (
<View key={benefit} style={[styles.paywallValueRow, { backgroundColor: colors.surfaceMuted }]}>
<Ionicons
name={index === 0 ? 'scan-outline' : index === 1 ? 'medkit-outline' : 'calendar-outline'}
size={17}
color={colors.primary}
/>
<Text style={[styles.paywallValueText, { color: colors.text }]}>{benefit}</Text>
</View>
))}
</View>
<TouchableOpacity
style={[styles.paywallPlanCardPrimary, { borderColor: colors.primary, backgroundColor: colors.primary + '12' }]}
onPress={() => handlePurchase('yearly_pro')}
disabled={isUpdating || !storeReady || Boolean(storeError)}
activeOpacity={0.9}
>
<View style={styles.planTopRow}>
<View>
<Text style={[styles.guestPlanName, { color: colors.text }]}>GreenLens Pro</Text>
<Text style={[styles.planSubline, { color: colors.textMuted }]}>{copy.saveLabel}</Text>
</View>
<View style={[styles.proBadge, { backgroundColor: colors.primary }]}>
<Text style={styles.proBadgeText}>{copy.yearlyTrialBadge}</Text>
</View>
</View>
<View style={styles.priceRow}>
<Text style={[styles.guestPlanPrice, { color: colors.text }]}>{yearlyPrice}</Text>
<Text style={[styles.planTerm, { color: colors.textMuted }]}>{copy.perYear}</Text>
</View>
<Text style={[styles.guestPlanRenew, { color: colors.textMuted }]}>{copy.yearlySubline}</Text>
<View style={[styles.trialCallout, { backgroundColor: colors.primarySoft }]}>
<Ionicons name="sparkles-outline" size={16} color={colors.primary} />
<Text style={[styles.trialCalloutText, { color: colors.text }]}>{copy.startTrial}</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.paywallPlanCardSecondary,
{ borderColor: colors.border },
(!storeReady || isUpdating || Boolean(storeError)) && styles.disabledPlanCard,
]}
onPress={() => handlePurchase('monthly_pro')}
disabled={isUpdating || !storeReady || Boolean(storeError)}
activeOpacity={0.9}
>
<View style={styles.planTopRow}>
<View style={{ flex: 1 }}>
<Text style={[styles.guestPlanName, { color: colors.text }]}>Monatlich</Text>
<Text style={[styles.planSubline, { color: colors.textMuted }]}>{copy.monthlySubline}</Text>
</View>
<View style={[styles.secondaryBadge, { borderColor: colors.borderStrong }]}>
<Text style={[styles.secondaryBadgeText, { color: colors.textSecondary }]}>{copy.monthlyBadge}</Text>
</View>
</View>
<View style={styles.priceRow}>
<Text style={[styles.guestPlanPrice, { color: colors.text }]}>{monthlyPrice}</Text>
<Text style={[styles.planTerm, { color: colors.textMuted }]}>{copy.perMonth}</Text>
</View>
<View style={[styles.monthlyCtaButton, { backgroundColor: colors.surfaceMuted, borderColor: colors.border }]}>
{!storeReady ? (
<ActivityIndicator size="small" color={colors.primary} />
) : (
<>
<Text style={[styles.monthlyCtaText, { color: colors.primary }]}>{copy.monthlyCta}</Text>
<Ionicons name="arrow-forward" size={14} color={colors.primary} />
</>
)}
</View>
</TouchableOpacity>
<View style={[styles.legalLinksRow, { marginTop: 16 }]}>
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
<Text style={[styles.legalLink, { color: colors.primary }]}>Privacy Policy</Text>
</TouchableOpacity>
<Text style={[styles.legalSep, { color: colors.textMuted }]}> · </Text>
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/terms')}>
<Text style={[styles.legalLink, { color: colors.primary }]}>Terms of Use</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.restoreBtn} onPress={handleRestore} disabled={isUpdating}>
<Text style={[styles.legalLink, { color: colors.textMuted }]}>{copy.restorePurchases}</Text>
</TouchableOpacity>
</View>
)}
{session && planId === 'pro' && !isExpoGo ? (
{session && !isExpoGo ? (
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.border }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{copy.topupTitle}</Text>
<Text style={[styles.modalHint, { color: colors.text + '80', marginBottom: 8 }]}>{copy.topupHint}</Text>
@@ -1264,30 +1112,18 @@ const styles = StyleSheet.create({
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,
upgradeCta: {
marginTop: 16,
height: 50,
borderRadius: 14,
flexDirection: 'row',
alignItems: 'center',
gap: 9,
justifyContent: 'center',
gap: 8,
},
paywallValueText: {
flex: 1,
fontSize: 13,
fontWeight: '700',
lineHeight: 18,
upgradeCtaText: {
fontSize: 16,
fontWeight: '800',
},
topupBtn: {
flexDirection: 'row',
@@ -1446,114 +1282,9 @@ const styles = StyleSheet.create({
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',