App copy
This commit is contained in:
@@ -13,58 +13,116 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { useSafeAnalytics } from '../services/analytics';
|
||||
import { getWelcomeHeadlineVariant, WelcomeHeadlineVariant } from '../services/experiments';
|
||||
import { APP_STORE_RATING, getTestimonialForLanguage, translatedNote } from '../constants/socialProof';
|
||||
import { Language } from '../types';
|
||||
|
||||
const getWelcomeCopy = (language: Language) => {
|
||||
const getWelcomeCopy = (language: Language, variant: WelcomeHeadlineVariant) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
headline: 'Willkommen bei GreenLens!',
|
||||
subline: 'Pflanzen erkennen, verstehen und pflegen — ganz einfach.',
|
||||
testimonial: '„Endlich überleben meine Pflanzen! Absolute Empfehlung."',
|
||||
testimonialAuthor: 'Anna M.',
|
||||
cta: "Los geht's",
|
||||
login: 'Anmelden',
|
||||
demoScan: 'Oder direkt eine Pflanze scannen',
|
||||
// Variante A (symptom): steigt direkt in Emmas akuten Moment ein.
|
||||
// Variante B (contrast): grenzt gegen kostenlose Identifikations-Apps ab.
|
||||
headline:
|
||||
variant === 'symptom'
|
||||
? 'Gelbe Blätter. Und jetzt?'
|
||||
: 'Andere Apps sagen dir, wie deine Pflanze heißt.',
|
||||
subline:
|
||||
variant === 'symptom'
|
||||
? 'GreenLens nennt dir die wahrscheinlichsten Ursachen, was du selbst prüfen solltest und die eine sichere Sache, die du jetzt tun kannst.'
|
||||
: 'GreenLens sagt dir, was mit ihr los ist: wahrscheinliche Ursache, was du prüfen sollst, was du als Nächstes tust - und ein Check, ob es gewirkt hat.',
|
||||
cta: 'Meine Pflanze prüfen',
|
||||
login: 'Ich habe schon ein Konto',
|
||||
demoScan: 'Erst ausprobieren - ohne Konto',
|
||||
legal: 'Mit dem Fortfahren akzeptierst du unsere Datenschutzerklärung und AGB.',
|
||||
rating: '4,8',
|
||||
ratingValue: APP_STORE_RATING.value.toLocaleString('de-DE', { minimumFractionDigits: 1 }),
|
||||
ratingCount: `${APP_STORE_RATING.count} Bewertungen`,
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
headline: '¡Bienvenido a GreenLens!',
|
||||
subline: 'Identifica, entiende y cuida tus plantas — sin esfuerzo.',
|
||||
testimonial: '"¡Por fin mis plantas sobreviven! Muy recomendable."',
|
||||
testimonialAuthor: 'Anna M.',
|
||||
cta: 'Empezar',
|
||||
login: 'Iniciar sesión',
|
||||
demoScan: 'O escanea una planta ahora',
|
||||
headline:
|
||||
variant === 'symptom'
|
||||
? 'Hojas amarillas. ¿Y ahora?'
|
||||
: 'Otras apps te dicen cómo se llama tu planta.',
|
||||
subline:
|
||||
variant === 'symptom'
|
||||
? 'GreenLens te da las causas más probables, qué revisar tú mismo y la única cosa segura que puedes hacer ahora.'
|
||||
: 'GreenLens te dice qué le pasa: causa probable, qué revisar, qué hacer después - y una comprobación de si funcionó.',
|
||||
cta: 'Revisar mi planta',
|
||||
login: 'Ya tengo cuenta',
|
||||
demoScan: 'Probar primero - sin cuenta',
|
||||
legal: 'Al continuar aceptas nuestra Política de privacidad y Términos.',
|
||||
rating: '4.8',
|
||||
ratingValue: APP_STORE_RATING.value.toLocaleString('es-ES', { minimumFractionDigits: 1 }),
|
||||
ratingCount: `${APP_STORE_RATING.count} valoraciones`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
headline: 'Welcome to GreenLens!',
|
||||
subline: 'Identify, understand and care for your plants — effortlessly.',
|
||||
testimonial: '"Finally my plants stay alive! Highly recommend."',
|
||||
testimonialAuthor: 'Anna M.',
|
||||
cta: "Let's Go",
|
||||
login: 'Log in',
|
||||
demoScan: 'Or scan a plant right now',
|
||||
headline:
|
||||
variant === 'symptom'
|
||||
? 'Yellow leaves. Now what?'
|
||||
: 'Other apps tell you what your plant is called.',
|
||||
subline:
|
||||
variant === 'symptom'
|
||||
? 'GreenLens gives you the most likely causes, what to check yourself, and the one safe thing to do right now.'
|
||||
: "GreenLens tells you what's wrong with it: likely cause, what to check, what to do next - and a follow-up on whether it worked.",
|
||||
cta: 'Check my plant',
|
||||
login: 'I already have an account',
|
||||
demoScan: 'Try it first - no account needed',
|
||||
legal: 'By continuing you agree to our Privacy Policy and Terms.',
|
||||
rating: '4.8',
|
||||
ratingValue: APP_STORE_RATING.value.toLocaleString('en-US', { minimumFractionDigits: 1 }),
|
||||
ratingCount: `${APP_STORE_RATING.count} ratings`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Proof-Punkte statt Testimonial: greifen, solange keine echten Store-Zitate
|
||||
* hinterlegt sind. Beide Zeilen sind Produktaussagen, keine Behauptungen ueber
|
||||
* Ergebnisse - sie halten damit die Positionierung "Triage statt Diagnose".
|
||||
*/
|
||||
const getProofPoints = (language: Language): string[] => {
|
||||
if (language === 'de') {
|
||||
return [
|
||||
'Wahrscheinlichste Ursachen - mit Angabe, wie sicher wir sind',
|
||||
'Konkrete Prüfschritte statt allgemeiner Pflegetipps',
|
||||
'Nach 7 Tagen fragen wir nach, ob es gewirkt hat',
|
||||
];
|
||||
}
|
||||
if (language === 'es') {
|
||||
return [
|
||||
'Causas más probables - indicando qué tan seguros estamos',
|
||||
'Pasos concretos de revisión, no consejos genéricos',
|
||||
'A los 7 días preguntamos si funcionó',
|
||||
];
|
||||
}
|
||||
return [
|
||||
'Most likely causes - with how confident we actually are',
|
||||
'Concrete things to check, not generic care tips',
|
||||
'After 7 days we ask whether it worked',
|
||||
];
|
||||
};
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const { language } = useApp();
|
||||
const { height } = useWindowDimensions();
|
||||
const compact = height < 700;
|
||||
const posthog = useSafeAnalytics();
|
||||
const copy = getWelcomeCopy(language);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [variant, setVariant] = React.useState<WelcomeHeadlineVariant | null>(null);
|
||||
const copy = getWelcomeCopy(language, variant ?? 'symptom');
|
||||
const testimonial = getTestimonialForLanguage(language);
|
||||
const proofPoints = getProofPoints(language);
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('onboarding_welcome_viewed');
|
||||
let active = true;
|
||||
void getWelcomeHeadlineVariant().then((assigned) => {
|
||||
if (!active) return;
|
||||
setVariant(assigned);
|
||||
posthog.capture('onboarding_welcome_viewed', { welcome_variant: assigned });
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [posthog]);
|
||||
|
||||
return (
|
||||
@@ -87,23 +145,43 @@ export default function OnboardingScreen() {
|
||||
Green<Text style={styles.brandAccent}>Lens</Text>
|
||||
</Text>
|
||||
</View>
|
||||
{/* Rating und Anzahl immer gemeinsam: 5,0 aus 6 Bewertungen ist
|
||||
ehrlich und ueberpruefbar - eine nackte Sternezahl waere es nicht. */}
|
||||
<View style={styles.ratingPill}>
|
||||
<Ionicons name="star" size={13} color="#f5c04e" />
|
||||
<Text style={styles.ratingText}>{copy.rating}</Text>
|
||||
<Text style={styles.ratingText}>{copy.ratingValue}</Text>
|
||||
<Text style={styles.ratingCount}>· {copy.ratingCount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
<Text style={styles.testimonialText}>{copy.testimonial}</Text>
|
||||
<View style={styles.testimonialMeta}>
|
||||
<Text style={styles.testimonialAuthor}>{copy.testimonialAuthor}</Text>
|
||||
<View style={styles.starsRow}>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<Ionicons key={i} name="star" size={14} color="#f5c04e" />
|
||||
))}
|
||||
{testimonial ? (
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
<Text style={styles.testimonialText} numberOfLines={compact ? 3 : 4}>
|
||||
„{testimonial.quote}"
|
||||
</Text>
|
||||
<View style={styles.testimonialMeta}>
|
||||
<Text style={styles.testimonialAuthor}>
|
||||
{testimonial.author}
|
||||
{testimonial.isTranslated ? ` · ${translatedNote(language)}` : ''}
|
||||
</Text>
|
||||
<View style={styles.starsRow}>
|
||||
{Array.from({ length: testimonial.stars }).map((_, i) => (
|
||||
<Ionicons key={i} name="star" size={14} color="#f5c04e" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
/* Kein belegtes Zitat vorhanden → Argument statt erfundenem Social Proof. */
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
{proofPoints.map((point) => (
|
||||
<View key={point} style={styles.proofRow}>
|
||||
<Ionicons name="checkmark-circle" size={15} color="#a6d66f" />
|
||||
<Text style={styles.proofText}>{point}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -111,15 +189,20 @@ export default function OnboardingScreen() {
|
||||
<View style={styles.sheetHandle} />
|
||||
<View style={styles.sheetContent}>
|
||||
<View style={styles.topSpacer} />
|
||||
<Text style={[styles.headline, compact && styles.headlineCompact]}>{copy.headline}</Text>
|
||||
<Text style={styles.subline}>{copy.subline}</Text>
|
||||
{/* Erst rendern, wenn die Variante feststeht. Sonst blitzt fuer die
|
||||
Haelfte der Nutzer kurz die falsche Headline auf und wechselt dann -
|
||||
das sieht kaputt aus und verfaelscht den Test. */}
|
||||
<View style={{ opacity: variant ? 1 : 0 }}>
|
||||
<Text style={[styles.headline, compact && styles.headlineCompact]}>{copy.headline}</Text>
|
||||
<Text style={styles.subline}>{copy.subline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.cta}
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_started');
|
||||
posthog.capture('onboarding_started', { welcome_variant: variant ?? 'unassigned' });
|
||||
router.push('/onboarding/slides');
|
||||
}}
|
||||
activeOpacity={0.86}
|
||||
@@ -131,7 +214,15 @@ export default function OnboardingScreen() {
|
||||
<Text style={styles.loginText}>{copy.login}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => router.push('/scanner')} style={styles.demoLink}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_demo_scan_started', {
|
||||
welcome_variant: variant ?? 'unassigned',
|
||||
});
|
||||
router.push('/scanner');
|
||||
}}
|
||||
style={styles.demoLink}
|
||||
>
|
||||
<Ionicons name="scan-outline" size={18} color="#a6d66f" />
|
||||
<Text style={styles.demoText}>{copy.demoScan}</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -209,6 +300,24 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
ratingCount: {
|
||||
color: '#c9d3c2',
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
proofRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
proofText: {
|
||||
flex: 1,
|
||||
color: '#e6efe2',
|
||||
fontSize: 13.5,
|
||||
lineHeight: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
testimonialCard: {
|
||||
backgroundColor: 'rgba(16, 26, 18, 0.75)',
|
||||
borderRadius: 16,
|
||||
|
||||
Reference in New Issue
Block a user