CAMDOM - Onboarding Flow
Complete onboarding walkthrough: HowTo instructions, Bluetooth permission request, platform-specific behavior, and the redirect-to-main-app completion path
Overview
The onboarding flow is the entry point for new users. It is a 2-step process stored at app/onboarding.tsx. Once completed, the flag isOnboardingComplete is set to true in MMKV storage, and all subsequent app launches redirect directly to the main screen.
Route Behavior
if (isOnboardingComplete) return <Redirect href={"/"} />;
If onboarding is already complete, the component immediately redirects to the main index route. This means the onboarding screen is only visible on first launch (or after clearing storage).
Step 0: How To Use a Camdom
What the User Sees
- Title: "how to use a camdom" (localized)
- Separator: Full-width horizontal line via
<Separator /> - Content:
<HowTo />component centered in the remaining space
The <HowTo /> component (from the Menu system) renders:
- Instruction text: "install the app on all devices" (localized, with
\nfor line breaks) - Bluetooth notice: "Bluetooth must be on" in a styled sub-container
<CamdomInstruction />-- a visual step-by-step guide showing how the app works
Interaction
The user reads the instructions and presses the "continue" button at the bottom.
iOS Behavior
On iOS, pressing "continue" at step 0 immediately completes onboarding:
if (onboardingStep === 0 && Platform.OS === "ios")
setOnboardingIsComplete(true);
iOS does not require explicit Bluetooth permissions at the app level (BLE permissions are handled via Info.plist descriptions). The user proceeds directly to the main app.
Android Behavior
On Android, pressing "continue" at step 0 advances to step 1 (permissions). The onboarding is NOT completed yet.
Step 1: Permission Request
What the User Sees
- Title: "Give Permissions" (localized as
ask_permission) - Separator: Full-width horizontal line
- Description: "Camdom uses bluetooth to make connections, please allow it when prompted" (localized as
ask_permission_text) - Icon: FontAwesome Bluetooth icon (
bluetooth, size 64, black) - Button: "continue" (localized)
The Step and StepStep Helper Components
The onboarding file defines two local helper components:
Step -- Conditional rendering wrapper:
const Step: FC<PropsWithChildren<{ visibleOnStep: number; step: number }>>
Only renders its children when step >= visibleOnStep. Used to progressively reveal content as the user advances.
StepStep -- A numbered instruction row:
const StepStep: FC<{
step?: number;
text: string;
image?: ImageSourcePropType;
shouldRecenter1?: boolean;
hideNumberOnEnd?: boolean;
}>
Renders a row with step number on both sides of the text, and an optional image below. Currently only text is used in the actual onboarding (no step number or image passed).
Permission Flow
When the user presses "continue" on step 1:
if (onboardingStep >= 1)
requestPermissions(async (permissionGranted) => {
if (permissionGranted) {
setOnboardingIsComplete(true);
}
});
The requestPermissions function (from utils/requestPermissions.ts) is called. This triggers the platform-specific permission dialog:
- iOS: Immediately calls
cb(true)-- no dialog needed - Android API < 31: Shows
ACCESS_FINE_LOCATIONpermission dialog - Android API >= 31: Shows
BLUETOOTH_SCAN,BLUETOOTH_CONNECT,BLUETOOTH_ADVERTISEpermission dialogs
If permissions are granted, setOnboardingIsComplete(true) is called, which:
- Persists
trueto MMKV underStorageKeys.isOnboardingComplete - On next render, the
<Redirect href={"/"} />triggers, sending the user to the main app
If permissions are denied, the onboarding does NOT complete. The user remains on step 1 and can try again by pressing "continue" again.
Important: Step Increment Regardless of Permission Result
setOnboardingStep((val) => val + 1);
scrollViewRef.current?.scrollToEnd({ animated: true });
The step counter increments and the scroll view scrolls to the end regardless of whether permissions were granted. However, isOnboardingComplete is only set inside the permission callback on success. This means if the user denies permissions, the step advances visually but onboarding remains incomplete.
Visual Layout
Scroll-Based Layout
The onboarding uses a ScrollView with a ref for programmatic scrolling. The "continue" button is positioned absolutely at the bottom of the screen.
Responsive Design
Uses react-native-unistyles for responsive breakpoints:
marginTop: UnistylesRuntime.breakpoint === "sm"
? UnistylesRuntime.insets.top + 20
: 80,
marginBottom: UnistylesRuntime.breakpoint === "sm"
? UnistylesRuntime.insets.bottom + 5
: 10,
smbreakpoint (small screens): Uses safe area insets + smaller margins- Larger screens: Fixed 80px top margin, 10px bottom margin
Background
<NoisyBlurredBackground /> is rendered behind all content (default props -- no balls shown, just the Skia canvas background color).
Styling
All styles are defined inline in the component via StyleSheet.create:
| Style | Properties |
|---|---|
title | BasicSans-Bold, 16px, uppercase, primary color, left-aligned |
subtitle | BasicSans-Bold, 14px, uppercase, primary color |
subSubtitle | BasicSans-Bold, 11px, uppercase, primary color, center-aligned |
continueButtonContainer | Absolute bottom, full width, 15px horizontal padding |
continueButton | Full width, primary color border top/bottom (1px), centered content, 12px vertical padding |
continueButtonText | BasicSans-SemiBold, 14px, uppercase, primary color |
Button Style
The "continue" button uses a distinctive double-border style: only borderTopWidth and borderBottomWidth are set (1px each in primary color), with no left/right borders. This creates a horizontal-line-framed button aesthetic consistent with the CAMDOM design language.
Haptic Feedback
Every "continue" press triggers Haptics.selectionAsync() for tactile feedback, using the lightest haptic style.
State Machine Summary
Launch
|
+-- isOnboardingComplete? --> Redirect to "/" (main app)
|
+-- Step 0: HowTo instructions
| |
| +-- [iOS] "continue" --> setOnboardingComplete(true) --> Main app
| +-- [Android] "continue" --> Step 1
|
+-- Step 1: Permission request
|
+-- "continue" --> requestPermissions()
|
+-- granted --> setOnboardingComplete(true) --> Main app
+-- denied --> Stay on step 1 (can retry)
Key Observations
- iOS skips Bluetooth permission dialogs entirely -- the permission flow on iOS is a no-op, making the onboarding a single-tap process
- Android requires one "continue" press before the permission dialog -- the user sees the explanation screen first, then the system dialog on the next press
- The onboarding does not gate on permission denial -- the step advances regardless, but the completion flag is only set on grant. This means a user who denies permissions can still see the step advance, but on next app launch they will be redirected back to onboarding (since
isOnboardingCompleteis stillfalse) - The HowTo component is shared -- the same
<HowTo />used in the menu system is embedded in onboarding step 0, ensuring consistent instructions across both entry points - The
ScrollViewref is used forscrollToEndon step transition, but the actual content may not overflow -- this suggests the scroll is used primarily for the visual animation of content sliding up - No skip mechanism -- there is no "skip" button. The user must complete the flow or force-quit the app