feat(app): benefit slides with native overlays
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
495
app/onboarding/slides.tsx
Normal file
495
app/onboarding/slides.tsx
Normal file
@@ -0,0 +1,495 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { Language } from '../../types';
|
||||
|
||||
type ColorsType = ReturnType<typeof useColors>;
|
||||
|
||||
const getSlidesCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
slides: [
|
||||
{
|
||||
title: 'Scanne jede Pflanze',
|
||||
body: 'Richte die Kamera auf eine Pflanze und GreenLens erkennt sie in Sekunden.',
|
||||
},
|
||||
{
|
||||
title: 'Health Check & Pflegeplan',
|
||||
body: 'GreenLens erkennt Probleme früh und erstellt deinen Rettungsplan.',
|
||||
},
|
||||
{
|
||||
title: 'Nie mehr Gießen vergessen',
|
||||
body: 'Smarte Erinnerungen und deine Pflanzen-Bibliothek halten alles im Blick.',
|
||||
},
|
||||
],
|
||||
resultChip: 'Monstera · 98%',
|
||||
healthCheckLabel: 'Health Check',
|
||||
overwateringDetected: 'Überwässerung erkannt',
|
||||
rescuePlanReady: '7-Tage-Rettungsplan bereit',
|
||||
waterReminder: 'Monstera gießen — heute',
|
||||
fertilizeReminder: 'Basilikum düngen — in 3 Tagen',
|
||||
continueLabel: 'Weiter',
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
slides: [
|
||||
{
|
||||
title: 'Escanea cualquier planta',
|
||||
body: 'Apunta la cámara a una planta y GreenLens la identifica en segundos.',
|
||||
},
|
||||
{
|
||||
title: 'Chequeo de salud y plan de cuidados',
|
||||
body: 'GreenLens detecta problemas a tiempo y crea tu plan de rescate.',
|
||||
},
|
||||
{
|
||||
title: 'No olvides regar nunca más',
|
||||
body: 'Recordatorios inteligentes y tu biblioteca de plantas lo mantienen todo al día.',
|
||||
},
|
||||
],
|
||||
resultChip: 'Monstera · 98%',
|
||||
healthCheckLabel: 'Chequeo de salud',
|
||||
overwateringDetected: 'Exceso de riego detectado',
|
||||
rescuePlanReady: 'Plan de rescate de 7 días listo',
|
||||
waterReminder: 'Regar Monstera — hoy',
|
||||
fertilizeReminder: 'Abonar albahaca — en 3 días',
|
||||
continueLabel: 'Continuar',
|
||||
};
|
||||
}
|
||||
return {
|
||||
slides: [
|
||||
{
|
||||
title: 'Scan Any Plant',
|
||||
body: 'Point your camera at a plant and GreenLens identifies it in seconds.',
|
||||
},
|
||||
{
|
||||
title: 'Health Check & Care Plan',
|
||||
body: 'GreenLens spots problems early and builds a rescue plan for you.',
|
||||
},
|
||||
{
|
||||
title: 'Never Forget Watering',
|
||||
body: 'Smart reminders and your personal plant library keep everything on track.',
|
||||
},
|
||||
],
|
||||
resultChip: 'Monstera · 98%',
|
||||
healthCheckLabel: 'Health Check',
|
||||
overwateringDetected: 'Overwatering detected',
|
||||
rescuePlanReady: '7-day rescue plan ready',
|
||||
waterReminder: 'Water Monstera — today',
|
||||
fertilizeReminder: 'Fertilize Basil — in 3 days',
|
||||
continueLabel: 'Continue',
|
||||
};
|
||||
};
|
||||
|
||||
function ScanFrameOverlay({ resultChip, colors }: { resultChip: string; colors: ColorsType }) {
|
||||
return (
|
||||
<>
|
||||
<View style={styles.scanFrameWrap} pointerEvents="none">
|
||||
<View style={[styles.cornerTL, { borderColor: colors.primary }]} />
|
||||
<View style={[styles.cornerTR, { borderColor: colors.primary }]} />
|
||||
<View style={[styles.cornerBL, { borderColor: colors.primary }]} />
|
||||
<View style={[styles.cornerBR, { borderColor: colors.primary }]} />
|
||||
</View>
|
||||
<View style={styles.resultChip}>
|
||||
<Ionicons name="leaf" size={15} color={colors.primary} />
|
||||
<Text style={styles.resultChipText}>{resultChip}</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HealthCardOverlay({
|
||||
label,
|
||||
overwateringDetected,
|
||||
rescuePlanReady,
|
||||
}: {
|
||||
label: string;
|
||||
overwateringDetected: string;
|
||||
rescuePlanReady: string;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.healthCard}>
|
||||
<View style={styles.healthCardHeader}>
|
||||
<View style={styles.healthCardIcon}>
|
||||
<Ionicons name="medkit" size={16} color="#C62828" />
|
||||
</View>
|
||||
<Text style={styles.healthCardTitle}>{label}</Text>
|
||||
</View>
|
||||
<View style={[styles.healthRow, styles.healthRowWarning]}>
|
||||
<Ionicons name="warning" size={15} color="#C62828" />
|
||||
<Text style={styles.healthRowWarningText}>{overwateringDetected}</Text>
|
||||
</View>
|
||||
<View style={[styles.healthRow, styles.healthRowSuccess]}>
|
||||
<Ionicons name="checkmark-circle" size={15} color="#2e7d32" />
|
||||
<Text style={styles.healthRowSuccessText}>{rescuePlanReady}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ReminderChipsOverlay({ waterReminder, fertilizeReminder }: { waterReminder: string; fertilizeReminder: string }) {
|
||||
const [waterLabel, waterMeta] = splitReminder(waterReminder);
|
||||
const [fertilizeLabel, fertilizeMeta] = splitReminder(fertilizeReminder);
|
||||
return (
|
||||
<>
|
||||
<View style={[styles.reminderChip, styles.reminderChipTop]}>
|
||||
<View style={[styles.reminderIcon, { backgroundColor: '#dff2e6' }]}>
|
||||
<Ionicons name="water" size={16} color="#2e7d32" />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.reminderLabel}>{waterLabel}</Text>
|
||||
<Text style={styles.reminderMeta}>{waterMeta}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.reminderChip, styles.reminderChipBottom]}>
|
||||
<View style={[styles.reminderIcon, { backgroundColor: '#e3f3c8' }]}>
|
||||
<Ionicons name="leaf" size={16} color="#558b2f" />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.reminderLabel}>{fertilizeLabel}</Text>
|
||||
<Text style={styles.reminderMeta}>{fertilizeMeta}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Splits "Water Monstera — today" into ["Water Monstera", "today"] for a two-line chip.
|
||||
function splitReminder(text: string): [string, string] {
|
||||
const parts = text.split('—').map((part) => part.trim());
|
||||
if (parts.length === 2) return [parts[0], parts[1]];
|
||||
return [text, ''];
|
||||
}
|
||||
|
||||
export default function OnboardingSlidesScreen() {
|
||||
const router = useRouter();
|
||||
const { language, isDarkMode, colorPalette } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const posthog = useSafeAnalytics();
|
||||
const [page, setPage] = useState(0);
|
||||
const copy = getSlidesCopy(language);
|
||||
const slide = copy.slides[page];
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('onboarding_slide_viewed', { index: page });
|
||||
}, [page, posthog]);
|
||||
|
||||
const next = () => {
|
||||
if (page < copy.slides.length - 1) {
|
||||
setPage(page + 1);
|
||||
} else {
|
||||
router.replace('/onboarding/source');
|
||||
}
|
||||
};
|
||||
|
||||
const back = () => {
|
||||
if (page > 0) {
|
||||
setPage(page - 1);
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]}>
|
||||
<View style={styles.imageArea}>
|
||||
<Image
|
||||
source={
|
||||
page === 0
|
||||
? require('../../assets/paywall_scan_background.png')
|
||||
: page === 1
|
||||
? require('../../assets/onboarding_health_scan_mockup.png')
|
||||
: require('../../assets/welcome_botanical_header.png')
|
||||
}
|
||||
style={styles.image}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<SafeAreaView style={styles.imageSafeArea} edges={['top']}>
|
||||
<TouchableOpacity onPress={back} style={styles.backBtn} activeOpacity={0.85}>
|
||||
<Ionicons name="arrow-back" size={20} color="#1f2520" />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
{page === 0 && <ScanFrameOverlay resultChip={copy.resultChip} colors={colors} />}
|
||||
{page === 1 && (
|
||||
<HealthCardOverlay
|
||||
label={copy.healthCheckLabel}
|
||||
overwateringDetected={copy.overwateringDetected}
|
||||
rescuePlanReady={copy.rescuePlanReady}
|
||||
/>
|
||||
)}
|
||||
{page === 2 && (
|
||||
<ReminderChipsOverlay waterReminder={copy.waterReminder} fertilizeReminder={copy.fertilizeReminder} />
|
||||
)}
|
||||
</View>
|
||||
<View style={[styles.sheet, { backgroundColor: colors.surface }]}>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{slide.title}</Text>
|
||||
<Text style={[styles.body, { color: colors.textSecondary }]}>{slide.body}</Text>
|
||||
<View style={styles.dots}>
|
||||
{copy.slides.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.dot,
|
||||
index === page
|
||||
? [styles.dotActive, { backgroundColor: colors.primary }]
|
||||
: { backgroundColor: colors.border },
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.cta, { backgroundColor: colors.primary }]}
|
||||
onPress={next}
|
||||
activeOpacity={0.86}
|
||||
>
|
||||
<Text style={[styles.ctaText, { color: colors.onPrimary }]}>{copy.continueLabel}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
imageArea: {
|
||||
height: '58%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
},
|
||||
imageSafeArea: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
backBtn: {
|
||||
marginLeft: 16,
|
||||
marginTop: 8,
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
backgroundColor: 'rgba(255,255,255,0.85)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
// Scan frame overlay (slide 1)
|
||||
scanFrameWrap: {
|
||||
position: 'absolute',
|
||||
top: '22%',
|
||||
left: '20%',
|
||||
right: '20%',
|
||||
bottom: '26%',
|
||||
},
|
||||
cornerTL: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderTopWidth: 4,
|
||||
borderLeftWidth: 4,
|
||||
borderTopLeftRadius: 8,
|
||||
},
|
||||
cornerTR: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderTopWidth: 4,
|
||||
borderRightWidth: 4,
|
||||
borderTopRightRadius: 8,
|
||||
},
|
||||
cornerBL: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderBottomWidth: 4,
|
||||
borderLeftWidth: 4,
|
||||
borderBottomLeftRadius: 8,
|
||||
},
|
||||
cornerBR: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderBottomWidth: 4,
|
||||
borderRightWidth: 4,
|
||||
borderBottomRightRadius: 8,
|
||||
},
|
||||
resultChip: {
|
||||
position: 'absolute',
|
||||
bottom: '10%',
|
||||
left: 20,
|
||||
right: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
backgroundColor: 'rgba(255,255,255,0.94)',
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
resultChipText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
color: '#1f2520',
|
||||
},
|
||||
// Health card overlay (slide 2)
|
||||
healthCard: {
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.96)',
|
||||
borderRadius: 18,
|
||||
padding: 14,
|
||||
gap: 8,
|
||||
},
|
||||
healthCardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
healthCardIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#fdeaea',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
healthCardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
color: '#1f2520',
|
||||
},
|
||||
healthRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
healthRowWarning: {
|
||||
backgroundColor: '#fdeaea',
|
||||
},
|
||||
healthRowWarningText: {
|
||||
fontSize: 13.5,
|
||||
fontWeight: '700',
|
||||
color: '#C62828',
|
||||
},
|
||||
healthRowSuccess: {
|
||||
backgroundColor: '#e8f3e3',
|
||||
},
|
||||
healthRowSuccessText: {
|
||||
fontSize: 13.5,
|
||||
fontWeight: '700',
|
||||
color: '#2e7d32',
|
||||
},
|
||||
// Reminder chips overlay (slide 3)
|
||||
reminderChip: {
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
backgroundColor: 'rgba(255,255,255,0.96)',
|
||||
borderRadius: 999,
|
||||
paddingVertical: 8,
|
||||
paddingRight: 18,
|
||||
paddingLeft: 8,
|
||||
},
|
||||
reminderChipTop: {
|
||||
top: '24%',
|
||||
right: 20,
|
||||
},
|
||||
reminderChipBottom: {
|
||||
top: '42%',
|
||||
left: 20,
|
||||
},
|
||||
reminderIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
reminderLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
color: '#1f2520',
|
||||
},
|
||||
reminderMeta: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#5a8a3d',
|
||||
},
|
||||
// Bottom sheet
|
||||
sheet: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
marginTop: -24,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 30,
|
||||
fontWeight: '900',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
body: {
|
||||
fontSize: 15.5,
|
||||
lineHeight: 22,
|
||||
textAlign: 'center',
|
||||
maxWidth: 320,
|
||||
marginBottom: 20,
|
||||
},
|
||||
dots: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
dotActive: {
|
||||
width: 26,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
cta: {
|
||||
alignSelf: 'stretch',
|
||||
height: 58,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
ctaText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '800',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user