---
type: reference
title: "CAMDOM - Onboarding Flow"
description: "Complete onboarding walkthrough: HowTo instructions, Bluetooth permission request, platform-specific behavior, and the redirect-to-main-app completion path"
tags: [camdom, onboarding, permissions, ux-flow]
timestamp: "2026-07-20"
---

## 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

```tsx
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:
1. Instruction text: "install the app on all devices" (localized, with `\n` for line breaks)
2. Bluetooth notice: "Bluetooth must be on" in a styled sub-container
3. `<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**:

```ts
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:
```tsx
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:
```tsx
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:

```ts
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_LOCATION` permission dialog
- **Android API >= 31**: Shows `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `BLUETOOTH_ADVERTISE` permission dialogs

If permissions are granted, `setOnboardingIsComplete(true)` is called, which:
1. Persists `true` to MMKV under `StorageKeys.isOnboardingComplete`
2. 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

```ts
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:

```tsx
marginTop: UnistylesRuntime.breakpoint === "sm"
  ? UnistylesRuntime.insets.top + 20
  : 80,
marginBottom: UnistylesRuntime.breakpoint === "sm"
  ? UnistylesRuntime.insets.bottom + 5
  : 10,
```

- **`sm` breakpoint** (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

1. **iOS skips Bluetooth permission dialogs entirely** -- the permission flow on iOS is a no-op, making the onboarding a single-tap process
2. **Android requires one "continue" press before the permission dialog** -- the user sees the explanation screen first, then the system dialog on the next press
3. **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 `isOnboardingComplete` is still `false`)
4. **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
5. **The `ScrollView` ref is used** for `scrollToEnd` on step transition, but the actual content may not overflow -- this suggests the scroll is used primarily for the visual animation of content sliding up
6. **No skip mechanism** -- there is no "skip" button. The user must complete the flow or force-quit the app
