This commit is contained in:
2026-07-27 16:42:04 +02:00
parent 45a7704ea6
commit 018b751723
23 changed files with 2327 additions and 635 deletions

View File

@@ -15,24 +15,35 @@ const getSlidesCopy = (language: Language) => {
return {
slides: [
{
title: 'Scanne jede Pflanze',
body: 'Richte die Kamera auf eine Pflanze und GreenLens erkennt sie in Sekunden.',
// Slide 1 = Triage. Bewusst NICHT die Identifikation: die kann Google
// Lens gratis. Der Pitch startet erst nach der Identifikation.
title: 'Was ist mit meiner Pflanze los?',
body: 'Fotografiere das Symptom. Du bekommst die wahrscheinlichsten Ursachen - und was du prüfen sollst.',
},
{
title: 'Health Check & Pflegeplan',
body: 'GreenLens erkennt Probleme früh und erstellt deinen Rettungsplan.',
title: 'Ein konkreter Plan. Für diese Pflanze.',
body: 'Sofortmaßnahme, 7-Tage-Plan, und danach die Frage: Hat es gewirkt?',
},
{
title: 'Nie mehr Gießen vergessen',
body: 'Smarte Erinnerungen und deine Pflanzen-Bibliothek halten alles im Blick.',
// Der Screen, den keine Konkurrenz-App hat. Beantwortet den Einwand
// "Eine andere App hat mir selbstbewusst etwas Falsches gesagt."
title: 'Wir sagen dir auch, wenn wir unsicher sind.',
body: 'Ein Foto zeigt keine Wurzeln und keine Erdfeuchte. Deshalb Wahrscheinlichkeiten statt erfundener Gewissheit.',
},
],
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',
causeTitle: 'Wahrscheinlichste Ursache',
causePrimary: 'Überwässerung',
causePrimaryLevel: '64 %',
causeSecondary: 'Nährstoffmangel',
causeSecondaryLevel: '41 %',
checkLabel: 'Prüfe zuerst',
checkItems: 'Erde 3 cm tief · Abzugslöcher · Blattunterseiten',
planLabel: '7-Tage-Plan',
planNow: 'Heute: nicht gießen',
planFollowUp: 'Tag 7: Wir fragen nach, ob es besser wird',
uncertainTitle: 'Was ein Foto nicht zeigt',
uncertainItems: ['Wurzeln', 'Erdfeuchte', 'Vorgeschichte'],
uncertainNote: 'Deshalb: Wahrscheinlichkeiten statt Diagnose.',
continueLabel: 'Weiter',
};
}
@@ -40,53 +51,70 @@ const getSlidesCopy = (language: Language) => {
return {
slides: [
{
title: 'Escanea cualquier planta',
body: 'Apunta la cámara a una planta y GreenLens la identifica en segundos.',
title: '¿Qué le pasa a mi planta?',
body: 'Fotografía el síntoma. Recibes las causas más probables - y qué revisar.',
},
{
title: 'Chequeo de salud y plan de cuidados',
body: 'GreenLens detecta problemas a tiempo y crea tu plan de rescate.',
title: 'Un plan concreto. Para esta planta.',
body: 'Acción inmediata, plan de 7 días, y luego la pregunta: ¿funcionó?',
},
{
title: 'No olvides regar nunca más',
body: 'Recordatorios inteligentes y tu biblioteca de plantas lo mantienen todo al día.',
title: 'También te decimos cuándo no estamos seguros.',
body: 'Una foto no muestra las raíces ni la humedad. Por eso probabilidades, no certezas inventadas.',
},
],
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',
causeTitle: 'Causa más probable',
causePrimary: 'Exceso de riego',
causePrimaryLevel: '64 %',
causeSecondary: 'Falta de nutrientes',
causeSecondaryLevel: '41 %',
checkLabel: 'Revisa primero',
checkItems: 'Sustrato a 3 cm · Drenaje · Reverso de las hojas',
planLabel: 'Plan de 7 días',
planNow: 'Hoy: no regar',
planFollowUp: 'Día 7: preguntamos si va mejor',
uncertainTitle: 'Lo que una foto no muestra',
uncertainItems: ['Raíces', 'Humedad', 'Historial'],
uncertainNote: 'Por eso: probabilidades, no diagnóstico.',
continueLabel: 'Continuar',
};
}
return {
slides: [
{
title: 'Scan Any Plant',
body: 'Point your camera at a plant and GreenLens identifies it in seconds.',
title: "What's wrong with my plant?",
body: 'Photograph the symptom. You get the most likely causes - and what to check.',
},
{
title: 'Health Check & Care Plan',
body: 'GreenLens spots problems early and builds a rescue plan for you.',
title: 'A concrete plan. For this plant.',
body: 'Immediate action, a 7-day plan, and then the question: did it work?',
},
{
title: 'Never Forget Watering',
body: 'Smart reminders and your personal plant library keep everything on track.',
title: "We also tell you when we're not sure.",
body: 'A photo cannot show roots or soil moisture. So: probabilities, not invented certainty.',
},
],
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',
causeTitle: 'Most likely cause',
causePrimary: 'Overwatering',
causePrimaryLevel: '64%',
causeSecondary: 'Nutrient deficiency',
causeSecondaryLevel: '41%',
checkLabel: 'Check first',
checkItems: 'Soil 3 cm deep · Drainage holes · Leaf undersides',
planLabel: '7-day plan',
planNow: 'Today: do not water',
planFollowUp: 'Day 7: we ask whether it improved',
uncertainTitle: 'What a photo cannot show',
uncertainItems: ['Roots', 'Soil moisture', 'History'],
uncertainNote: 'So: probabilities, not diagnosis.',
continueLabel: 'Continue',
};
};
function ScanFrameOverlay({ resultChip, colors }: { resultChip: string; colors: ColorsType }) {
type SlidesCopy = ReturnType<typeof getSlidesCopy>;
/** Slide 1: Ursachen-Ranking statt Prozent-Behauptung. */
function CauseRankingOverlay({ copy, colors }: { copy: SlidesCopy; colors: ColorsType }) {
return (
<>
<View style={styles.scanFrameWrap} pointerEvents="none">
@@ -95,77 +123,75 @@ function ScanFrameOverlay({ resultChip, colors }: { resultChip: string; colors:
<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 style={styles.healthCard}>
<Text style={styles.overlayEyebrow}>{copy.causeTitle}</Text>
<View style={[styles.healthRow, styles.healthRowWarning]}>
<Ionicons name="alert-circle" size={15} color="#C62828" />
<Text style={styles.healthRowWarningText}>
{copy.causePrimary} · {copy.causePrimaryLevel}
</Text>
</View>
<View style={[styles.healthRow, styles.healthRowNeutral]}>
<Ionicons name="help-circle-outline" size={15} color="#6b7280" />
<Text style={styles.healthRowNeutralText}>
{copy.causeSecondary} · {copy.causeSecondaryLevel}
</Text>
</View>
{/* In derselben Karte statt als zweites absolutes Element: zwei frei
positionierte Overlays ueberlappen sich, sobald Uebersetzungen die
Karte wachsen lassen. */}
<View style={styles.checkInline}>
<Text style={styles.checkChipLabel}>{copy.checkLabel}</Text>
<Text style={styles.checkChipText}>{copy.checkItems}</Text>
</View>
</View>
</>
);
}
function HealthCardOverlay({
label,
overwateringDetected,
rescuePlanReady,
}: {
label: string;
overwateringDetected: string;
rescuePlanReady: string;
}) {
/** Slide 2: der Plan inklusive Follow-up - das ist der Kaufgrund. */
function PlanOverlay({ copy }: { copy: SlidesCopy }) {
return (
<View style={styles.healthCard}>
<View style={styles.healthCardHeader}>
<View style={styles.healthCardIcon}>
<Ionicons name="medkit" size={16} color="#C62828" />
<Ionicons name="calendar" size={16} color="#2e7d32" />
</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>
<Text style={styles.healthCardTitle}>{copy.planLabel}</Text>
</View>
<View style={[styles.healthRow, styles.healthRowSuccess]}>
<Ionicons name="checkmark-circle" size={15} color="#2e7d32" />
<Text style={styles.healthRowSuccessText}>{rescuePlanReady}</Text>
<Ionicons name="water-outline" size={15} color="#2e7d32" />
<Text style={styles.healthRowSuccessText}>{copy.planNow}</Text>
</View>
<View style={[styles.healthRow, styles.healthRowNeutral]}>
<Ionicons name="chatbubble-ellipses-outline" size={15} color="#6b7280" />
<Text style={styles.healthRowNeutralText}>{copy.planFollowUp}</Text>
</View>
</View>
);
}
function ReminderChipsOverlay({ waterReminder, fertilizeReminder }: { waterReminder: string; fertilizeReminder: string }) {
const [waterLabel, waterMeta] = splitReminder(waterReminder);
const [fertilizeLabel, fertilizeMeta] = splitReminder(fertilizeReminder);
/** Slide 3: die Grenzen des Verfahrens, sichtbar gemacht. */
function UncertaintyOverlay({ copy }: { copy: SlidesCopy }) {
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 style={styles.healthCard}>
<View style={styles.healthCardHeader}>
<View style={styles.healthCardIcon}>
<Ionicons name="eye-off-outline" size={16} color="#6b7280" />
</View>
<Text style={styles.healthCardTitle}>{copy.uncertainTitle}</Text>
</View>
<View style={[styles.reminderChip, styles.reminderChipBottom]}>
<View style={[styles.reminderIcon, { backgroundColor: '#e3f3c8' }]}>
<Ionicons name="leaf" size={16} color="#558b2f" />
{copy.uncertainItems.map((item) => (
<View key={item} style={[styles.healthRow, styles.healthRowNeutral]}>
<Ionicons name="close-circle-outline" size={15} color="#6b7280" />
<Text style={styles.healthRowNeutralText}>{item}</Text>
</View>
<View>
<Text style={styles.reminderLabel}>{fertilizeLabel}</Text>
<Text style={styles.reminderMeta}>{fertilizeMeta}</Text>
</View>
</View>
</>
))}
<Text style={styles.overlayNote}>{copy.uncertainNote}</Text>
</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 { height } = useWindowDimensions();
@@ -178,7 +204,7 @@ export default function OnboardingSlidesScreen() {
const slide = copy.slides[page];
useEffect(() => {
posthog.capture('onboarding_slide_viewed', { index: page });
posthog.capture('onboarding_slide_viewed', { index: page, slide: ['triage', 'plan', 'uncertainty'][page] });
}, [page, posthog]);
const next = () => {
@@ -203,9 +229,9 @@ export default function OnboardingSlidesScreen() {
<Image
source={
page === 0
? require('../../assets/paywall_scan_background.png')
? require('../../assets/onboarding_health_scan_mockup_vertical.png')
: page === 1
? require('../../assets/onboarding_health_scan_mockup_vertical.png')
? require('../../assets/paywall_scan_background.png')
: require('../../assets/welcome_botanical_header.png')
}
style={styles.image}
@@ -216,17 +242,9 @@ export default function OnboardingSlidesScreen() {
<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} />
)}
{page === 0 && <CauseRankingOverlay copy={copy} colors={colors} />}
{page === 1 && <PlanOverlay copy={copy} />}
{page === 2 && <UncertaintyOverlay copy={copy} />}
</View>
<View style={[styles.sheet, { backgroundColor: colors.surface }]}>
<Text style={[styles.title, { color: colors.text }]}>{slide.title}</Text>
@@ -334,24 +352,6 @@ const styles = StyleSheet.create({
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',
@@ -406,43 +406,54 @@ const styles = StyleSheet.create({
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,
// Neutrale Zeile: bewusst grau, damit "weniger wahrscheinlich" und
// "was wir nicht sehen" optisch NICHT wie eine Zusage aussehen.
healthRowNeutral: {
backgroundColor: '#f1f2f0',
},
reminderChipTop: {
top: '24%',
right: 20,
healthRowNeutralText: {
fontSize: 13.5,
fontWeight: '600',
color: '#5b6158',
},
reminderChipBottom: {
top: '42%',
left: 20,
overlayEyebrow: {
fontSize: 10.5,
fontWeight: '900',
letterSpacing: 0.9,
textTransform: 'uppercase',
color: '#6b7280',
marginBottom: 8,
},
reminderIcon: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
overlayNote: {
fontSize: 11.5,
lineHeight: 15,
fontWeight: '600',
color: '#6b7280',
marginTop: 8,
},
reminderLabel: {
fontSize: 14,
fontWeight: '800',
checkInline: {
borderTopWidth: 1,
borderTopColor: 'rgba(0,0,0,0.08)',
paddingTop: 9,
marginTop: 2,
},
// Ein Stack statt zwei absoluter Elemente: die Bloecke koennen sich nicht
// mehr ueberlappen, egal wie lang der uebersetzte Text wird.
checkChipLabel: {
fontSize: 10,
fontWeight: '900',
letterSpacing: 0.8,
textTransform: 'uppercase',
color: '#6b7280',
marginBottom: 3,
},
checkChipText: {
fontSize: 12.5,
lineHeight: 17,
fontWeight: '700',
color: '#1f2520',
},
reminderMeta: {
fontSize: 12,
fontWeight: '600',
color: '#5a8a3d',
},
// Reminder chips overlay (slide 3)
// Bottom sheet
sheet: {
flex: 1,