Not a Plant Fehlermeldung
This commit is contained in:
107
app/_layout.tsx
107
app/_layout.tsx
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Animated, Easing, Image, StyleSheet, Text, View } from 'react-native';
|
||||
import { Animated, AppState, Easing, Image, StyleSheet, Text, View } from 'react-native';
|
||||
import { Redirect, Stack, usePathname } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
@@ -53,14 +53,14 @@ const ensureInstallConsistency = async (): Promise<boolean> => {
|
||||
|
||||
import { AnimatedSplashScreen } from '../components/AnimatedSplashScreen';
|
||||
|
||||
function RootLayoutInner() {
|
||||
const { isDarkMode, colorPalette, signOut, session, isInitializing, isLoadingPlants, syncRevenueCatState } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const pathname = usePathname();
|
||||
const [installCheckDone, setInstallCheckDone] = useState(false);
|
||||
const [splashAnimationComplete, setSplashAnimationComplete] = useState(false);
|
||||
const [revenueCatReady, setRevenueCatReady] = useState(Constants.appOwnership === 'expo');
|
||||
const posthog = usePostHog();
|
||||
function RootLayoutInner() {
|
||||
const { isDarkMode, colorPalette, signOut, session, isInitializing, isLoadingPlants, syncRevenueCatState } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const pathname = usePathname();
|
||||
const [installCheckDone, setInstallCheckDone] = useState(false);
|
||||
const [splashAnimationComplete, setSplashAnimationComplete] = useState(false);
|
||||
const [revenueCatReady, setRevenueCatReady] = useState(Constants.appOwnership === 'expo');
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
// RevenueCat requires native store access — not available in Expo Go
|
||||
@@ -72,42 +72,42 @@ function RootLayoutInner() {
|
||||
|
||||
Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
|
||||
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 });
|
||||
}
|
||||
setRevenueCatReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
if (isExpoGo || !revenueCatReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
if (session?.serverUserId) {
|
||||
await Purchases.logIn(session.serverUserId);
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
if (!cancelled) {
|
||||
await syncRevenueCatState(customerInfo as any, 'app_init');
|
||||
}
|
||||
} else {
|
||||
await Purchases.logOut();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to align RevenueCat identity', error);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [revenueCatReady, session?.serverUserId, syncRevenueCatState]);
|
||||
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 });
|
||||
}
|
||||
setRevenueCatReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
if (isExpoGo || !revenueCatReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
if (session?.serverUserId) {
|
||||
await Purchases.logIn(session.serverUserId);
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
if (!cancelled) {
|
||||
await syncRevenueCatState(customerInfo as any, 'app_init');
|
||||
}
|
||||
} else {
|
||||
await Purchases.logOut();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to align RevenueCat identity', error);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [revenueCatReady, session?.serverUserId, syncRevenueCatState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.serverUserId) {
|
||||
@@ -120,6 +120,20 @@ function RootLayoutInner() {
|
||||
}
|
||||
}, [session, posthog]);
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('screen_viewed', { screen: pathname });
|
||||
}, [pathname, posthog]);
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('app_opened');
|
||||
const subscription = AppState.addEventListener('change', (nextState) => {
|
||||
if (nextState === 'active') {
|
||||
posthog.capture('app_opened');
|
||||
}
|
||||
});
|
||||
return () => subscription.remove();
|
||||
}, [posthog]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const didResetSessionForFreshInstall = await ensureInstallConsistency();
|
||||
@@ -226,7 +240,8 @@ export default function RootLayout() {
|
||||
return (
|
||||
<PostHogProvider apiKey={POSTHOG_API_KEY} options={{
|
||||
host: 'https://us.i.posthog.com',
|
||||
enableSessionReplay: true,
|
||||
enableSessionReplay: false,
|
||||
debug: __DEV__,
|
||||
}}>
|
||||
<AppProvider>
|
||||
<CoachMarksProvider>
|
||||
|
||||
@@ -12,6 +12,7 @@ import Purchases, {
|
||||
PurchasesStoreProduct,
|
||||
} from 'react-native-purchases';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { usePostHog } from 'posthog-react-native';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
@@ -211,6 +212,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 copy = getBillingCopy(language);
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
|
||||
@@ -272,6 +274,10 @@ export default function BillingScreen() {
|
||||
};
|
||||
}, [isExpoGo]);
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('paywall_viewed', { plan_id: planId });
|
||||
}, [posthog, planId]);
|
||||
|
||||
const monthlyPackage = subscriptionPackages.monthly_pro;
|
||||
const yearlyPackage = subscriptionPackages.yearly_pro;
|
||||
|
||||
@@ -290,6 +296,7 @@ export default function BillingScreen() {
|
||||
|
||||
const handlePurchase = async (productId: PurchaseProductId) => {
|
||||
setIsUpdating(true);
|
||||
posthog.capture('purchase_initiated', { product_id: productId });
|
||||
try {
|
||||
if (isExpoGo) {
|
||||
// ExpoGo has no native RevenueCat — use simulation for development only
|
||||
@@ -316,6 +323,7 @@ export default function BillingScreen() {
|
||||
// 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');
|
||||
posthog.capture('subscription_started', { product_id: productId });
|
||||
} else {
|
||||
const selectedProduct = topupProducts[productId];
|
||||
if (!selectedProduct) {
|
||||
@@ -324,6 +332,7 @@ export default function BillingScreen() {
|
||||
await Purchases.purchaseStoreProduct(selectedProduct);
|
||||
const customerInfo = await Purchases.getCustomerInfo();
|
||||
await syncRevenueCatState(customerInfo as any, 'topup_purchase');
|
||||
posthog.capture('topup_purchased', { product_id: productId });
|
||||
}
|
||||
}
|
||||
setSubModalVisible(false);
|
||||
@@ -332,6 +341,7 @@ export default function BillingScreen() {
|
||||
const userCancelled = typeof e === 'object' && e !== null && 'userCancelled' in e && Boolean((e as { userCancelled?: boolean }).userCancelled);
|
||||
|
||||
if (userCancelled) {
|
||||
posthog.capture('purchase_cancelled', { product_id: productId });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,6 +355,7 @@ export default function BillingScreen() {
|
||||
}
|
||||
|
||||
console.error('Payment failed', e);
|
||||
posthog.capture('purchase_failed', { product_id: productId, error: msg });
|
||||
Alert.alert('Unerwarteter Fehler', msg);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
|
||||
@@ -37,6 +37,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
timeoutTitle: 'Scan zu langsam',
|
||||
timeoutMessage: 'Die Analyse hat zu lange gedauert. Bitte erneut versuchen.',
|
||||
retryLabel: 'Erneut versuchen',
|
||||
notAPlantTitle: 'Keine Pflanze erkannt',
|
||||
notAPlantMessage: 'Das Bild zeigt keine erkennbare Pflanze. Bitte fotografiere eine Pflanze und versuche es erneut.',
|
||||
providerErrorMessage: 'KI-Scan gerade nicht verfügbar. Bitte versuche es erneut.',
|
||||
healthProviderErrorMessage: 'KI-Health-Check gerade nicht verfügbar. Bitte versuche es erneut.',
|
||||
healthTitle: 'Health Check',
|
||||
@@ -61,6 +63,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
timeoutTitle: 'Escaneo lento',
|
||||
timeoutMessage: 'El análisis tardó demasiado. Inténtalo de nuevo.',
|
||||
retryLabel: 'Reintentar',
|
||||
notAPlantTitle: 'No es una planta',
|
||||
notAPlantMessage: 'La imagen no muestra una planta reconocible. Por favor fotografía una planta e inténtalo de nuevo.',
|
||||
providerErrorMessage: 'Escaneo IA no disponible ahora. Inténtalo de nuevo.',
|
||||
healthProviderErrorMessage: 'Health-check IA no disponible ahora. Inténtalo de nuevo.',
|
||||
healthTitle: 'Health Check',
|
||||
@@ -84,6 +88,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
timeoutTitle: 'Scan Too Slow',
|
||||
timeoutMessage: 'Analysis took too long. Please try again.',
|
||||
retryLabel: 'Try again',
|
||||
notAPlantTitle: 'No plant detected',
|
||||
notAPlantMessage: 'The image does not show a recognizable plant. Please photograph a plant and try again.',
|
||||
providerErrorMessage: 'AI scan is currently unavailable. Please try again.',
|
||||
healthProviderErrorMessage: 'AI health check is currently unavailable. Please try again.',
|
||||
healthTitle: 'Health Check',
|
||||
@@ -181,8 +187,8 @@ export default function ScannerScreen() {
|
||||
try {
|
||||
const result = await ImageManipulator.manipulateAsync(
|
||||
uri,
|
||||
[{ resize: { width: 1024 } }],
|
||||
{ compress: 0.6, format: ImageManipulator.SaveFormat.JPEG, base64: true },
|
||||
[{ resize: { width: 768 } }],
|
||||
{ compress: 0.7, format: ImageManipulator.SaveFormat.JPEG, base64: true },
|
||||
);
|
||||
return result.base64 ? `data:image/jpeg;base64,${result.base64}` : result.uri;
|
||||
} catch {
|
||||
@@ -334,6 +340,12 @@ export default function ScannerScreen() {
|
||||
{ text: billingCopy.retryLabel, onPress: () => analyzeImage(imageUri, galleryImageUri) },
|
||||
],
|
||||
);
|
||||
} else if (isBackendApiError(error) && error.code === 'NOT_A_PLANT') {
|
||||
Alert.alert(
|
||||
billingCopy.notAPlantTitle,
|
||||
billingCopy.notAPlantMessage,
|
||||
[{ text: billingCopy.dismiss, style: 'cancel' }],
|
||||
);
|
||||
} else if (isBackendApiError(error) && error.code === 'PROVIDER_ERROR') {
|
||||
Alert.alert(
|
||||
billingCopy.genericErrorTitle,
|
||||
@@ -357,14 +369,11 @@ export default function ScannerScreen() {
|
||||
const takePicture = async () => {
|
||||
if (!cameraRef.current || isAnalyzing) return;
|
||||
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
const photo = await cameraRef.current.takePictureAsync({ base64: true, quality: 0.5 });
|
||||
const photo = await cameraRef.current.takePictureAsync({ base64: false, quality: 0.9 });
|
||||
if (photo) {
|
||||
const analysisUri = photo.base64
|
||||
? `data:image/jpeg;base64,${photo.base64}`
|
||||
: photo.uri;
|
||||
const galleryUri = photo.uri || analysisUri;
|
||||
const analysisUri = await resizeForAnalysis(photo.uri);
|
||||
setSelectedImage(analysisUri);
|
||||
analyzeImage(analysisUri, galleryUri);
|
||||
analyzeImage(analysisUri, photo.uri);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user