Harte Paywall
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import Constants from 'expo-constants';
|
||||
import Purchases, {
|
||||
PACKAGE_TYPE,
|
||||
@@ -102,6 +103,7 @@ const getBillingCopy = (language: Language) => {
|
||||
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',
|
||||
@@ -165,6 +167,7 @@ const getBillingCopy = (language: Language) => {
|
||||
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',
|
||||
@@ -228,6 +231,7 @@ const getBillingCopy = (language: Language) => {
|
||||
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',
|
||||
@@ -290,7 +294,7 @@ export default function BillingScreen() {
|
||||
const [cancelStep, setCancelStep] = useState<'none' | 'survey' | 'offer'>('none');
|
||||
|
||||
const planId = billingSummary?.entitlement?.plan || 'free';
|
||||
const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? '--');
|
||||
const credits = isLoadingBilling && !billingSummary ? '...' : (billingSummary?.credits?.available ?? 0);
|
||||
const showPaywallPlans = !session || planId !== 'pro';
|
||||
|
||||
useEffect(() => {
|
||||
@@ -365,13 +369,31 @@ export default function BillingScreen() {
|
||||
await Linking.openURL('itms-apps://apps.apple.com/account/subscriptions');
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
const handleBack = useCallback(() => {
|
||||
if (showPaywallPlans) {
|
||||
router.replace(session ? '/scanner' : '/onboarding');
|
||||
router.replace('/onboarding');
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
if (router.canGoBack()) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
router.replace('/(tabs)');
|
||||
}, [router, showPaywallPlans]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
if (!showPaywallPlans) {
|
||||
return false;
|
||||
}
|
||||
router.replace('/onboarding');
|
||||
return true;
|
||||
});
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [router, showPaywallPlans]),
|
||||
);
|
||||
|
||||
const completeExpoGoSimulation = async (productId: PurchaseProductId) => {
|
||||
setIsUpdating(true);
|
||||
@@ -380,6 +402,8 @@ export default function BillingScreen() {
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
posthog.capture('subscription_started', { product_id: productId, simulated: true });
|
||||
posthog.capture('trial_started', { product_id: productId, simulated: true });
|
||||
setSubModalVisible(false);
|
||||
router.replace('/(tabs)');
|
||||
} else {
|
||||
posthog.capture('topup_purchased', { product_id: productId, simulated: true });
|
||||
}
|
||||
@@ -404,7 +428,8 @@ export default function BillingScreen() {
|
||||
setIsUpdating(false);
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
Alert.alert(copy.expoGoPurchaseTitle, copy.expoGoPurchaseMessage, [
|
||||
{ text: 'OK', style: 'cancel' },
|
||||
{ text: copy.continueWithoutPro, style: 'cancel' },
|
||||
{ text: copy.expoGoSimulate, onPress: () => completeExpoGoSimulation(productId) },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
@@ -428,10 +453,16 @@ export default function BillingScreen() {
|
||||
});
|
||||
throw new Error('Abo-Paket konnte nicht geladen werden. Bitte RevenueCat Offering prüfen.');
|
||||
}
|
||||
await Purchases.purchasePackage(selectedPackage);
|
||||
// Derive plan locally from RevenueCat — backend sync via webhook comes later (Step 3)
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
await syncRevenueCatState(customerInfo as any, 'subscription_purchase');
|
||||
const purchaseResult = await Purchases.purchasePackage(selectedPackage);
|
||||
// Apply RevenueCat entitlement locally and let backend sync finish in the background.
|
||||
const customerInfo = (purchaseResult as { customerInfo?: unknown }).customerInfo
|
||||
?? await Purchases.getCustomerInfo();
|
||||
void syncRevenueCatState(customerInfo as any, 'subscription_purchase');
|
||||
posthog.capture('subscription_started', { product_id: productId });
|
||||
posthog.capture('trial_started', { product_id: productId });
|
||||
setSubModalVisible(false);
|
||||
setTimeout(() => router.replace('/(tabs)'), 0);
|
||||
return;
|
||||
} else {
|
||||
const selectedProduct = topupProducts[productId];
|
||||
if (!selectedProduct) {
|
||||
@@ -442,12 +473,7 @@ export default function BillingScreen() {
|
||||
await syncRevenueCatState(customerInfo as any, 'topup_purchase');
|
||||
}
|
||||
}
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
posthog.capture('subscription_started', { product_id: productId });
|
||||
posthog.capture('trial_started', { product_id: productId });
|
||||
} else {
|
||||
posthog.capture('topup_purchased', { product_id: productId });
|
||||
}
|
||||
posthog.capture('topup_purchased', { product_id: productId });
|
||||
setSubModalVisible(false);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
@@ -532,9 +558,9 @@ export default function BillingScreen() {
|
||||
) : (
|
||||
<>
|
||||
{session && planId === 'pro' && (
|
||||
<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 }]}>
|
||||
<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>
|
||||
@@ -597,7 +623,11 @@ export default function BillingScreen() {
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.paywallPlanCardSecondary, { borderColor: colors.border }]}
|
||||
style={[
|
||||
styles.paywallPlanCardSecondary,
|
||||
{ borderColor: colors.border },
|
||||
(!storeReady || isUpdating) && styles.disabledPlanCard,
|
||||
]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
activeOpacity={0.9}
|
||||
@@ -615,7 +645,16 @@ export default function BillingScreen() {
|
||||
<Text style={[styles.guestPlanPrice, { color: colors.text }]}>{monthlyPrice}</Text>
|
||||
<Text style={[styles.planTerm, { color: colors.textMuted }]}>{copy.perMonth}</Text>
|
||||
</View>
|
||||
<Text style={[styles.monthlyCtaText, { color: colors.primary }]}>{copy.monthlyCta}</Text>
|
||||
<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 }]}>
|
||||
@@ -1106,6 +1145,9 @@ const styles = StyleSheet.create({
|
||||
padding: 14,
|
||||
marginTop: 10,
|
||||
},
|
||||
disabledPlanCard: {
|
||||
opacity: 0.72,
|
||||
},
|
||||
guestPlanHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -1174,7 +1216,18 @@ const styles = StyleSheet.create({
|
||||
monthlyCtaText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
marginTop: 8,
|
||||
},
|
||||
monthlyCtaButton: {
|
||||
minHeight: 42,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
marginTop: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
guestSubscribeBtn: {
|
||||
marginTop: 14,
|
||||
|
||||
Reference in New Issue
Block a user