Skip to Content
Mobile AppOnboarding Flow

Onboarding Flow

The Loop Health mobile app features a streamlined 5-screen onboarding experience designed to capture user intent, set up essential permissions, and configure biometric security — all while maintaining state persistence for interrupted sessions.

Overview

The onboarding flow guides new users through essential setup steps:

  1. Welcome — Introduction and value proposition
  2. Intent Tracking — Health goals and experience preferences
  3. Permissions — Push notification authorization
  4. Biometrics — Face ID/Touch ID setup
  5. Completion — Success confirmation and next steps

Flow Architecture

Screen Details

1. Welcome Screen (app/(onboarding)/welcome.tsx)

Purpose: Introduce Loop Health value proposition and set expectations.

Content:

  • Loop Health logo and branding
  • Key benefits: “Optimize your health with AI guidance”
  • Privacy assurance: “Your data stays secure”
  • Continue button to proceed

State Management:

interface OnboardingState { currentStep: 'welcome' | 'intent' | 'permissions' | 'biometrics' | 'complete'; completedSteps: string[]; userIntent: { primaryGoals: string[]; experienceLevel: 'beginner' | 'intermediate' | 'advanced'; notificationPreferences: NotificationPrefs; }; }

2. Intent Tracking (app/(onboarding)/intent.tsx)

Purpose: Capture user health goals and experience level for personalization.

Data Collection:

  • Primary Goals (multi-select):

    • Weight management
    • Muscle building
    • Recovery optimization
    • Sleep improvement
    • Energy enhancement
    • Longevity focus
  • Experience Level (single-select):

    • Beginner: “New to peptides and optimization”
    • Intermediate: “Some experience with health protocols”
    • Advanced: “Experienced with peptides and biohacking”
  • Communication Preferences:

    • Notification frequency (daily, weekly, as-needed)
    • Preferred reminder times
    • Content depth (basic, detailed, technical)

Implementation:

const [selectedGoals, setSelectedGoals] = useState<string[]>([]); const [experienceLevel, setExperienceLevel] = useState<string>(''); const [notificationPrefs, setNotificationPrefs] = useState<NotificationPrefs>(); const handleContinue = async () => { await saveOnboardingProgress({ currentStep: 'permissions', userIntent: { primaryGoals: selectedGoals, experienceLevel, notificationPreferences: notificationPrefs, }, }); router.push('/(onboarding)/permissions'); };

3. Push Notification Permissions (app/(onboarding)/permissions.tsx)

Purpose: Request push notification authorization with clear value explanation.

Permission Types:

  • Alerts — Low supply warnings, dose reminders
  • Updates — Lab results, protocol changes
  • Insights — Weekly progress, health tips
  • Milestones — Achievement celebrations

User Experience:

  • Clear explanation of each notification type
  • Option to customize frequency immediately
  • “Allow All” vs “Customize” vs “Skip for Now” options
  • Visual examples of notification content

Implementation:

import * as Notifications from 'expo-notifications'; const requestPermissions = async () => { const { status } = await Notifications.requestPermissionsAsync(); if (status === 'granted') { // Register for push notifications with Knock await registerWithKnock(userId); // Save preference await saveOnboardingProgress({ currentStep: 'biometrics', notificationsEnabled: true, }); } else { // Continue without notifications await saveOnboardingProgress({ currentStep: 'biometrics', notificationsEnabled: false, }); } router.push('/(onboarding)/biometrics'); };

4. Biometric Authentication Setup (app/(onboarding)/biometrics.tsx)

Purpose: Configure Face ID/Touch ID for secure app access.

Security Options:

  • Biometric Lock — Face ID, Touch ID, or fingerprint
  • PIN Fallback — 4-digit backup code
  • Auto-Lock Timeout — 1min, 5min, 15min, or immediate
  • Skip Option — Continue without biometric security

Device Compatibility:

  • iOS: Face ID, Touch ID detection
  • Android: Fingerprint, face unlock detection
  • Fallback: PIN-only mode for unsupported devices

Implementation:

import * as LocalAuthentication from 'expo-local-authentication'; const setupBiometrics = async () => { // Check device capability const hasHardware = await LocalAuthentication.hasHardwareAsync(); const isEnrolled = await LocalAuthentication.isEnrolledAsync(); if (hasHardware && isEnrolled) { const supportedTypes = await LocalAuthentication.supportedAuthenticationTypesAsync(); // Show appropriate UI for Face ID vs Touch ID vs Fingerprint const authType = supportedTypes.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) ? 'Face ID' : 'Touch ID'; // Prompt user to enable const result = await LocalAuthentication.authenticateAsync({ promptMessage: `Enable ${authType} for secure access`, cancelLabel: 'Skip', }); if (result.success) { await saveBiometricPreference(true); } } router.push('/(onboarding)/complete'); };

5. Completion Screen (app/(onboarding)/complete.tsx)

Purpose: Celebrate successful setup and guide to main app features.

Content:

  • Success animation/checkmark
  • Personalized welcome message based on captured intent
  • Quick tour of main features (Luna chat, supply tracking)
  • “Get Started” button to enter main app

Next Steps:

  • Mark onboarding as complete in AsyncStorage
  • Initialize user preferences in app state
  • Navigate to main dashboard
  • Trigger welcome notification (if enabled)

State Persistence

AsyncStorage Schema

interface OnboardingProgress { isComplete: boolean; currentStep: OnboardingStep; completedSteps: OnboardingStep[]; userIntent: UserIntent; preferences: { notificationsEnabled: boolean; biometricsEnabled: boolean; autoLockTimeout: number; }; startedAt: string; completedAt?: string; }

Resume Logic

const checkOnboardingStatus = async () => { const progress = await AsyncStorage.getItem('@onboarding_progress'); if (!progress) { // First time user router.replace('/(onboarding)/welcome'); return; } const parsed = JSON.parse(progress) as OnboardingProgress; if (parsed.isComplete) { // Onboarding finished, go to main app router.replace('/(tabs)'); return; } // Resume from last step router.replace(`/(onboarding)/${parsed.currentStep}`); };

Analytics & Tracking

Onboarding Metrics

Track key funnel metrics for optimization:

// Step completion rates analytics.track('onboarding_step_completed', { step: 'welcome', userId, timestamp: new Date().toISOString(), }); // Drop-off points analytics.track('onboarding_abandoned', { step: 'permissions', userId, timeSpent: 45, // seconds }); // Final completion analytics.track('onboarding_completed', { userId, totalTime: 180, // seconds selectedGoals: ['weight-management', 'sleep'], experienceLevel: 'beginner', });

Intent Analysis

Use captured intent data for:

  • Personalized Luna responses — Tailor AI advice to user goals
  • Custom notification scheduling — Align with experience level
  • Feature recommendations — Highlight relevant app sections
  • Content curation — Show appropriate educational materials

Accessibility

Screen Reader Support

<TouchableOpacity accessibilityLabel="Continue to set up notifications" accessibilityHint="Proceeds to the next step of onboarding" accessibilityRole="button" > <Text>Continue</Text> </TouchableOpacity>

Visual Accessibility

  • High contrast mode support
  • Dynamic font sizing
  • Color-blind friendly indicators
  • Reduced motion options

Testing

Unit Tests

describe('Onboarding Flow', () => { it('should save progress after each step', async () => { const { getByText } = render(<WelcomeScreen />); fireEvent.press(getByText('Continue')); const progress = await AsyncStorage.getItem('@onboarding_progress'); expect(JSON.parse(progress).currentStep).toBe('intent'); }); it('should resume from interrupted step', async () => { await AsyncStorage.setItem('@onboarding_progress', JSON.stringify({ currentStep: 'permissions', completedSteps: ['welcome', 'intent'], })); const { getByText } = render(<OnboardingNavigator />); expect(getByText('Allow Notifications')).toBeTruthy(); }); });

E2E Tests

describe('Complete Onboarding Flow', () => { it('should complete full onboarding successfully', async () => { await device.launchApp({ newInstance: true }); // Welcome screen await expect(element(by.text('Welcome to Loop Health'))).toBeVisible(); await element(by.text('Get Started')).tap(); // Intent tracking await element(by.text('Weight management')).tap(); await element(by.text('Beginner')).tap(); await element(by.text('Continue')).tap(); // Permissions await element(by.text('Allow All')).tap(); // Biometrics await element(by.text('Enable Face ID')).tap(); // Completion await expect(element(by.text('You\'re all set!'))).toBeVisible(); await element(by.text('Enter App')).tap(); // Should be in main app await expect(element(by.text('Dashboard'))).toBeVisible(); }); });

See Also