Add profile photo to onboarding flow

This commit is contained in:
jjbrunton committed 2026-01-17 19:22:36 +00:00
1 parent 349f460e15
commit b070a6f6a2
15 files changed
+731 -210

No files matched your search

+26 -31
View File
@@ -1,17 +1,18 @@
import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity, Switch, Platform } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
useAnimatedStyle,
withSpring,
useSharedValue,
FadeIn,
} from 'react-native-reanimated';
import { ShimmerEffect } from './ui/ShimmerEffect';
import { colors, gradients, spacing, radius, typography, blur, shadows, animations } from '../theme';
import type { QuestionPack } from '../types';
import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity, Platform } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
useAnimatedStyle,
withSpring,
useSharedValue,
FadeIn,
} from 'react-native-reanimated';
import { ShimmerEffect } from './ui/ShimmerEffect';
import { GlassToggle } from './ui/GlassToggle';
import { colors, gradients, spacing, radius, typography, blur, shadows, animations } from '../theme';
import type { QuestionPack } from '../types';
// Boutique gradient palette for pack cards
const BOUTIQUE_GRADIENTS: Array<readonly [string, string]> = [
@@ -147,23 +148,17 @@ export function BoutiquePackCard({
<View style={styles.footer}>
<View style={styles.questionBadge}>
<Text style={styles.questionCount}>{questionCount}</Text>
<Text style={styles.questionLabel}>questions</Text>
</View>
<Switch
value={isEnabled}
onValueChange={onToggle}
disabled={isPremiumLocked}
trackColor={{
false: 'rgba(255, 255, 255, 0.15)',
true: pack.is_premium ? colors.premium.gold : colors.primary,
}}
thumbColor={colors.text}
ios_backgroundColor="rgba(255, 255, 255, 0.15)"
style={isPremiumLocked ? { opacity: 0.4 } : undefined}
/>
</View>
</View>
<Text style={styles.questionLabel}>questions</Text>
</View>
<GlassToggle
value={isEnabled}
onValueChange={onToggle}
disabled={isPremiumLocked}
style={isPremiumLocked ? { opacity: 0.4 } : undefined}
/>
</View>
</View>
{/* Lock overlay for premium locked packs */}
{isPremiumLocked && (
@@ -0,0 +1,139 @@
import React, { useEffect } from 'react';
import { StyleSheet, Pressable, ViewStyle, Platform } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
interpolateColor,
} from 'react-native-reanimated';
import { colors, gradients, radius, animations } from '../../theme';
// Haptics is not supported on web
const triggerHaptic = async () => {
if (Platform.OS === 'web') return;
const Haptics = await import('expo-haptics');
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
interface GlassToggleProps {
value: boolean;
onValueChange: (value: boolean) => void;
disabled?: boolean;
style?: ViewStyle;
}
const TRACK_WIDTH = 52;
const TRACK_HEIGHT = 30;
const THUMB_SIZE = 26;
const PADDING = 2;
export function GlassToggle({
value,
onValueChange,
disabled = false,
style,
}: GlassToggleProps) {
// Shared values for animation
const progress = useSharedValue(value ? 1 : 0);
// Sync shared value when prop changes
useEffect(() => {
progress.value = withSpring(value ? 1 : 0, {
mass: 0.8,
damping: 15,
stiffness: 120,
});
}, [value]);
const handlePress = async () => {
if (disabled) return;
await triggerHaptic();
onValueChange(!value);
};
const thumbStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: progress.value * (TRACK_WIDTH - THUMB_SIZE - (PADDING * 2)),
},
],
};
});
const trackStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(
progress.value,
[0, 1],
['rgba(255, 255, 255, 0.1)', 'transparent'] // We use LinearGradient for active state
);
return {
backgroundColor,
};
});
const activeGradientStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
};
});
return (
<Pressable
onPress={handlePress}
disabled={disabled}
style={[styles.container, style, disabled && styles.disabled]}
>
<Animated.View style={[styles.track, trackStyle]}>
{/* Active Gradient Background (fades in) */}
<Animated.View style={[StyleSheet.absoluteFill, activeGradientStyle]}>
<LinearGradient
colors={gradients.primary as [string, string, ...string[]]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={StyleSheet.absoluteFill}
/>
</Animated.View>
{/* Thumb */}
<Animated.View style={[styles.thumb, thumbStyle]} />
</Animated.View>
</Pressable>
);
}
const styles = StyleSheet.create({
container: {
width: TRACK_WIDTH,
height: TRACK_HEIGHT,
},
track: {
width: '100%',
height: '100%',
borderRadius: radius.full,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
overflow: 'hidden',
justifyContent: 'center',
padding: PADDING,
},
thumb: {
width: THUMB_SIZE,
height: THUMB_SIZE,
borderRadius: radius.full,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
disabled: {
opacity: 0.5,
},
});
+1
View File
@@ -10,5 +10,6 @@ export { LoadingOverlay } from './LoadingOverlay';
export type { LoadingOverlayProps, LoadingVariant } from './LoadingOverlay';
export { ReadReceipt, getReceiptStatus } from './ReadReceipt';
export type { ReadReceiptProps, ReceiptStatus } from './ReadReceipt';
export { GlassToggle } from './GlassToggle';
export { AmbientOrbs } from './AmbientOrbs';
export type { AmbientOrbsProps, OrbVariant, OrbPosition } from './AmbientOrbs';