App copy
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { ScrollView, 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';
|
||||
@@ -16,57 +16,84 @@ const ONBOARDING_BACKGROUND = {
|
||||
const getHealthOnboardingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
title: 'Wo ist der Health-Scan?',
|
||||
subtitle: 'Du findest ihn auf jeder gespeicherten Pflanze, direkt unter der Beschreibung.',
|
||||
buttonPreview: 'Health-Scan starten',
|
||||
cta: 'Weiter',
|
||||
skip: 'Spaeter',
|
||||
flow: ['Pflanze scannen', 'Speichern', 'Detailseite oeffnen', 'Health-Scan starten'],
|
||||
outputTitle: 'Was du danach bekommst',
|
||||
outputs: [
|
||||
'Gesundheits-Score mit Status: stabil, beobachten oder kritisch.',
|
||||
'Ausfuehrliche Analyse mit sichtbaren Hinweisen und Unsicherheit.',
|
||||
'Wahrscheinlichste Ursachen mit Confidence-Werten.',
|
||||
'Sofortmassnahmen plus konkreter 7-Tage-Pflegeplan.',
|
||||
title: 'So sieht eine Antwort aus',
|
||||
subtitle: 'Ein echtes Beispiel - damit du weißt, was du bekommst, bevor du etwas entscheidest.',
|
||||
exampleSymptomLabel: 'Symptom',
|
||||
exampleSymptom: 'Untere Blätter gelb, Erde feucht',
|
||||
causeLabel: 'Wahrscheinlichste Ursache',
|
||||
// Bewusst ein Fall MITTLERER Sicherheit, kein Vorzeige-Treffer - das ist
|
||||
// der Beweis fuer Belief 3. Die Werte summieren sich absichtlich NICHT auf
|
||||
// 100 %: der Backend-Prompt (server/lib/openai.js, likelyIssues) liefert je
|
||||
// Ursache eine unabhaengige Sicherheit, keine Wahrscheinlichkeitsverteilung.
|
||||
// Ein Beispiel, das sich auf 100 addiert, wuerde ein Verhalten zeigen, das
|
||||
// das Produkt nicht hat.
|
||||
causes: [
|
||||
{ name: 'Überwässerung', level: '64 %', tone: 'high' as const },
|
||||
{ name: 'Nährstoffmangel', level: '41 %', tone: 'mid' as const },
|
||||
{ name: 'Lichtmangel', level: '22 %', tone: 'low' as const },
|
||||
],
|
||||
guidanceNote: 'Tipp: Fotografiere die ganze Pflanze, die Blattunterseiten und die Erde. Je klarer das Foto, desto genauer wird der Plan.',
|
||||
// Schwelle 65 % entspricht der Kalibrierung im Backend-Prompt:
|
||||
// 0.65-0.84 "sehr wahrscheinlich", 0.40-0.64 "mehrdeutig".
|
||||
confidenceNote: 'Unter 65 % sagen wir dir das deutlich - dann sind die Prüfschritte wichtiger als die Ursache.',
|
||||
checkLabel: 'Prüfe zuerst',
|
||||
checks: ['Erde 3 cm tief anfassen', 'Hat der Topf Abzugslöcher?', 'Blattunterseiten ansehen'],
|
||||
actionLabel: 'Tu jetzt',
|
||||
action: 'Nicht gießen. In 5 Tagen erneut prüfen.',
|
||||
followUpLabel: 'In 7 Tagen',
|
||||
followUp: 'Wir fragen nach, ob es besser wird - und passen den Plan an, wenn nicht.',
|
||||
limitNote: 'Ein Foto zeigt keine Wurzeln, keine Erdfeuchte und keine Vorgeschichte. Deshalb bekommst du Wahrscheinlichkeiten und Prüfschritte - keine Diagnose.',
|
||||
guidanceNote: 'Tipp: Fotografiere die ganze Pflanze, die Blattunterseiten und die Erde. Je klarer das Foto, desto präziser der Plan.',
|
||||
cta: 'Weiter',
|
||||
skip: 'Später',
|
||||
};
|
||||
}
|
||||
|
||||
if (language === 'es') {
|
||||
return {
|
||||
title: 'Donde esta el health-scan?',
|
||||
subtitle: 'Lo encuentras en cada planta guardada, justo debajo de la descripcion.',
|
||||
buttonPreview: 'Iniciar health-scan',
|
||||
cta: 'Continuar',
|
||||
skip: 'Mas tarde',
|
||||
flow: ['Escanear planta', 'Guardar', 'Abrir detalle', 'Iniciar health-scan'],
|
||||
outputTitle: 'Que recibes despues',
|
||||
outputs: [
|
||||
'Puntaje de salud con estado: estable, observar o critico.',
|
||||
'Analisis detallado con senales visibles e incertidumbre.',
|
||||
'Causas probables con valores de confianza.',
|
||||
'Acciones inmediatas y plan concreto de 7 dias.',
|
||||
title: 'Así se ve una respuesta',
|
||||
subtitle: 'Un ejemplo real - para que sepas qué recibes antes de decidir nada.',
|
||||
exampleSymptomLabel: 'Síntoma',
|
||||
exampleSymptom: 'Hojas inferiores amarillas, sustrato húmedo',
|
||||
causeLabel: 'Causa más probable',
|
||||
causes: [
|
||||
{ name: 'Exceso de riego', level: '64 %', tone: 'high' as const },
|
||||
{ name: 'Falta de nutrientes', level: '41 %', tone: 'mid' as const },
|
||||
{ name: 'Falta de luz', level: '22 %', tone: 'low' as const },
|
||||
],
|
||||
guidanceNote: 'Consejo: fotografia la planta completa, el reverso de las hojas y el sustrato. Cuanto mas clara sea la foto, mas preciso sera el plan.',
|
||||
confidenceNote: 'Por debajo del 65 % te lo decimos claramente - entonces los pasos de revisión importan más que la causa.',
|
||||
checkLabel: 'Revisa primero',
|
||||
checks: ['Tocar el sustrato a 3 cm', '¿La maceta tiene drenaje?', 'Mirar el reverso de las hojas'],
|
||||
actionLabel: 'Haz ahora',
|
||||
action: 'No regar. Volver a revisar en 5 días.',
|
||||
followUpLabel: 'En 7 días',
|
||||
followUp: 'Preguntamos si va mejor - y ajustamos el plan si no.',
|
||||
limitNote: 'Una foto no muestra las raíces, la humedad del sustrato ni el historial. Por eso recibes probabilidades y pasos de revisión - no un diagnóstico.',
|
||||
guidanceNote: 'Consejo: fotografía la planta completa, el reverso de las hojas y el sustrato. Cuanto más clara sea la foto, más preciso será el plan.',
|
||||
cta: 'Continuar',
|
||||
skip: 'Más tarde',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Where is the health scan?',
|
||||
subtitle: 'It lives on every saved plant, directly below the plant description.',
|
||||
buttonPreview: 'Start health scan',
|
||||
title: 'This is what an answer looks like',
|
||||
subtitle: 'A real example - so you know what you get before you decide anything.',
|
||||
exampleSymptomLabel: 'Symptom',
|
||||
exampleSymptom: 'Lower leaves yellow, soil damp',
|
||||
causeLabel: 'Most likely cause',
|
||||
causes: [
|
||||
{ name: 'Overwatering', level: '64%', tone: 'high' as const },
|
||||
{ name: 'Nutrient deficiency', level: '41%', tone: 'mid' as const },
|
||||
{ name: 'Too little light', level: '22%', tone: 'low' as const },
|
||||
],
|
||||
confidenceNote: 'Below 65% we say so plainly - then the checks matter more than the cause.',
|
||||
checkLabel: 'Check first',
|
||||
checks: ['Feel the soil 3 cm down', 'Does the pot have drainage holes?', 'Look at the leaf undersides'],
|
||||
actionLabel: 'Do now',
|
||||
action: 'Do not water. Check again in 5 days.',
|
||||
followUpLabel: 'In 7 days',
|
||||
followUp: 'We ask whether it is improving - and adjust the plan if it is not.',
|
||||
limitNote: 'A photo cannot show roots, soil moisture or history. So you get probabilities and things to check - not a diagnosis.',
|
||||
guidanceNote: 'Tip: photograph the full plant, leaf undersides, and the soil. The clearer the photo, the more precise the plan.',
|
||||
cta: 'Continue',
|
||||
skip: 'Later',
|
||||
flow: ['Scan plant', 'Save', 'Open detail', 'Start health scan'],
|
||||
outputTitle: 'What you get after',
|
||||
outputs: [
|
||||
'Health score with stable, watch, or critical status.',
|
||||
'Detailed analysis with visible signals and uncertainty.',
|
||||
'Most likely causes with confidence values.',
|
||||
'Immediate actions plus a concrete 7-day care plan.',
|
||||
],
|
||||
guidanceNote: 'Tip: photograph the full plant, leaf undersides, and the soil. The clearer the photo, the more precise the plan.',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -105,36 +132,73 @@ export default function HealthCheckOnboardingScreen() {
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<ImageBackground
|
||||
source={require('../../assets/onboarding_health_scan_mockup.png')}
|
||||
style={[styles.illustration, { borderColor: colors.border, backgroundColor: isDarkMode ? '#0a110b' : '#fbfaf3' }]}
|
||||
imageStyle={styles.illustrationImage}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<View style={[styles.illustrationOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.08)' : 'rgba(251, 250, 243, 0.04)' }]} />
|
||||
</ImageBackground>
|
||||
{/* Worked Example statt Wegbeschreibung: ein echtes Ergebnis schlaegt
|
||||
jede Behauptung ueber die Qualitaet der Ergebnisse. */}
|
||||
<View style={[styles.exampleCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.exampleSymptomLabel}</Text>
|
||||
<Text style={[styles.exampleSymptom, { color: colors.text }]}>{copy.exampleSymptom}</Text>
|
||||
|
||||
<View style={[styles.flowCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
{copy.flow.map((item, index) => (
|
||||
<View key={item} style={styles.flowRow}>
|
||||
<View style={[styles.flowIndex, { backgroundColor: index === 3 ? colors.primary : colors.surfaceMuted }]}>
|
||||
<Text style={[styles.flowIndexText, { color: index === 3 ? colors.onPrimary : colors.textMuted }]}>
|
||||
{index + 1}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.flowText, { color: colors.text }]}>{item}</Text>
|
||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||
|
||||
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.causeLabel}</Text>
|
||||
{copy.causes.map((cause) => (
|
||||
<View key={cause.name} style={styles.causeRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.causeDot,
|
||||
{
|
||||
backgroundColor:
|
||||
cause.tone === 'high' ? colors.danger : cause.tone === 'mid' ? colors.warning : colors.textMuted,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.causeName,
|
||||
{ color: cause.tone === 'high' ? colors.text : colors.textSecondary },
|
||||
cause.tone === 'high' && styles.causeNamePrimary,
|
||||
]}
|
||||
>
|
||||
{cause.name}
|
||||
</Text>
|
||||
<Text style={[styles.causeLevel, { color: colors.textMuted }]}>{cause.level}</Text>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={[styles.confidenceNote, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons name="alert-circle-outline" size={14} color={colors.textMuted} />
|
||||
<Text style={[styles.confidenceNoteText, { color: colors.textSecondary }]}>{copy.confidenceNote}</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||
|
||||
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.checkLabel}</Text>
|
||||
{copy.checks.map((check) => (
|
||||
<View key={check} style={styles.outputRow}>
|
||||
<Ionicons name="ellipse-outline" size={15} color={colors.primary} />
|
||||
<Text style={[styles.outputText, { color: colors.textSecondary }]}>{check}</Text>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={[styles.actionBox, { backgroundColor: colors.primarySoft }]}>
|
||||
<Text style={[styles.actionLabel, { color: colors.primaryDark }]}>{copy.actionLabel}</Text>
|
||||
<Text style={[styles.actionText, { color: colors.primaryDark }]}>{copy.action}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.followUpRow}>
|
||||
<Ionicons name="chatbubble-ellipses-outline" size={15} color={colors.textMuted} />
|
||||
<View style={styles.followUpCopy}>
|
||||
<Text style={[styles.followUpLabel, { color: colors.text }]}>{copy.followUpLabel}</Text>
|
||||
<Text style={[styles.followUpText, { color: colors.textSecondary }]}>{copy.followUp}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.outputCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.outputTitle, { color: colors.text }]}>{copy.outputTitle}</Text>
|
||||
{copy.outputs.map((item) => (
|
||||
<View key={item} style={styles.outputRow}>
|
||||
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
|
||||
<Text style={[styles.outputText, { color: colors.textSecondary }]}>{item}</Text>
|
||||
</View>
|
||||
))}
|
||||
{/* Die Grenze des Verfahrens steht bewusst direkt unter dem Beispiel,
|
||||
nicht im Kleingedruckten. Sie ist Teil des Verkaufsarguments. */}
|
||||
<View style={[styles.limitCard, { backgroundColor: colors.surfaceMuted, borderColor: colors.border }]}>
|
||||
<Ionicons name="information-circle-outline" size={17} color={colors.textMuted} />
|
||||
<Text style={[styles.limitText, { color: colors.textSecondary }]}>{copy.limitNote}</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.guidanceCard, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
|
||||
@@ -170,30 +234,36 @@ const styles = StyleSheet.create({
|
||||
title: { fontSize: 30, lineHeight: 34, fontWeight: '900' },
|
||||
subtitle: { fontSize: 14, lineHeight: 20 },
|
||||
content: { gap: 14, paddingBottom: 12 },
|
||||
illustration: { height: 230, borderRadius: 28, borderWidth: 1, justifyContent: 'center', overflow: 'hidden' },
|
||||
illustrationImage: { borderRadius: 28 },
|
||||
illustrationOverlay: { ...StyleSheet.absoluteFillObject },
|
||||
phone: { width: 178, minHeight: 156, borderRadius: 26, borderWidth: 1, padding: 12, gap: 10, marginLeft: 16 },
|
||||
phoneHeader: { height: 58, borderRadius: 18, justifyContent: 'flex-end', padding: 10 },
|
||||
phoneTitle: { fontSize: 13, fontWeight: '800' },
|
||||
phoneRows: { gap: 8 },
|
||||
phoneRowLong: { height: 8, borderRadius: 999 },
|
||||
phoneRowShort: { width: '66%', height: 8, borderRadius: 999 },
|
||||
healthButtonPreview: { height: 34, borderRadius: 14, borderWidth: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 5 },
|
||||
healthButtonText: { fontSize: 10, fontWeight: '800' },
|
||||
scanCard: { position: 'absolute', right: 16, bottom: 20, width: 136, borderRadius: 20, borderWidth: 1, padding: 14, gap: 7 },
|
||||
scanScore: { fontSize: 25, lineHeight: 29, fontWeight: '900' },
|
||||
scanLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase' },
|
||||
scanLine: { height: 8, borderRadius: 999 },
|
||||
scanLineShort: { width: '68%', height: 8, borderRadius: 999 },
|
||||
flowCard: { borderRadius: 18, borderWidth: 1, padding: 14, gap: 10 },
|
||||
flowRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
flowIndex: { width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
|
||||
flowIndexText: { fontSize: 12, fontWeight: '900' },
|
||||
flowText: { flex: 1, fontSize: 14, fontWeight: '700' },
|
||||
outputCard: { borderRadius: 18, borderWidth: 1, padding: 16, gap: 11 },
|
||||
outputTitle: { fontSize: 15, fontWeight: '800' },
|
||||
outputRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9 },
|
||||
outputRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9, marginBottom: 6 },
|
||||
exampleCard: { borderRadius: 20, borderWidth: 1, padding: 18, gap: 4 },
|
||||
exampleEyebrow: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase', marginBottom: 6 },
|
||||
exampleSymptom: { fontSize: 16, fontWeight: '700', lineHeight: 22 },
|
||||
divider: { height: 1, marginVertical: 14 },
|
||||
causeRow: { flexDirection: 'row', alignItems: 'center', gap: 9, marginBottom: 8 },
|
||||
causeDot: { width: 8, height: 8, borderRadius: 4 },
|
||||
causeName: { flex: 1, fontSize: 14.5, fontWeight: '600' },
|
||||
causeNamePrimary: { fontWeight: '800' },
|
||||
causeLevel: { fontSize: 12.5, fontWeight: '800', fontVariant: ['tabular-nums'] },
|
||||
confidenceNote: { flexDirection: 'row', alignItems: 'flex-start', gap: 7, borderRadius: 10, padding: 10, marginTop: 4 },
|
||||
confidenceNoteText: { flex: 1, fontSize: 12, lineHeight: 16.5, fontWeight: '500' },
|
||||
actionBox: { borderRadius: 14, padding: 13, marginTop: 12, gap: 3 },
|
||||
actionLabel: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase' },
|
||||
actionText: { fontSize: 14.5, fontWeight: '700', lineHeight: 20 },
|
||||
followUpRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9, marginTop: 14 },
|
||||
followUpCopy: { flex: 1, gap: 2 },
|
||||
followUpLabel: { fontSize: 13, fontWeight: '800' },
|
||||
followUpText: { fontSize: 13, lineHeight: 18, fontWeight: '500' },
|
||||
limitCard: { borderRadius: 16, borderWidth: 1, padding: 14, flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
|
||||
limitText: { flex: 1, fontSize: 12.5, lineHeight: 18, fontWeight: '500' },
|
||||
outputText: { flex: 1, fontSize: 13, lineHeight: 18 },
|
||||
guidanceCard: { borderRadius: 18, borderWidth: 1, padding: 14, flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
|
||||
guidanceText: { flex: 1, fontSize: 12, lineHeight: 18, fontWeight: '600' },
|
||||
|
||||
@@ -7,33 +7,116 @@ import Svg, { Circle } from 'react-native-svg';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { PreAuthOnboardingService, PreAuthAnswers } from '../../services/preAuthOnboardingService';
|
||||
import { Language } from '../../types';
|
||||
|
||||
const getCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
status: 'Dein Pflegeplan wird personalisiert…',
|
||||
steps: ['Antworten werden analysiert', 'Pflegeplan wird erstellt', 'Scan-Credits werden vorbereitet', 'Plan wird finalisiert'],
|
||||
testimonial: '„GreenLens hat meine Geigenfeige gerettet. Die täglichen Routinen sind unglaublich präzise."',
|
||||
author: 'Elena R.',
|
||||
rating: '4,8 APP-STORE-BEWERTUNG',
|
||||
status: 'Dein Plan wird zusammengestellt…',
|
||||
// Schritte beschreiben den Nutzen fuer den Nutzer, nicht unsere Prozesse.
|
||||
steps: [
|
||||
'Deine Antworten werden ausgewertet',
|
||||
'Mögliche Ursachen für dein Symptom werden gewichtet',
|
||||
'Prüfschritte für deine Lichtsituation werden ausgewählt',
|
||||
'Dein 7-Tage-Plan steht',
|
||||
],
|
||||
summaryTitle: 'Dein Plan basiert auf',
|
||||
noAnswers: 'Deinen Angaben aus dem Gespräch',
|
||||
situations: {
|
||||
acute: 'Akutes Problem',
|
||||
slow: 'Langsame Veränderung',
|
||||
prevention: 'Vorbeugung',
|
||||
} as Record<string, string>,
|
||||
symptoms: {
|
||||
yellow: 'Gelbe Blätter',
|
||||
brown_tips: 'Braune Spitzen',
|
||||
drooping: 'Hängende Blätter',
|
||||
spots: 'Flecken oder Belag',
|
||||
pests: 'Schädlinge',
|
||||
other: 'Anderes Symptom',
|
||||
} as Record<string, string>,
|
||||
lights: {
|
||||
bright_indirect: 'Helles, indirektes Licht',
|
||||
low_light: 'Wenig Licht',
|
||||
direct_sunlight: 'Direkte Sonne',
|
||||
} as Record<string, string>,
|
||||
experiences: {
|
||||
beginner: 'Einsteigerin',
|
||||
intermediate: 'Etwas Erfahrung',
|
||||
advanced: 'Erfahren',
|
||||
} as Record<string, string>,
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
status: 'Personalizando tu plan de cuidados…',
|
||||
steps: ['Analizando tus respuestas', 'Creando tu plan de cuidados', 'Preparando tus créditos de escaneo', 'Finalizando tu plan'],
|
||||
testimonial: '"GreenLens salvó mi ficus lyrata. Las rutinas diarias son increíblemente precisas."',
|
||||
author: 'Elena R.',
|
||||
rating: '4.8 VALORACIÓN EN APP STORE',
|
||||
status: 'Preparando tu plan…',
|
||||
steps: [
|
||||
'Analizando tus respuestas',
|
||||
'Ponderando las causas posibles de tu síntoma',
|
||||
'Eligiendo pasos de revisión para tu luz',
|
||||
'Tu plan de 7 días está listo',
|
||||
],
|
||||
summaryTitle: 'Tu plan se basa en',
|
||||
noAnswers: 'Lo que nos contaste en el chat',
|
||||
situations: {
|
||||
acute: 'Problema agudo',
|
||||
slow: 'Cambio lento',
|
||||
prevention: 'Prevención',
|
||||
} as Record<string, string>,
|
||||
symptoms: {
|
||||
yellow: 'Hojas amarillas',
|
||||
brown_tips: 'Puntas marrones',
|
||||
drooping: 'Hojas caídas',
|
||||
spots: 'Manchas o residuo',
|
||||
pests: 'Plagas',
|
||||
other: 'Otro síntoma',
|
||||
} as Record<string, string>,
|
||||
lights: {
|
||||
bright_indirect: 'Luz brillante indirecta',
|
||||
low_light: 'Poca luz',
|
||||
direct_sunlight: 'Sol directo',
|
||||
} as Record<string, string>,
|
||||
experiences: {
|
||||
beginner: 'Principiante',
|
||||
intermediate: 'Algo de experiencia',
|
||||
advanced: 'Con experiencia',
|
||||
} as Record<string, string>,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'Personalizing your care plan…',
|
||||
steps: ['Analyzing your answers', 'Building your care plan', 'Preparing your scan credits', 'Finalizing your plan'],
|
||||
testimonial: '"GreenLens completely saved my Fiddle Leaf Fig. The daily routines feel incredibly precise."',
|
||||
author: 'Elena R.',
|
||||
rating: '4.8 APP STORE RATING',
|
||||
status: 'Putting your plan together…',
|
||||
steps: [
|
||||
'Reviewing your answers',
|
||||
'Weighing possible causes for your symptom',
|
||||
'Picking checks for your light situation',
|
||||
'Your 7-day plan is ready',
|
||||
],
|
||||
summaryTitle: 'Your plan is based on',
|
||||
noAnswers: 'What you told us in the chat',
|
||||
situations: {
|
||||
acute: 'Acute problem',
|
||||
slow: 'Slow change',
|
||||
prevention: 'Prevention',
|
||||
} as Record<string, string>,
|
||||
symptoms: {
|
||||
yellow: 'Yellow leaves',
|
||||
brown_tips: 'Brown tips',
|
||||
drooping: 'Drooping leaves',
|
||||
spots: 'Spots or residue',
|
||||
pests: 'Pests',
|
||||
other: 'Other symptom',
|
||||
} as Record<string, string>,
|
||||
lights: {
|
||||
bright_indirect: 'Bright, indirect light',
|
||||
low_light: 'Low light',
|
||||
direct_sunlight: 'Direct sun',
|
||||
} as Record<string, string>,
|
||||
experiences: {
|
||||
beginner: 'Beginner',
|
||||
intermediate: 'Some experience',
|
||||
advanced: 'Experienced',
|
||||
} as Record<string, string>,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,8 +135,22 @@ export default function OnboardingPersonalizingScreen() {
|
||||
const copy = getCopy(language);
|
||||
const progress = useRef(new Animated.Value(0)).current;
|
||||
const [percent, setPercent] = useState(0);
|
||||
const [answers, setAnswers] = useState<PreAuthAnswers>({});
|
||||
const navigated = useRef(false);
|
||||
|
||||
// Die eigenen Angaben zurueckspiegeln beweist "es geht um MEINE Pflanze"
|
||||
// (Belief 4) durch Demonstration statt durch Behauptung.
|
||||
useEffect(() => {
|
||||
void PreAuthOnboardingService.getAnswers().then(setAnswers);
|
||||
}, []);
|
||||
|
||||
const summaryChips = [
|
||||
answers.situation ? copy.situations[answers.situation] : null,
|
||||
answers.symptom ? copy.symptoms[answers.symptom] : null,
|
||||
answers.lightLevel ? copy.lights[answers.lightLevel] : null,
|
||||
answers.experienceLevel ? copy.experiences[answers.experienceLevel] : null,
|
||||
].filter((chip): chip is string => Boolean(chip));
|
||||
|
||||
const strokeDashoffset = progress.interpolate({
|
||||
inputRange: [0, 100],
|
||||
outputRange: [RING_CIRCUMFERENCE, 0],
|
||||
@@ -129,17 +226,18 @@ export default function OnboardingPersonalizingScreen() {
|
||||
})}
|
||||
</View>
|
||||
<View style={[styles.testimonialCard, { backgroundColor: colors.surface }]}>
|
||||
<View style={styles.testimonialHeader}>
|
||||
<Text style={[styles.testimonialAuthor, { color: colors.text }]}>{copy.author}</Text>
|
||||
<View style={styles.starsRow}>
|
||||
{[0, 1, 2, 3, 4].map((i) => <Ionicons key={i} name="star" size={13} color="#f5c04e" />)}
|
||||
</View>
|
||||
<Text style={[styles.summaryTitle, { color: colors.textMuted }]}>{copy.summaryTitle}</Text>
|
||||
<View style={styles.summaryChips}>
|
||||
{summaryChips.length > 0 ? (
|
||||
summaryChips.map((chip) => (
|
||||
<View key={chip} style={[styles.summaryChip, { backgroundColor: colors.primarySoft }]}>
|
||||
<Text style={[styles.summaryChipText, { color: colors.primaryDark }]}>{chip}</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text style={[styles.testimonialText, { color: colors.textSecondary }]}>{copy.noAnswers}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.testimonialText, { color: colors.textSecondary }]}>{copy.testimonial}</Text>
|
||||
</View>
|
||||
<View style={[styles.ratingBadge, { borderColor: colors.primary }]}>
|
||||
<Ionicons name="ribbon-outline" size={16} color={colors.primary} />
|
||||
<Text style={[styles.ratingText, { color: colors.primary }]}>{copy.rating}</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -156,10 +254,9 @@ const styles = StyleSheet.create({
|
||||
checkRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||||
checkLabel: { fontSize: 16.5, fontWeight: '700' },
|
||||
testimonialCard: { alignSelf: 'stretch', borderRadius: 18, padding: 16, marginBottom: 14 },
|
||||
testimonialHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 },
|
||||
testimonialAuthor: { fontSize: 14.5, fontWeight: '800' },
|
||||
starsRow: { flexDirection: 'row', gap: 2 },
|
||||
testimonialText: { fontSize: 14, lineHeight: 20, fontStyle: 'italic' },
|
||||
ratingBadge: { flexDirection: 'row', alignItems: 'center', gap: 7, borderWidth: 1.5, borderRadius: 999, paddingHorizontal: 16, paddingVertical: 9 },
|
||||
ratingText: { fontSize: 12.5, fontWeight: '900', letterSpacing: 0.6 },
|
||||
testimonialText: { fontSize: 14, lineHeight: 20 },
|
||||
summaryTitle: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase', marginBottom: 10 },
|
||||
summaryChips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||
summaryChip: { borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
|
||||
summaryChipText: { fontSize: 12.5, fontWeight: '700' },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -37,35 +37,56 @@ interface Message {
|
||||
const getChatCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
title: 'Monstera',
|
||||
title: 'GreenLens',
|
||||
skip: 'Überspringen',
|
||||
continue: 'Weiter',
|
||||
today: 'Heute, 20:42 Uhr',
|
||||
typing: 'schreibt...',
|
||||
botGreeting: "Hi! Ich bin deine neue Monstera. 🌿 Ich freue mich riesig darauf, mit dir zu wachsen!",
|
||||
botSource: "Als erstes: Wie hast du von GreenLens gehört?",
|
||||
botGoal: "Verstanden! Und was ist dein Hauptziel mit deinen Pflanzen?",
|
||||
botExperience: "Schön! Wie erfahren bist du in der Pflanzenpflege?",
|
||||
botLight: "Damit ich glücklich bleibe und diese großen, schönen Blätter wachsen, muss ich ein bisschen über mein neues Zuhause wissen. Wie viel Licht bekomme ich hier?",
|
||||
botComplete: "Perfekt! Ich bin bereit einzuziehen. Lass uns deinen Pflegeplan erstellen!",
|
||||
// Reihenfolge folgt dem Nutzer, nicht dem Unternehmen: erst sein Problem,
|
||||
// dann Kontext, zuletzt die Marketing-Frage.
|
||||
botGreeting: 'Hi! Ich helfe dir herauszufinden, was mit deinen Pflanzen los ist. 🌿',
|
||||
botSituation: 'Damit ich weiß, wo wir starten: Hat gerade eine deiner Pflanzen ein Problem?',
|
||||
botSymptom: 'Verstanden. Was siehst du genau? Das grenzt die möglichen Ursachen am stärksten ein.',
|
||||
botNoSymptom: 'Gut zu wissen - dann richten wir alles auf Vorbeugung aus.',
|
||||
botLight: 'Licht entscheidet bei den meisten Problemen mit. Wie viel Licht bekommt deine Pflanze?',
|
||||
botExperience: 'Fast geschafft. Wie sicher fühlst du dich bei Pflanzenpflege? Danach passe ich an, wie ausführlich ich erkläre.',
|
||||
botSource: 'Letzte Frage, rein aus Neugier: Wie hast du von GreenLens gehört?',
|
||||
botComplete: 'Perfekt. Ich stelle dir jetzt deinen Plan zusammen.',
|
||||
stepSituation: 'Hat eine Pflanze gerade ein Problem?',
|
||||
stepSymptom: 'Was siehst du?',
|
||||
stepLight: 'Wie viel Licht bekommt sie?',
|
||||
stepExperience: 'Wie sicher fühlst du dich?',
|
||||
stepSource: 'Wie hast du von uns gehört?',
|
||||
situations: {
|
||||
acute: 'Ja - und es wird schnell schlechter',
|
||||
slow: 'Ja - es verändert sich langsam',
|
||||
prevention: 'Nein - ich will vorbeugen',
|
||||
},
|
||||
symptoms: {
|
||||
yellow: 'Gelbe Blätter',
|
||||
brown_tips: 'Braune Spitzen oder Ränder',
|
||||
drooping: 'Hängt oder lässt die Blätter fallen',
|
||||
spots: 'Flecken oder Belag',
|
||||
pests: 'Krabbeltiere',
|
||||
other: 'Etwas anderes',
|
||||
},
|
||||
sources: {
|
||||
app_store: 'App Store / Play Store',
|
||||
instagram: 'Instagram',
|
||||
tiktok: 'TikTok',
|
||||
friend: 'Freunde oder Familie',
|
||||
search: 'Google oder Suche',
|
||||
// Bewusst unwahrscheinliche Optionen: sie brechen das Muster der
|
||||
// Standard-Attributionsfrage und machen den letzten Schritt leichter.
|
||||
tv: 'Im Fernsehen (haben wir nicht - aber schön wärs)',
|
||||
radio: 'Im Radio zwischen zwei Songs',
|
||||
plant: 'Meine Pflanze hat es mir empfohlen',
|
||||
other: 'Etwas anderes',
|
||||
},
|
||||
goals: {
|
||||
identify: 'Pflanzen erkennen',
|
||||
care: 'Pflegepläne',
|
||||
collection: 'Sammlung vergrößern',
|
||||
learn: 'Über Pflanzen lernen',
|
||||
},
|
||||
experiences: {
|
||||
beginner: 'Anfänger (Ich bin neu)',
|
||||
intermediate: 'Fortgeschritten (Ich habe Pflanzen)',
|
||||
advanced: 'Experte (Ich weiß Bescheid)',
|
||||
beginner: 'Unsicher - ich fange gerade an',
|
||||
intermediate: 'Geht so - ich habe ein paar Pflanzen',
|
||||
advanced: 'Sicher - ich kenne mich aus',
|
||||
},
|
||||
lights: {
|
||||
bright_indirect: {
|
||||
@@ -85,35 +106,52 @@ const getChatCopy = (language: Language) => {
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
title: 'Monstera',
|
||||
title: 'GreenLens',
|
||||
skip: 'Omitir',
|
||||
continue: 'Continuar',
|
||||
today: 'Hoy, 8:42 PM',
|
||||
typing: 'escribiendo...',
|
||||
botGreeting: "¡Hola! Soy tu nueva Monstera. 🌿 ¡Estoy muy emocionada de crecer contigo!",
|
||||
botSource: "Primero, ¿cómo te enteraste de GreenLens?",
|
||||
botGoal: "¡Entendido! ¿Y cuál es tu objetivo principal con tus plantas?",
|
||||
botExperience: "¡Genial! ¿Qué experiencia tienes en el cuidado de plantas?",
|
||||
botLight: "Para estar feliz y que crezcan esas hojas grandes y hermosas, necesito saber un poco sobre mi nuevo hogar. ¿Cuánta luz recibiré aquí?",
|
||||
botComplete: "¡Perfecto! Estoy lista para mudarme. ¡Creemos tu plan de cuidados!",
|
||||
botGreeting: '¡Hola! Te ayudo a averiguar qué les pasa a tus plantas. 🌿',
|
||||
botSituation: 'Para saber por dónde empezamos: ¿alguna de tus plantas tiene un problema ahora mismo?',
|
||||
botSymptom: 'Entendido. ¿Qué ves exactamente? Esto es lo que más acota las causas posibles.',
|
||||
botNoSymptom: 'Bien saberlo - entonces lo enfocamos todo en la prevención.',
|
||||
botLight: 'La luz influye en la mayoría de los problemas. ¿Cuánta luz recibe tu planta?',
|
||||
botExperience: 'Casi listo. ¿Qué tan seguro te sientes cuidando plantas? Con eso ajusto cuánto explico.',
|
||||
botSource: 'Última pregunta, por curiosidad: ¿cómo te enteraste de GreenLens?',
|
||||
botComplete: 'Perfecto. Ahora preparo tu plan.',
|
||||
stepSituation: '¿Alguna planta tiene un problema?',
|
||||
stepSymptom: '¿Qué ves?',
|
||||
stepLight: '¿Cuánta luz recibe?',
|
||||
stepExperience: '¿Qué tan seguro te sientes?',
|
||||
stepSource: '¿Cómo nos conociste?',
|
||||
situations: {
|
||||
acute: 'Sí - y empeora rápido',
|
||||
slow: 'Sí - cambia poco a poco',
|
||||
prevention: 'No - quiero prevenir',
|
||||
},
|
||||
symptoms: {
|
||||
yellow: 'Hojas amarillas',
|
||||
brown_tips: 'Puntas o bordes marrones',
|
||||
drooping: 'Se cae o pierde hojas',
|
||||
spots: 'Manchas o residuo',
|
||||
pests: 'Bichos',
|
||||
other: 'Otra cosa',
|
||||
},
|
||||
sources: {
|
||||
app_store: 'App Store / Play Store',
|
||||
instagram: 'Instagram',
|
||||
tiktok: 'TikTok',
|
||||
friend: 'Amigos o familia',
|
||||
search: 'Google o búsqueda',
|
||||
tv: 'En la tele (no tenemos anuncios - pero ojalá)',
|
||||
radio: 'En la radio entre dos canciones',
|
||||
plant: 'Me la recomendó mi planta',
|
||||
other: 'Otra cosa',
|
||||
},
|
||||
goals: {
|
||||
identify: 'Identificar plantas',
|
||||
care: 'Horarios de cuidado',
|
||||
collection: 'Crecer mi colección',
|
||||
learn: 'Aprender sobre plantas',
|
||||
},
|
||||
experiences: {
|
||||
beginner: 'Principiante (Soy nuevo)',
|
||||
intermediate: 'Intermedio (Tengo plantas)',
|
||||
advanced: 'Experto (Sé bastante)',
|
||||
beginner: 'Inseguro - recién empiezo',
|
||||
intermediate: 'Más o menos - tengo algunas plantas',
|
||||
advanced: 'Seguro - sé bastante',
|
||||
},
|
||||
lights: {
|
||||
bright_indirect: {
|
||||
@@ -132,35 +170,52 @@ const getChatCopy = (language: Language) => {
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Monstera',
|
||||
title: 'GreenLens',
|
||||
skip: 'Skip',
|
||||
continue: 'Continue',
|
||||
today: 'Today, 8:42 PM',
|
||||
typing: 'typing...',
|
||||
botGreeting: "Hi! I'm your new Monstera. 🌿 I'm so excited to grow with you!",
|
||||
botSource: "First, how did you hear about GreenLens?",
|
||||
botGoal: "Got it! And what is your main goal with your plants?",
|
||||
botExperience: "Nice! How experienced are you with plant care?",
|
||||
botLight: "To stay happy and grow those big, beautiful leaves, I need to know a bit about my new home. How much light do I get here?",
|
||||
botComplete: "Perfect! I'm ready to move in. Let's build your care plan!",
|
||||
botGreeting: "Hi! I'll help you work out what's going on with your plants. 🌿",
|
||||
botSituation: 'So I know where to start: is one of your plants having a problem right now?',
|
||||
botSymptom: 'Got it. What exactly do you see? This narrows down the possible causes more than anything else.',
|
||||
botNoSymptom: "Good to know - then we'll focus everything on prevention.",
|
||||
botLight: 'Light plays a role in most problems. How much light does your plant get?',
|
||||
botExperience: 'Almost done. How confident do you feel about plant care? I use that to adjust how much I explain.',
|
||||
botSource: 'Last question, purely out of curiosity: how did you hear about GreenLens?',
|
||||
botComplete: "Perfect. I'm putting your plan together now.",
|
||||
stepSituation: 'Is a plant having a problem?',
|
||||
stepSymptom: 'What do you see?',
|
||||
stepLight: 'How much light does it get?',
|
||||
stepExperience: 'How confident do you feel?',
|
||||
stepSource: 'How did you hear about us?',
|
||||
situations: {
|
||||
acute: "Yes - and it's getting worse fast",
|
||||
slow: "Yes - it's changing slowly",
|
||||
prevention: 'No - I want to prevent problems',
|
||||
},
|
||||
symptoms: {
|
||||
yellow: 'Yellow leaves',
|
||||
brown_tips: 'Brown tips or edges',
|
||||
drooping: 'Drooping or dropping leaves',
|
||||
spots: 'Spots or residue',
|
||||
pests: 'Bugs',
|
||||
other: 'Something else',
|
||||
},
|
||||
sources: {
|
||||
app_store: 'App Store / Play Store',
|
||||
instagram: 'Instagram',
|
||||
tiktok: 'TikTok',
|
||||
friend: 'Friends or family',
|
||||
search: 'Google or search',
|
||||
tv: "On TV (we don't run ads - but a girl can dream)",
|
||||
radio: 'On the radio between two songs',
|
||||
plant: 'My plant recommended it',
|
||||
other: 'Something else',
|
||||
},
|
||||
goals: {
|
||||
identify: 'Identify plants',
|
||||
care: 'Care schedules',
|
||||
collection: 'Grow collection',
|
||||
learn: 'Learn about plants',
|
||||
},
|
||||
experiences: {
|
||||
beginner: 'Beginner (I\'m a new plant parent)',
|
||||
intermediate: 'Intermediate (I have some plants)',
|
||||
advanced: 'Advanced (I\'m a plant expert)',
|
||||
beginner: "Not confident - I'm just starting",
|
||||
intermediate: 'So-so - I have a few plants',
|
||||
advanced: 'Confident - I know my way around',
|
||||
},
|
||||
lights: {
|
||||
bright_indirect: {
|
||||
@@ -179,6 +234,11 @@ const getChatCopy = (language: Language) => {
|
||||
};
|
||||
};
|
||||
|
||||
/** Fragenreihenfolge des Chats. Bewusst: Problem zuerst, Attribution zuletzt. */
|
||||
const STEP_KEYS = ['situation', 'symptom', 'lightLevel', 'experienceLevel', 'acquisitionSource'] as const;
|
||||
type StepKey = (typeof STEP_KEYS)[number];
|
||||
const TOTAL_STEPS = STEP_KEYS.length;
|
||||
|
||||
const TypingIndicator = ({ colors }: { colors: any }) => {
|
||||
const dot1 = useRef(new Animated.Value(0)).current;
|
||||
const dot2 = useRef(new Animated.Value(0)).current;
|
||||
@@ -233,7 +293,7 @@ export default function OnboardingChatScreen() {
|
||||
const posthog = useSafeAnalytics();
|
||||
const { session, colorPalette, language } = useApp();
|
||||
// This screen is hardcoded dark (#131e14 backgrounds), so always use the
|
||||
// dark color set — theme-dependent text would be unreadable in light mode.
|
||||
// dark color set - theme-dependent text would be unreadable in light mode.
|
||||
const colors = useColors(true, colorPalette);
|
||||
const copy = getChatCopy(language);
|
||||
const { height } = useWindowDimensions();
|
||||
@@ -244,10 +304,11 @@ export default function OnboardingChatScreen() {
|
||||
const [isTyping, setIsTyping] = useState<boolean>(false);
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false);
|
||||
const [answers, setAnswers] = useState({
|
||||
acquisitionSource: null as string | null,
|
||||
primaryGoal: null as string | null,
|
||||
experienceLevel: null as string | null,
|
||||
situation: null as string | null,
|
||||
symptom: null as string | null,
|
||||
lightLevel: null as string | null,
|
||||
experienceLevel: null as string | null,
|
||||
acquisitionSource: null as string | null,
|
||||
});
|
||||
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
@@ -296,9 +357,9 @@ export default function OnboardingChatScreen() {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: 'question-source',
|
||||
id: 'question-situation',
|
||||
sender: 'bot',
|
||||
text: copy.botSource,
|
||||
text: copy.botSituation,
|
||||
timestamp: copy.today,
|
||||
},
|
||||
]);
|
||||
@@ -331,15 +392,17 @@ export default function OnboardingChatScreen() {
|
||||
};
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
|
||||
const nextStep = currentStep + 1;
|
||||
// Wer "vorbeugen" waehlt, hat kein Symptom - die Symptomfrage wird
|
||||
// uebersprungen. Ohne das antwortet der Bot "dann beugen wir vor" und
|
||||
// fragt im selben Atemzug "Was siehst du?".
|
||||
const skipsSymptom = currentStep === 0 && id === 'prevention';
|
||||
const nextStep = skipsSymptom ? 2 : currentStep + 1;
|
||||
setCurrentStep(nextStep);
|
||||
|
||||
// Save answer
|
||||
const nextAnswers = { ...answers };
|
||||
if (currentStep === 0) nextAnswers.acquisitionSource = id;
|
||||
else if (currentStep === 1) nextAnswers.primaryGoal = id;
|
||||
else if (currentStep === 2) nextAnswers.experienceLevel = id;
|
||||
else if (currentStep === 3) nextAnswers.lightLevel = id;
|
||||
const stepKey: StepKey | undefined = STEP_KEYS[currentStep];
|
||||
if (stepKey) nextAnswers[stepKey] = id;
|
||||
setAnswers(nextAnswers);
|
||||
|
||||
// Trigger bot response typing
|
||||
@@ -350,12 +413,16 @@ export default function OnboardingChatScreen() {
|
||||
let botSubtext = undefined;
|
||||
|
||||
if (nextStep === 1) {
|
||||
botText = copy.botGoal;
|
||||
botText = copy.botSymptom;
|
||||
} else if (nextStep === 2) {
|
||||
botText = copy.botExperience;
|
||||
// Nach dem uebersprungenen Symptomschritt zuerst die Ueberleitung,
|
||||
// damit die Antwort des Nutzers nicht unkommentiert bleibt.
|
||||
botText = skipsSymptom ? `${copy.botNoSymptom} ${copy.botLight}` : copy.botLight;
|
||||
} else if (nextStep === 3) {
|
||||
botText = copy.botLight;
|
||||
botText = copy.botExperience;
|
||||
} else if (nextStep === 4) {
|
||||
botText = copy.botSource;
|
||||
} else if (nextStep === TOTAL_STEPS) {
|
||||
botText = copy.botComplete;
|
||||
}
|
||||
|
||||
@@ -382,15 +449,17 @@ export default function OnboardingChatScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevStep = currentStep - 1;
|
||||
// Gegenstueck zum uebersprungenen Symptomschritt: von Schritt 2 zurueck
|
||||
// landet ein Vorbeuger wieder bei der Situationsfrage, nicht bei einer
|
||||
// Symptomfrage, die er nie gesehen hat.
|
||||
const skippedSymptom = currentStep === 2 && answers.situation === 'prevention';
|
||||
const prevStep = skippedSymptom ? 0 : currentStep - 1;
|
||||
setCurrentStep(prevStep);
|
||||
|
||||
// Revert last answers
|
||||
const nextAnswers = { ...answers };
|
||||
if (prevStep === 0) nextAnswers.acquisitionSource = null;
|
||||
else if (prevStep === 1) nextAnswers.primaryGoal = null;
|
||||
else if (prevStep === 2) nextAnswers.experienceLevel = null;
|
||||
else if (prevStep === 3) nextAnswers.lightLevel = null;
|
||||
const prevKey: StepKey | undefined = STEP_KEYS[prevStep];
|
||||
if (prevKey) nextAnswers[prevKey] = null;
|
||||
setAnswers(nextAnswers);
|
||||
|
||||
// Remove user bubble and bot bubble from messages history
|
||||
@@ -411,20 +480,22 @@ export default function OnboardingChatScreen() {
|
||||
const onFinish = () => {
|
||||
if (session?.userId) {
|
||||
if (answers.acquisitionSource) OnboardingProgressService.setAcquisitionSource(session.userId, answers.acquisitionSource);
|
||||
if (answers.primaryGoal) OnboardingProgressService.setPrimaryGoal(session.userId, answers.primaryGoal);
|
||||
if (answers.experienceLevel) OnboardingProgressService.setExperienceLevel(session.userId, answers.experienceLevel);
|
||||
}
|
||||
|
||||
|
||||
if (answers.acquisitionSource) void PreAuthOnboardingService.setAnswer('acquisitionSource', answers.acquisitionSource);
|
||||
if (answers.primaryGoal) void PreAuthOnboardingService.setAnswer('primaryGoal', answers.primaryGoal);
|
||||
if (answers.experienceLevel) void PreAuthOnboardingService.setAnswer('experienceLevel', answers.experienceLevel);
|
||||
if (answers.lightLevel) void PreAuthOnboardingService.setAnswer('lightLevel', answers.lightLevel);
|
||||
// Situation und Symptom steuern spaeter die Personalisierung des Plans.
|
||||
if (answers.situation) void PreAuthOnboardingService.setAnswer('situation', answers.situation);
|
||||
if (answers.symptom) void PreAuthOnboardingService.setAnswer('symptom', answers.symptom);
|
||||
|
||||
posthog.capture('onboarding_chat_completed', {
|
||||
source: answers.acquisitionSource,
|
||||
goal: answers.primaryGoal,
|
||||
experience: answers.experienceLevel,
|
||||
situation: answers.situation,
|
||||
symptom: answers.symptom,
|
||||
light: answers.lightLevel,
|
||||
experience: answers.experienceLevel,
|
||||
source: answers.acquisitionSource,
|
||||
});
|
||||
|
||||
router.replace('/onboarding/health-check');
|
||||
@@ -434,18 +505,43 @@ export default function OnboardingChatScreen() {
|
||||
const renderOptions = () => {
|
||||
if (!showOptions || isTyping) return null;
|
||||
|
||||
// Schritt 1 - Situation. Erste Frage gehoert dem Nutzer, nicht dem Marketing.
|
||||
if (currentStep === 0) {
|
||||
return (
|
||||
<View style={styles.optionsWrap}>
|
||||
<Text style={styles.stepTitle}>How did you find GreenLens?</Text>
|
||||
<Text style={styles.stepTitle}>{copy.stepSituation}</Text>
|
||||
<View style={styles.optionsVertical}>
|
||||
{([
|
||||
{ id: 'acute', label: copy.situations.acute, icon: 'alert-circle-outline' as const },
|
||||
{ id: 'slow', label: copy.situations.slow, icon: 'trending-down-outline' as const },
|
||||
{ id: 'prevention', label: copy.situations.prevention, icon: 'shield-checkmark-outline' as const },
|
||||
]).map((opt) => (
|
||||
<TouchableOpacity key={opt.id} style={[styles.optionRow, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
||||
<View style={styles.optionRowLeft}>
|
||||
<Ionicons name={opt.icon} size={18} color={colors.primary} style={{ marginRight: 10 }} />
|
||||
<Text style={[styles.rowText, { color: colors.text }]}>{opt.label}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Schritt 2 - Symptom. Der Moment, in dem sich der Nutzer wiedererkennt.
|
||||
if (currentStep === 1) {
|
||||
return (
|
||||
<View style={styles.optionsWrap}>
|
||||
<Text style={styles.stepTitle}>{copy.stepSymptom}</Text>
|
||||
<View style={styles.optionsGrid}>
|
||||
{([
|
||||
{ id: 'app_store', label: copy.sources.app_store, icon: 'logo-apple' as const },
|
||||
{ id: 'instagram', label: copy.sources.instagram, icon: 'logo-instagram' as const },
|
||||
{ id: 'tiktok', label: copy.sources.tiktok, icon: 'logo-youtube' as const },
|
||||
{ id: 'friend', label: copy.sources.friend, icon: 'people-outline' as const },
|
||||
{ id: 'search', label: copy.sources.search, icon: 'search-outline' as const },
|
||||
{ id: 'other', label: copy.sources.other, icon: 'sparkles-outline' as const },
|
||||
{ id: 'yellow', label: copy.symptoms.yellow, icon: 'leaf-outline' as const },
|
||||
{ id: 'brown_tips', label: copy.symptoms.brown_tips, icon: 'flame-outline' as const },
|
||||
{ id: 'drooping', label: copy.symptoms.drooping, icon: 'trending-down-outline' as const },
|
||||
{ id: 'spots', label: copy.symptoms.spots, icon: 'ellipse-outline' as const },
|
||||
{ id: 'pests', label: copy.symptoms.pests, icon: 'bug-outline' as const },
|
||||
{ id: 'other', label: copy.symptoms.other, icon: 'help-circle-outline' as const },
|
||||
]).map((opt) => (
|
||||
<TouchableOpacity key={opt.id} style={[styles.pillBtn, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
||||
<Ionicons name={opt.icon} size={15} color={colors.primary} style={{ marginRight: 4 }} />
|
||||
@@ -457,57 +553,11 @@ export default function OnboardingChatScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep === 1) {
|
||||
return (
|
||||
<View style={styles.optionsWrap}>
|
||||
<Text style={styles.stepTitle}>What is your main goal?</Text>
|
||||
<View style={styles.optionsVertical}>
|
||||
{([
|
||||
{ id: 'identify', label: copy.goals.identify, icon: 'search-outline' as const },
|
||||
{ id: 'care', label: copy.goals.care, icon: 'water-outline' as const },
|
||||
{ id: 'collection', label: copy.goals.collection, icon: 'leaf-outline' as const },
|
||||
{ id: 'learn', label: copy.goals.learn, icon: 'book-outline' as const },
|
||||
]).map((opt) => (
|
||||
<TouchableOpacity key={opt.id} style={[styles.optionRow, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
||||
<View style={styles.optionRowLeft}>
|
||||
<Ionicons name={opt.icon} size={18} color={colors.primary} style={{ marginRight: 10 }} />
|
||||
<Text style={[styles.rowText, { color: colors.text }]}>{opt.label}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Schritt 3 - Licht.
|
||||
if (currentStep === 2) {
|
||||
return (
|
||||
<View style={styles.optionsWrap}>
|
||||
<Text style={styles.stepTitle}>What is your experience level?</Text>
|
||||
<View style={styles.optionsVertical}>
|
||||
{([
|
||||
{ id: 'beginner', label: copy.experiences.beginner, icon: 'leaf-outline' as const },
|
||||
{ id: 'intermediate', label: copy.experiences.intermediate, icon: 'sunny-outline' as const },
|
||||
{ id: 'advanced', label: copy.experiences.advanced, icon: 'flask-outline' as const },
|
||||
]).map((opt) => (
|
||||
<TouchableOpacity key={opt.id} style={[styles.optionRow, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
||||
<View style={styles.optionRowLeft}>
|
||||
<Ionicons name={opt.icon} size={18} color={colors.primary} style={{ marginRight: 10 }} />
|
||||
<Text style={[styles.rowText, { color: colors.text }]}>{opt.label}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep === 3) {
|
||||
return (
|
||||
<View style={styles.optionsWrap}>
|
||||
<Text style={styles.stepTitle}>How much light do I get here?</Text>
|
||||
<Text style={styles.stepTitle}>{copy.stepLight}</Text>
|
||||
<View style={styles.optionsVertical}>
|
||||
{([
|
||||
{ id: 'bright_indirect', label: copy.lights.bright_indirect.title, sub: copy.lights.bright_indirect.sub, icon: 'wb-twilight' },
|
||||
@@ -527,7 +577,62 @@ export default function OnboardingChatScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
// Schritt 4 - Erfahrung. Formuliert als Sicherheitsgefuehl, nicht als
|
||||
// Wissenstest: Emma soll sich nicht schon hier als Anfaengerin abstempeln.
|
||||
if (currentStep === 3) {
|
||||
return (
|
||||
<View style={styles.optionsWrap}>
|
||||
<Text style={styles.stepTitle}>{copy.stepExperience}</Text>
|
||||
<View style={styles.optionsVertical}>
|
||||
{([
|
||||
{ id: 'beginner', label: copy.experiences.beginner, icon: 'leaf-outline' as const },
|
||||
{ id: 'intermediate', label: copy.experiences.intermediate, icon: 'sunny-outline' as const },
|
||||
{ id: 'advanced', label: copy.experiences.advanced, icon: 'flask-outline' as const },
|
||||
]).map((opt) => (
|
||||
<TouchableOpacity key={opt.id} style={[styles.optionRow, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
||||
<View style={styles.optionRowLeft}>
|
||||
<Ionicons name={opt.icon} size={18} color={colors.primary} style={{ marginRight: 10 }} />
|
||||
<Text style={[styles.rowText, { color: colors.text }]}>{opt.label}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Schritt 5 - Attribution. Ganz ans Ende verschoben: die Frage nuetzt uns,
|
||||
// nicht dem Nutzer. Die unwahrscheinlichen Optionen (TV, Radio, "meine
|
||||
// Pflanze") brechen das Muster und machen den letzten Tap leicht.
|
||||
if (currentStep === 4) {
|
||||
return (
|
||||
<View style={styles.optionsWrap}>
|
||||
<Text style={styles.stepTitle}>{copy.stepSource}</Text>
|
||||
<View style={styles.optionsGrid}>
|
||||
{([
|
||||
{ id: 'app_store', label: copy.sources.app_store, icon: 'logo-apple' as const },
|
||||
{ id: 'instagram', label: copy.sources.instagram, icon: 'logo-instagram' as const },
|
||||
{ id: 'tiktok', label: copy.sources.tiktok, icon: 'musical-notes-outline' as const },
|
||||
{ id: 'friend', label: copy.sources.friend, icon: 'people-outline' as const },
|
||||
{ id: 'search', label: copy.sources.search, icon: 'search-outline' as const },
|
||||
{ id: 'tv', label: copy.sources.tv, icon: 'tv-outline' as const },
|
||||
{ id: 'radio', label: copy.sources.radio, icon: 'radio-outline' as const },
|
||||
{ id: 'plant', label: copy.sources.plant, icon: 'leaf-outline' as const },
|
||||
{ id: 'other', label: copy.sources.other, icon: 'sparkles-outline' as const },
|
||||
]).map((opt) => (
|
||||
<TouchableOpacity key={opt.id} style={[styles.pillBtn, { borderColor: colors.border }]} onPress={() => selectOption(opt.id, opt.label)}>
|
||||
<Ionicons name={opt.icon} size={15} color={colors.primary} style={{ marginRight: 4 }} />
|
||||
<Text style={[styles.pillLabel, { color: colors.text }]}>{opt.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Alle Fragen beantwortet -> Abschluss-CTA.
|
||||
if (currentStep >= TOTAL_STEPS) {
|
||||
return (
|
||||
<View style={styles.footerWrap}>
|
||||
<TouchableOpacity style={[styles.continueCta, { backgroundColor: colors.primary }]} onPress={onFinish} activeOpacity={0.86}>
|
||||
@@ -562,7 +667,7 @@ export default function OnboardingChatScreen() {
|
||||
);
|
||||
};
|
||||
|
||||
const progressPercent = `${Math.min(currentStep * 25, 100)}%`;
|
||||
const progressPercent = `${Math.min(Math.round((currentStep / TOTAL_STEPS) * 100), 100)}%`;
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
|
||||
Reference in New Issue
Block a user