feat(app): personalizing progress screen, auto-advances to paywall
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
165
app/onboarding/personalizing.tsx
Normal file
165
app/onboarding/personalizing.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Animated, Easing, Image, StyleSheet, Text, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import Svg, { Circle } from 'react-native-svg';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
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',
|
||||
};
|
||||
}
|
||||
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',
|
||||
};
|
||||
}
|
||||
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',
|
||||
};
|
||||
};
|
||||
|
||||
const STEP_THRESHOLDS = [25, 50, 75, 95];
|
||||
const RING_SIZE = 150;
|
||||
const RING_STROKE_WIDTH = 7;
|
||||
const RING_RADIUS = (RING_SIZE - RING_STROKE_WIDTH) / 2;
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
|
||||
|
||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||
|
||||
export default function OnboardingPersonalizingScreen() {
|
||||
const { language, isDarkMode, colorPalette } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const posthog = useSafeAnalytics();
|
||||
const copy = getCopy(language);
|
||||
const progress = useRef(new Animated.Value(0)).current;
|
||||
const [percent, setPercent] = useState(0);
|
||||
const navigated = useRef(false);
|
||||
|
||||
const strokeDashoffset = progress.interpolate({
|
||||
inputRange: [0, 100],
|
||||
outputRange: [RING_CIRCUMFERENCE, 0],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('onboarding_personalizing_viewed');
|
||||
const listener = progress.addListener(({ value }) => setPercent(Math.round(value)));
|
||||
Animated.timing(progress, {
|
||||
toValue: 100,
|
||||
duration: 6000,
|
||||
easing: Easing.inOut(Easing.cubic),
|
||||
useNativeDriver: false,
|
||||
}).start(({ finished }) => {
|
||||
if (finished && !navigated.current) {
|
||||
navigated.current = true;
|
||||
setTimeout(() => {
|
||||
posthog.capture('paywall_opened', { source: 'onboarding' });
|
||||
router.replace('/profile/billing?view=paywall&context=onboarding');
|
||||
}, 450);
|
||||
}
|
||||
});
|
||||
return () => progress.removeListener(listener);
|
||||
}, [progress, posthog]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]}>
|
||||
<Text style={[styles.percent, { color: colors.primary }]}>{percent}%</Text>
|
||||
<View style={styles.ringWrap}>
|
||||
<Svg width={RING_SIZE} height={RING_SIZE} style={StyleSheet.absoluteFill}>
|
||||
<Circle
|
||||
cx={RING_SIZE / 2}
|
||||
cy={RING_SIZE / 2}
|
||||
r={RING_RADIUS}
|
||||
stroke={colors.primarySoft}
|
||||
strokeWidth={RING_STROKE_WIDTH}
|
||||
fill="none"
|
||||
/>
|
||||
<AnimatedCircle
|
||||
cx={RING_SIZE / 2}
|
||||
cy={RING_SIZE / 2}
|
||||
r={RING_RADIUS}
|
||||
stroke={colors.primary}
|
||||
strokeWidth={RING_STROKE_WIDTH}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${RING_CIRCUMFERENCE}, ${RING_CIRCUMFERENCE}`}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
rotation="-90"
|
||||
originX={RING_SIZE / 2}
|
||||
originY={RING_SIZE / 2}
|
||||
/>
|
||||
</Svg>
|
||||
<Image source={require('../../assets/paywall_scan_background.png')} style={styles.ringImage} />
|
||||
</View>
|
||||
<View style={[styles.statusPill, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons name="sync-outline" size={15} color={colors.textSecondary} />
|
||||
<Text style={[styles.statusText, { color: colors.textSecondary }]}>{copy.status}</Text>
|
||||
</View>
|
||||
<View style={styles.checklist}>
|
||||
{copy.steps.map((label, index) => {
|
||||
const done = percent >= STEP_THRESHOLDS[index];
|
||||
return (
|
||||
<View key={label} style={styles.checkRow}>
|
||||
<Ionicons
|
||||
name={done ? 'checkmark-circle' : 'ellipse-outline'}
|
||||
size={24}
|
||||
color={done ? colors.primary : colors.border}
|
||||
/>
|
||||
<Text style={[styles.checkLabel, { color: done ? colors.text : colors.textMuted }]}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, alignItems: 'center', paddingHorizontal: 24, paddingTop: 30 },
|
||||
percent: { fontSize: 56, fontWeight: '900', marginBottom: 16 },
|
||||
ringWrap: { width: 150, height: 150, alignItems: 'center', justifyContent: 'center', marginBottom: 22 },
|
||||
ringImage: { width: 112, height: 112, borderRadius: 56 },
|
||||
statusPill: { flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 999, paddingHorizontal: 18, paddingVertical: 11, marginBottom: 26 },
|
||||
statusText: { fontSize: 14.5, fontWeight: '800' },
|
||||
checklist: { alignSelf: 'stretch', gap: 15, marginBottom: 26, paddingHorizontal: 8 },
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user