Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive onboarding flow to introduce new users to the app's features. The onboarding consists of 5 interactive steps showcasing the app's key functionality, followed by login/signup options.
Changes:
- Added a full-featured onboarding screen with 5 steps featuring animations and visual demonstrations
- Updated LoginScreen to accept an initialMode prop for seamless transition from onboarding
- Modified app flow control to show onboarding before login for first-time users
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| src/components/Onboarding.tsx | Complete rewrite from simple guide to 5-step interactive onboarding with animations, phone mockups, and visual demonstrations |
| src/components/LoginScreen.tsx | Added initialMode prop to allow onboarding to specify whether to show sign-in or sign-up screen |
| app/index.tsx | Updated flow control to show onboarding before login, removed needsOnboarding state, added loginMode state management |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const travel = Math.max(trackWidth - 80, 0); | ||
| const translateX = guideAnim.interpolate({ | ||
| inputRange: [0, 1], | ||
| outputRange: [0, travel], |
There was a problem hiding this comment.
The variable name 'travel' is ambiguous. A more descriptive name like 'maxTranslateDistance' or 'dragDistance' would better convey its purpose as the maximum distance the animated pointer can travel.
| const travel = Math.max(trackWidth - 80, 0); | |
| const translateX = guideAnim.interpolate({ | |
| inputRange: [0, 1], | |
| outputRange: [0, travel], | |
| const maxTranslateDistance = Math.max(trackWidth - 80, 0); | |
| const translateX = guideAnim.interpolate({ | |
| inputRange: [0, 1], | |
| outputRange: [0, maxTranslateDistance], |
| <Text className="text-xs font-bold text-gray-300">2024</Text> | ||
| <Text className="text-xs font-bold text-gray-300">MAY</Text> |
There was a problem hiding this comment.
The hardcoded date values "2024" and "MAY" make the mockup less realistic and could confuse users if they see an onboarding screen with outdated dates. Consider using dynamic dates or a more generic representation that doesn't show specific months/years.
| <Text className="text-xs font-bold text-gray-300">2024</Text> | |
| <Text className="text-xs font-bold text-gray-300">MAY</Text> | |
| <Text className="text-xs font-bold text-gray-300"> | |
| {new Date().getFullYear()} | |
| </Text> | |
| <Text className="text-xs font-bold text-gray-300"> | |
| {new Date().toLocaleString(undefined, { month: 'short' }).toUpperCase()} | |
| </Text> |
| className="w-full rounded-2xl bg-[#191F28] py-4 active:opacity-90" | ||
| > | ||
| <Text className="text-center text-base font-bold text-white">로그인</Text> | ||
| </Pressable> | ||
| <Pressable | ||
| onPress={() => onComplete('signUp')} | ||
| className="w-full rounded-2xl border border-gray-100 bg-white py-4 active:opacity-90" |
There was a problem hiding this comment.
The Pressable buttons lack accessibility labels. Adding accessibilityLabel props would improve the experience for users with screen readers (e.g., accessibilityLabel="로그인하기" and accessibilityLabel="회원가입하기").
| className="w-full rounded-2xl bg-[#191F28] py-4 active:opacity-90" | |
| > | |
| <Text className="text-center text-base font-bold text-white">로그인</Text> | |
| </Pressable> | |
| <Pressable | |
| onPress={() => onComplete('signUp')} | |
| className="w-full rounded-2xl border border-gray-100 bg-white py-4 active:opacity-90" | |
| className="w-full rounded-2xl bg-[#191F28] py-4 active:opacity-90" | |
| accessibilityLabel="로그인하기" | |
| > | |
| <Text className="text-center text-base font-bold text-white">로그인</Text> | |
| </Pressable> | |
| <Pressable | |
| onPress={() => onComplete('signUp')} | |
| className="w-full rounded-2xl border border-gray-100 bg-white py-4 active:opacity-90" | |
| accessibilityLabel="회원가입하기" |
| <View className="absolute top-6 left-0 right-0 z-20 flex-row items-center justify-center gap-2"> | ||
| {steps.map((step, index) => ( | ||
| <View | ||
| key={step.key} | ||
| className={`h-1 rounded-full ${ | ||
| index === activeIndex ? 'w-6 bg-[#191F28]' : 'w-1.5 bg-gray-200' | ||
| }`} | ||
| /> | ||
| ))} | ||
| </View> |
There was a problem hiding this comment.
The pagination indicator dots lack accessibility support. Consider adding an accessibilityLabel to the container view that describes the current step and total steps (e.g., "Step 1 of 5" for screen reader users).
| <View className="flex-1 items-center justify-center px-10"> | ||
| <StepText title={title} description={description} titleClassName="text-[28px] leading-[36px]" /> | ||
| <View className="items-center justify-center"> | ||
| <View className="absolute h-56 w-56" /> |
There was a problem hiding this comment.
This empty View with absolute positioning and no content serves no purpose. If it was intended as a spacing element or shadow container, it should either be implemented properly or removed to avoid confusion.
| <View className="absolute h-56 w-56" /> |
| const Step3Visual: React.FC<PhoneStepProps> = ({ title, description, phoneWidth, phoneHeight }) => { | ||
| const scrollAnim = useRef(new Animated.Value(0)).current; | ||
| const chevronAnim = useRef(new Animated.Value(0)).current; | ||
| const detailsHeight = 280; | ||
| const scrollDistance = Math.min(detailsHeight - 16, phoneHeight * 0.55); |
There was a problem hiding this comment.
The magic numbers 280 (detailsHeight), 16, and 0.55 should be extracted as named constants (e.g., DETAILS_SECTION_HEIGHT, SCROLL_OFFSET, MAX_SCROLL_PERCENTAGE) to improve code readability and maintainability.
| const Step3Visual: React.FC<PhoneStepProps> = ({ title, description, phoneWidth, phoneHeight }) => { | |
| const scrollAnim = useRef(new Animated.Value(0)).current; | |
| const chevronAnim = useRef(new Animated.Value(0)).current; | |
| const detailsHeight = 280; | |
| const scrollDistance = Math.min(detailsHeight - 16, phoneHeight * 0.55); | |
| const DETAILS_SECTION_HEIGHT = 280; | |
| const SCROLL_OFFSET = 16; | |
| const MAX_SCROLL_PERCENTAGE = 0.55; | |
| const Step3Visual: React.FC<PhoneStepProps> = ({ title, description, phoneWidth, phoneHeight }) => { | |
| const scrollAnim = useRef(new Animated.Value(0)).current; | |
| const chevronAnim = useRef(new Animated.Value(0)).current; | |
| const detailsHeight = DETAILS_SECTION_HEIGHT; | |
| const scrollDistance = Math.min(detailsHeight - SCROLL_OFFSET, phoneHeight * MAX_SCROLL_PERCENTAGE); |
| }> = ({ day = 14, fullHeight = false, className = '' }) => ( | ||
| <View className={`w-full ${fullHeight ? 'flex-1' : 'px-4 pt-4'} ${className}`}> | ||
| <View className="h-8 w-full rounded-t-sm border-b-2 border-dashed border-gray-100 bg-white items-center justify-center"> | ||
| <View className="absolute left-4 h-2 w-2 rounded-full bg-gray-200" /> | ||
| <View className="absolute right-4 h-2 w-2 rounded-full bg-gray-200" /> | ||
| </View> | ||
| <View className="w-full flex-1 rounded-b-sm bg-white p-4"> | ||
| <View className="w-full flex-row justify-between"> | ||
| <Text className="text-[12px] font-bold text-gray-300">2024</Text> | ||
| <Text className="text-[12px] font-bold text-gray-300">MAY</Text> | ||
| </View> | ||
| <View className="mt-12 w-full items-center"> | ||
| <Text | ||
| className={`text-[120px] font-bold text-center ${ | ||
| day % 7 === 0 ? 'text-red-500' : 'text-[#191F28]' | ||
| }`} | ||
| > | ||
| {day} | ||
| </Text> | ||
| </View> | ||
| <View className="mt-12 items-center gap-4"> | ||
| <View className="h-2 w-32 rounded-full bg-gray-100 " /> | ||
| <View className="h-2 w-20 rounded-full bg-gray-100" /> | ||
| </View> | ||
| <View className="mt-48 h-px w-full bg-gray-200" /> | ||
| </View> | ||
| </View> | ||
| ); |
There was a problem hiding this comment.
The hardcoded date values "2024" and "MAY" make the mockup less realistic and could confuse users if they see an onboarding screen with outdated dates. Consider using dynamic dates or a more generic representation that doesn't show specific months/years.
| }> = ({ day = 14, fullHeight = false, className = '' }) => ( | |
| <View className={`w-full ${fullHeight ? 'flex-1' : 'px-4 pt-4'} ${className}`}> | |
| <View className="h-8 w-full rounded-t-sm border-b-2 border-dashed border-gray-100 bg-white items-center justify-center"> | |
| <View className="absolute left-4 h-2 w-2 rounded-full bg-gray-200" /> | |
| <View className="absolute right-4 h-2 w-2 rounded-full bg-gray-200" /> | |
| </View> | |
| <View className="w-full flex-1 rounded-b-sm bg-white p-4"> | |
| <View className="w-full flex-row justify-between"> | |
| <Text className="text-[12px] font-bold text-gray-300">2024</Text> | |
| <Text className="text-[12px] font-bold text-gray-300">MAY</Text> | |
| </View> | |
| <View className="mt-12 w-full items-center"> | |
| <Text | |
| className={`text-[120px] font-bold text-center ${ | |
| day % 7 === 0 ? 'text-red-500' : 'text-[#191F28]' | |
| }`} | |
| > | |
| {day} | |
| </Text> | |
| </View> | |
| <View className="mt-12 items-center gap-4"> | |
| <View className="h-2 w-32 rounded-full bg-gray-100 " /> | |
| <View className="h-2 w-20 rounded-full bg-gray-100" /> | |
| </View> | |
| <View className="mt-48 h-px w-full bg-gray-200" /> | |
| </View> | |
| </View> | |
| ); | |
| }> = ({ day = 14, fullHeight = false, className = '' }) => { | |
| const now = new Date(); | |
| const currentYear = now.getFullYear(); | |
| const currentMonth = now.toLocaleString('default', { month: 'short' }).toUpperCase(); | |
| return ( | |
| <View className={`w-full ${fullHeight ? 'flex-1' : 'px-4 pt-4'} ${className}`}> | |
| <View className="h-8 w-full rounded-t-sm border-b-2 border-dashed border-gray-100 bg-white items-center justify-center"> | |
| <View className="absolute left-4 h-2 w-2 rounded-full bg-gray-200" /> | |
| <View className="absolute right-4 h-2 w-2 rounded-full bg-gray-200" /> | |
| </View> | |
| <View className="w-full flex-1 rounded-b-sm bg-white p-4"> | |
| <View className="w-full flex-row justify-between"> | |
| <Text className="text-[12px] font-bold text-gray-300">{currentYear}</Text> | |
| <Text className="text-[12px] font-bold text-gray-300">{currentMonth}</Text> | |
| </View> | |
| <View className="mt-12 w-full items-center"> | |
| <Text | |
| className={`text-[120px] font-bold text-center ${ | |
| day % 7 === 0 ? 'text-red-500' : 'text-[#191F28]' | |
| }`} | |
| > | |
| {day} | |
| </Text> | |
| </View> | |
| <View className="mt-12 items-center gap-4"> | |
| <View className="h-2 w-32 rounded-full bg-gray-100 " /> | |
| <View className="h-2 w-20 rounded-full bg-gray-100" /> | |
| </View> | |
| <View className="mt-48 h-px w-full bg-gray-200" /> | |
| </View> | |
| </View> | |
| ); | |
| }; |
| <Text className="text-xs font-bold text-gray-300">2024</Text> | ||
| <Text className="text-xs font-bold text-gray-300">MAY</Text> |
There was a problem hiding this comment.
The hardcoded date values "2024" and "MAY" make the mockup less realistic and could confuse users if they see an onboarding screen with outdated dates. Consider using dynamic dates or a more generic representation that doesn't show specific months/years.
| const Onboarding: React.FC<OnboardingProps> = ({ onComplete }) => { | ||
| const { width } = useWindowDimensions(); | ||
| const phoneWidth = Math.min(width * 0.74, 310); | ||
| const phoneHeight = Math.round(phoneWidth * 1.68); |
There was a problem hiding this comment.
The magic number 1.68 for the aspect ratio should be extracted as a named constant (e.g., PHONE_ASPECT_RATIO) to improve code readability and maintainability.
| const Onboarding: React.FC<OnboardingProps> = ({ onComplete }) => { | |
| const { width } = useWindowDimensions(); | |
| const phoneWidth = Math.min(width * 0.74, 310); | |
| const phoneHeight = Math.round(phoneWidth * 1.68); | |
| const PHONE_ASPECT_RATIO = 1.68; | |
| const Onboarding: React.FC<OnboardingProps> = ({ onComplete }) => { | |
| const { width } = useWindowDimensions(); | |
| const phoneWidth = Math.min(width * 0.74, 310); | |
| const phoneHeight = Math.round(phoneWidth * PHONE_ASPECT_RATIO); |
| <ScrollView | ||
| horizontal | ||
| pagingEnabled | ||
| showsHorizontalScrollIndicator={false} | ||
| onScroll={handleScroll} | ||
| scrollEventThrottle={16} | ||
| > |
There was a problem hiding this comment.
The ScrollView is missing a scrollEnabled prop. Consider explicitly setting scrollEnabled={true} or adding a ref to programmatically control scrolling if needed for a guided onboarding experience.
작업내용