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:
- Welcome — Introduction and value proposition
- Intent Tracking — Health goals and experience preferences
- Permissions — Push notification authorization
- Biometrics — Face ID/Touch ID setup
- 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
- Push Notifications — Knock integration and notification workflows
- Luna Chat Interface — AI chat with personalized responses
- Expo Local Authentication
- Expo Notifications