Onboarding flow
This commit is contained in:
@@ -19,14 +19,13 @@ import { PlantRecognitionService } from '../services/plantRecognitionService';
|
||||
import { IdentificationResult } from '../types';
|
||||
import { ResultCard } from '../components/ResultCard';
|
||||
import { backendApiClient, isInsufficientCreditsError, isNetworkError, isTimeoutError } from '../services/backend/backendApiClient';
|
||||
import { isBackendApiError } from '../services/backend/contracts';
|
||||
import { createIdempotencyKey } from '../utils/idempotency';
|
||||
import { AuthService } from '../services/authService';
|
||||
import { getMockPlantByImage } from '../services/backend/mockCatalog';
|
||||
import { consumeSharedImageUri, SHARE_INTENT_KEY } from '../utils/shareHandoff';
|
||||
import { OutOfCreditsSheet } from '../components/OutOfCreditsSheet';
|
||||
import { isBackendApiError } from '../services/backend/contracts';
|
||||
import { createIdempotencyKey } from '../utils/idempotency';
|
||||
import { AuthService } from '../services/authService';
|
||||
import { consumeSharedImageUri, SHARE_INTENT_KEY } from '../utils/shareHandoff';
|
||||
import { OutOfCreditsSheet } from '../components/OutOfCreditsSheet';
|
||||
|
||||
const DEMO_SCAN_LIMIT = 5;
|
||||
const DEMO_SCAN_LIMIT = 3;
|
||||
|
||||
const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
if (language === 'de') {
|
||||
@@ -52,7 +51,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoTitle: 'Rettungsplan bereit',
|
||||
demoMessage: 'Wir haben mögliche Ursachen erkannt. Schalte die vollständige KI-Diagnose und deinen 7-Tage-Rettungsplan frei.',
|
||||
demoNoCreditsTitle: 'Demo-Scans aufgebraucht',
|
||||
demoNoCreditsMessage: 'Du hast deine 5 kostenlosen Demo-Scans auf diesem Gerät genutzt. Starte Pro, um weiter Pflanzen zu scannen.',
|
||||
demoNoCreditsMessage: 'Du hast deine 3 kostenlosen Demo-Scans auf diesem Gerät genutzt. Starte Pro, um weiter Pflanzen zu scannen.',
|
||||
demoCreditsRemaining: (count: number) => `${count} Demo-Scans übrig`,
|
||||
creditsRemaining: (count: number) => `${count} Scans übrig`,
|
||||
appleCta: 'Mit Apple fortfahren',
|
||||
@@ -84,7 +83,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoTitle: 'Plan de rescate listo',
|
||||
demoMessage: 'Detectamos posibles causas. Desbloquea el diagnóstico completo con IA y tu plan de rescate de 7 días.',
|
||||
demoNoCreditsTitle: 'Escaneos demo agotados',
|
||||
demoNoCreditsMessage: 'Ya usaste tus 5 escaneos demo gratuitos en este dispositivo. Inicia Pro para seguir escaneando plantas.',
|
||||
demoNoCreditsMessage: 'Ya usaste tus 3 escaneos demo gratuitos en este dispositivo. Inicia Pro para seguir escaneando plantas.',
|
||||
demoCreditsRemaining: (count: number) => `${count} escaneos demo restantes`,
|
||||
creditsRemaining: (count: number) => `${count} escaneos restantes`,
|
||||
appleCta: 'Continuar con Apple',
|
||||
@@ -115,7 +114,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoTitle: 'Rescue plan ready',
|
||||
demoMessage: 'We found possible causes. Unlock the full AI diagnosis and your 7-day rescue plan.',
|
||||
demoNoCreditsTitle: 'Demo scans used',
|
||||
demoNoCreditsMessage: 'You used your 5 free demo scans on this device. Start Pro to keep scanning plants.',
|
||||
demoNoCreditsMessage: 'You used your 3 free demo scans on this device. Start Pro to keep scanning plants.',
|
||||
demoCreditsRemaining: (count: number) => `${count} demo scans left`,
|
||||
creditsRemaining: (count: number) => `${count} scans left`,
|
||||
appleCta: 'Continue with Apple',
|
||||
@@ -161,7 +160,7 @@ export default function ScannerScreen() {
|
||||
: params.sharedImageKey;
|
||||
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
|
||||
&& billingSummary?.entitlement?.status === 'active';
|
||||
const isDemoMode = !session; // guests get the local demo scan; signed-in users burn real credits
|
||||
const isDemoMode = !session; // guests get limited AI demo scans; signed-in users burn real credits
|
||||
const availableCredits = billingSummary?.credits.available ?? 0;
|
||||
const demoScansRemaining = Math.max(0, DEMO_SCAN_LIMIT - guestScanCount);
|
||||
|
||||
@@ -262,17 +261,8 @@ export default function ScannerScreen() {
|
||||
if (isAnalyzing) return;
|
||||
|
||||
if (isDemoMode && guestScanCount >= DEMO_SCAN_LIMIT) {
|
||||
Alert.alert(
|
||||
billingCopy.demoNoCreditsTitle,
|
||||
billingCopy.demoNoCreditsMessage,
|
||||
[
|
||||
{ text: billingCopy.dismiss, style: 'cancel' },
|
||||
{
|
||||
text: billingCopy.managePlan,
|
||||
onPress: () => router.replace('/profile/billing'),
|
||||
},
|
||||
],
|
||||
);
|
||||
posthog.capture('out_of_credits_shown', { trigger: 'demo_limit', scan_type: 'identification' });
|
||||
setOutOfCreditsVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -303,21 +293,22 @@ export default function ScannerScreen() {
|
||||
}, 150);
|
||||
|
||||
try {
|
||||
if (isDemoMode) {
|
||||
posthog.capture('demo_scan_started', {
|
||||
authenticated: Boolean(session),
|
||||
scan_type: isHealthMode ? 'health_check' : 'identification',
|
||||
demo_scans_used: guestScanCount,
|
||||
demo_scans_remaining: demoScansRemaining,
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 2100));
|
||||
setAnalysisProgress(100);
|
||||
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
await new Promise(resolve => setTimeout(resolve, 350));
|
||||
const demoResult = getMockPlantByImage(galleryImageUri || imageUri, language, true);
|
||||
incrementGuestScanCount();
|
||||
setAnalysisResult(demoResult);
|
||||
posthog.capture('demo_scan_completed', {
|
||||
if (isDemoMode) {
|
||||
posthog.capture('demo_scan_started', {
|
||||
authenticated: Boolean(session),
|
||||
scan_type: isHealthMode ? 'health_check' : 'identification',
|
||||
demo_scans_used: guestScanCount,
|
||||
demo_scans_remaining: demoScansRemaining,
|
||||
});
|
||||
const demoResult = await PlantRecognitionService.identify(imageUri, language, {
|
||||
idempotencyKey: createIdempotencyKey('demo-scan-plant'),
|
||||
});
|
||||
setAnalysisProgress(100);
|
||||
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
await new Promise(resolve => setTimeout(resolve, 350));
|
||||
incrementGuestScanCount();
|
||||
setAnalysisResult(demoResult);
|
||||
posthog.capture('demo_scan_completed', {
|
||||
authenticated: Boolean(session),
|
||||
latency_ms: Date.now() - startTime,
|
||||
demo_scans_used_after: guestScanCount + 1,
|
||||
@@ -859,16 +850,27 @@ export default function ScannerScreen() {
|
||||
renewsAtIso={billingSummary?.credits.cycleEndsAt}
|
||||
onSeePlans={() => {
|
||||
setOutOfCreditsVisible(false);
|
||||
posthog.capture('paywall_opened', { source: 'out_of_credits' });
|
||||
router.push('/profile/billing?view=paywall');
|
||||
if (!session) {
|
||||
posthog.capture('paywall_opened', { source: 'out_of_credits', authenticated: false });
|
||||
router.replace('/profile/billing?view=paywall&context=out_of_credits');
|
||||
return;
|
||||
}
|
||||
posthog.capture('billing_opened', { source: 'out_of_credits', authenticated: true });
|
||||
router.replace('/profile/billing');
|
||||
}}
|
||||
onTopup={() => {
|
||||
onTopup={(productId) => {
|
||||
setOutOfCreditsVisible(false);
|
||||
router.push('/profile/billing'); // topups live on the billing management screen
|
||||
posthog.capture('topup_prompt_opened', { source: 'out_of_credits', product_id: productId, authenticated: Boolean(session) });
|
||||
if (session) {
|
||||
router.replace({ pathname: '/profile/billing', params: { topup: productId } });
|
||||
return;
|
||||
}
|
||||
router.replace({ pathname: '/auth/signup', params: { returnTo: 'billing', topup: productId } });
|
||||
}}
|
||||
onDismiss={() => {
|
||||
posthog.capture('paywall_dismissed', { source: 'out_of_credits_sheet' });
|
||||
setOutOfCreditsVisible(false);
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user