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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user