feat(app): scanner uses real credits for free users, demo mode only for guests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ 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';
|
||||
|
||||
const HEALTH_CHECK_CREDIT_COST = 2;
|
||||
const DEMO_SCAN_LIMIT = 5;
|
||||
@@ -57,6 +58,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoNoCreditsTitle: 'Demo-Scans aufgebraucht',
|
||||
demoNoCreditsMessage: 'Du hast deine 5 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',
|
||||
emailCta: 'Mit E-Mail fortfahren',
|
||||
unlockCta: 'Vollständige Diagnose freischalten',
|
||||
@@ -91,6 +93,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoNoCreditsTitle: 'Escaneos demo agotados',
|
||||
demoNoCreditsMessage: 'Ya usaste tus 5 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',
|
||||
emailCta: 'Continuar con email',
|
||||
unlockCta: 'Desbloquear diagnóstico completo',
|
||||
@@ -124,6 +127,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoNoCreditsTitle: 'Demo scans used',
|
||||
demoNoCreditsMessage: 'You used your 5 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',
|
||||
emailCta: 'Continue with email',
|
||||
unlockCta: 'Unlock full diagnosis',
|
||||
@@ -167,8 +171,8 @@ export default function ScannerScreen() {
|
||||
: params.sharedImageKey;
|
||||
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
|
||||
&& billingSummary?.entitlement?.status === 'active';
|
||||
const isDemoMode = !hasActiveEntitlement;
|
||||
const availableCredits = hasActiveEntitlement ? (billingSummary?.credits.available ?? 0) : 0;
|
||||
const isDemoMode = !session; // guests get the local demo scan; signed-in users burn real credits
|
||||
const availableCredits = billingSummary?.credits.available ?? 0;
|
||||
const demoScansRemaining = Math.max(0, DEMO_SCAN_LIMIT - guestScanCount);
|
||||
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
@@ -179,6 +183,7 @@ export default function ScannerScreen() {
|
||||
const [analysisProgress, setAnalysisProgress] = useState(0);
|
||||
const [analysisResult, setAnalysisResult] = useState<IdentificationResult | null>(null);
|
||||
const [demoResultVisible, setDemoResultVisible] = useState(false);
|
||||
const [outOfCreditsVisible, setOutOfCreditsVisible] = useState(false);
|
||||
const cameraRef = useRef<CameraView>(null);
|
||||
const scanLineProgress = useRef(new Animated.Value(0)).current;
|
||||
const scanPulse = useRef(new Animated.Value(0)).current;
|
||||
@@ -281,18 +286,10 @@ export default function ScannerScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDemoMode && availableCredits <= 0) {
|
||||
Alert.alert(
|
||||
billingCopy.noCreditsTitle,
|
||||
isHealthMode ? billingCopy.healthNoCreditsMessage : billingCopy.noCreditsMessage,
|
||||
[
|
||||
{ text: billingCopy.dismiss, style: 'cancel' },
|
||||
{
|
||||
text: billingCopy.managePlan,
|
||||
onPress: () => router.replace('/profile/billing'),
|
||||
},
|
||||
],
|
||||
);
|
||||
const requiredCredits = isHealthMode ? 2 : 1;
|
||||
if (!isDemoMode && availableCredits < requiredCredits) {
|
||||
posthog.capture('out_of_credits_shown', { trigger: 'pre_check', scan_type: isHealthMode ? 'health_check' : 'identification' });
|
||||
setOutOfCreditsVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -412,17 +409,8 @@ export default function ScannerScreen() {
|
||||
});
|
||||
|
||||
if (isInsufficientCreditsError(error)) {
|
||||
Alert.alert(
|
||||
billingCopy.noCreditsTitle,
|
||||
isHealthMode ? billingCopy.healthNoCreditsMessage : billingCopy.noCreditsMessage,
|
||||
[
|
||||
{ text: billingCopy.dismiss, style: 'cancel' },
|
||||
{
|
||||
text: billingCopy.managePlan,
|
||||
onPress: () => router.replace('/profile/billing'),
|
||||
},
|
||||
],
|
||||
);
|
||||
posthog.capture('out_of_credits_shown', { trigger: 'server_402', scan_type: isHealthMode ? 'health_check' : 'identification' });
|
||||
setOutOfCreditsVisible(true);
|
||||
} else if (isTimeoutError(error)) {
|
||||
Alert.alert(
|
||||
billingCopy.timeoutTitle,
|
||||
@@ -689,7 +677,11 @@ export default function ScannerScreen() {
|
||||
<View style={[styles.creditBadge, { backgroundColor: colors.heroButton, borderColor: colors.heroButtonBorder }]}>
|
||||
<Ionicons name={isDemoMode ? 'sparkles-outline' : 'wallet-outline'} size={12} color={colors.text} />
|
||||
<Text style={[styles.creditBadgeText, { color: colors.text }]}>
|
||||
{isDemoMode ? billingCopy.demoCreditsRemaining(demoScansRemaining) : `${billingCopy.creditsLabel}: ${availableCredits}`}
|
||||
{isDemoMode
|
||||
? billingCopy.demoCreditsRemaining(demoScansRemaining)
|
||||
: !hasActiveEntitlement
|
||||
? billingCopy.creditsRemaining(availableCredits)
|
||||
: `${billingCopy.creditsLabel}: ${availableCredits}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -865,6 +857,26 @@ export default function ScannerScreen() {
|
||||
<Text style={[styles.controlLabel, { color: colors.textMuted }]}>{t.help}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<OutOfCreditsSheet
|
||||
visible={outOfCreditsVisible}
|
||||
language={language}
|
||||
colors={colors}
|
||||
renewsAtIso={billingSummary?.credits.cycleEndsAt}
|
||||
onSeePlans={() => {
|
||||
setOutOfCreditsVisible(false);
|
||||
posthog.capture('paywall_opened', { source: 'out_of_credits' });
|
||||
router.push('/profile/billing?view=paywall');
|
||||
}}
|
||||
onTopup={() => {
|
||||
setOutOfCreditsVisible(false);
|
||||
router.push('/profile/billing'); // topups live on the billing management screen
|
||||
}}
|
||||
onDismiss={() => {
|
||||
posthog.capture('paywall_dismissed', { source: 'out_of_credits_sheet' });
|
||||
setOutOfCreditsVisible(false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user