App chrash + seo
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, ImageBackground, useWindowDimensions } from 'react-native';
|
||||
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, ImageBackground, Platform, useWindowDimensions } 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, {
|
||||
LOG_LEVEL,
|
||||
PACKAGE_TYPE,
|
||||
PRODUCT_CATEGORY,
|
||||
PurchasesOffering,
|
||||
@@ -13,7 +14,7 @@ import Purchases, {
|
||||
PurchasesStoreProduct,
|
||||
} from 'react-native-purchases';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { usePostHog } from 'posthog-react-native';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
@@ -82,6 +83,24 @@ const summarizeOfferingPackages = (offering: PurchasesOffering | null) => {
|
||||
};
|
||||
};
|
||||
|
||||
let revenueCatConfigured = false;
|
||||
|
||||
const ensureRevenueCatConfigured = () => {
|
||||
if (revenueCatConfigured || Constants.appOwnership === 'expo') {
|
||||
return;
|
||||
}
|
||||
|
||||
Purchases.setLogLevel(LOG_LEVEL.WARN);
|
||||
const iosApiKey = process.env.EXPO_PUBLIC_REVENUECAT_IOS_API_KEY || 'appl_hrSpsuUuVstbHhYIDnOqYxPOnmR';
|
||||
const androidApiKey = process.env.EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY || 'goog_placeholder';
|
||||
if (Platform.OS === 'ios') {
|
||||
Purchases.configure({ apiKey: iosApiKey });
|
||||
} else if (Platform.OS === 'android') {
|
||||
Purchases.configure({ apiKey: androidApiKey });
|
||||
}
|
||||
revenueCatConfigured = true;
|
||||
};
|
||||
|
||||
const getBillingCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
@@ -319,7 +338,7 @@ export default function BillingScreen() {
|
||||
const router = useRouter();
|
||||
const { isDarkMode, language, billingSummary, isLoadingBilling, simulatePurchase, simulateWebhookEvent, syncRevenueCatState, colorPalette, session } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const posthog = usePostHog();
|
||||
const posthog = useSafeAnalytics();
|
||||
const copy = getBillingCopy(language);
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
@@ -328,6 +347,7 @@ export default function BillingScreen() {
|
||||
const [subModalVisible, setSubModalVisible] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [storeReady, setStoreReady] = useState(isExpoGo);
|
||||
const [storeError, setStoreError] = useState<string | null>(null);
|
||||
const [subscriptionPackages, setSubscriptionPackages] = useState<SubscriptionPackages>({});
|
||||
const [topupProducts, setTopupProducts] = useState<TopupProducts>({});
|
||||
const [selectedPaywallPlan, setSelectedPaywallPlan] = useState<PaywallPlanId>('yearly');
|
||||
@@ -349,6 +369,7 @@ export default function BillingScreen() {
|
||||
}
|
||||
|
||||
try {
|
||||
ensureRevenueCatConfigured();
|
||||
const [offerings, topups] = await Promise.all([
|
||||
Purchases.getOfferings(),
|
||||
Purchases.getProducts(['topup_small', 'topup_medium', 'topup_large'], PRODUCT_CATEGORY.NON_SUBSCRIPTION),
|
||||
@@ -369,8 +390,12 @@ export default function BillingScreen() {
|
||||
topup_medium: topups.find((product) => product.identifier === 'topup_medium'),
|
||||
topup_large: topups.find((product) => product.identifier === 'topup_large'),
|
||||
});
|
||||
setStoreError(null);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load RevenueCat products', error);
|
||||
if (!cancelled) {
|
||||
setStoreError('Purchases are temporarily unavailable. Please try again later.');
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setStoreReady(true);
|
||||
@@ -386,12 +411,14 @@ export default function BillingScreen() {
|
||||
}, [isExpoGo]);
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('paywall_viewed', { plan_id: planId });
|
||||
try { posthog.capture('paywall_viewed', { plan_id: planId }); } catch {}
|
||||
if (showPaywallPlans) {
|
||||
posthog.capture('hard_paywall_viewed', {
|
||||
plan_id: planId,
|
||||
authenticated: Boolean(session),
|
||||
});
|
||||
try {
|
||||
posthog.capture('hard_paywall_viewed', {
|
||||
plan_id: planId,
|
||||
authenticated: Boolean(session),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
}, [posthog, planId, session?.serverUserId, showPaywallPlans]);
|
||||
|
||||
@@ -467,6 +494,11 @@ export default function BillingScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isExpoGo && storeError) {
|
||||
Alert.alert('Purchases unavailable', storeError);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdating(true);
|
||||
posthog.capture('purchase_initiated', { product_id: productId });
|
||||
try {
|
||||
@@ -483,6 +515,7 @@ export default function BillingScreen() {
|
||||
await completeExpoGoSimulation(productId);
|
||||
return;
|
||||
} else {
|
||||
ensureRevenueCatConfigured();
|
||||
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
|
||||
if (planId === 'pro') {
|
||||
await openAppleSubscriptions();
|
||||
@@ -553,6 +586,7 @@ export default function BillingScreen() {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
if (!isExpoGo) {
|
||||
ensureRevenueCatConfigured();
|
||||
const customerInfo = await Purchases.restorePurchases();
|
||||
await syncRevenueCatState(customerInfo as any, 'restore');
|
||||
}
|
||||
@@ -681,10 +715,10 @@ export default function BillingScreen() {
|
||||
style={[
|
||||
styles.hardPaywallCta,
|
||||
compactPaywall && styles.hardPaywallCtaCompact,
|
||||
(!storeReady || isUpdating) && styles.disabledPlanCard,
|
||||
(!storeReady || isUpdating || Boolean(storeError)) && styles.disabledPlanCard,
|
||||
]}
|
||||
onPress={() => handlePurchase(selectedProductId)}
|
||||
disabled={isUpdating || !storeReady}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
{isUpdating || !storeReady ? (
|
||||
@@ -699,6 +733,9 @@ export default function BillingScreen() {
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{storeError ? (
|
||||
<Text style={styles.hardPaywallFooter}>{storeError}</Text>
|
||||
) : null}
|
||||
<Text style={styles.hardPaywallFooter}>{paywallFooterLabel}</Text>
|
||||
<View style={styles.legalLinksRow}>
|
||||
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
|
||||
@@ -778,7 +815,7 @@ export default function BillingScreen() {
|
||||
<TouchableOpacity
|
||||
style={[styles.paywallPlanCardPrimary, { borderColor: colors.primary, backgroundColor: colors.primary + '12' }]}
|
||||
onPress={() => handlePurchase('yearly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<View style={styles.planTopRow}>
|
||||
@@ -805,10 +842,10 @@ export default function BillingScreen() {
|
||||
style={[
|
||||
styles.paywallPlanCardSecondary,
|
||||
{ borderColor: colors.border },
|
||||
(!storeReady || isUpdating) && styles.disabledPlanCard,
|
||||
(!storeReady || isUpdating || Boolean(storeError)) && styles.disabledPlanCard,
|
||||
]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<View style={styles.planTopRow}>
|
||||
@@ -876,7 +913,7 @@ export default function BillingScreen() {
|
||||
}
|
||||
]}
|
||||
onPress={() => handlePurchase(pack.id)}
|
||||
disabled={isUpdating || !storeReady}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Ionicons name="flash" size={18} color={colors.primary} />
|
||||
@@ -953,7 +990,7 @@ export default function BillingScreen() {
|
||||
planId === 'pro' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
|
||||
]}
|
||||
onPress={() => handlePurchase('monthly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.planHeaderRow}>
|
||||
@@ -984,7 +1021,7 @@ export default function BillingScreen() {
|
||||
planId === 'pro' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
|
||||
]}
|
||||
onPress={() => handlePurchase('yearly_pro')}
|
||||
disabled={isUpdating || !storeReady}
|
||||
disabled={isUpdating || !storeReady || Boolean(storeError)}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={styles.planHeaderRow}>
|
||||
@@ -1070,8 +1107,8 @@ export default function BillingScreen() {
|
||||
<TouchableOpacity
|
||||
style={styles.offerDeclineBtn}
|
||||
onPress={finalizeCancel}
|
||||
disabled={isUpdating || !storeReady}
|
||||
>
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<Text style={[styles.offerDeclineBtnText, { color: colors.textMuted }]}>{copy.offerDecline}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user