# Implementation Plan: Multi-Profile for Roku

> **Backend**: APIs already exist (shared with Samsung). The client must add the `duriooplus-subscriber-profile-id` header to all requests when a profile is active.

---

## Phase 1: Foundation — State & Headers (3-4h)

### 1.1 Add Profile State to Redoku
**Files:**
- `src/source/ActionTypes.bs`
- `src/source/Actions.bs`
- `src/source/Reducers.bs`
- `src/source/main.bs`

**Changes:**

**ActionTypes.bs** — Add new action types:
```brightscript
function ActionTypes() as object
    return {
        ' ... existing types ...
        SET_ACTIVE_PROFILE: "SET_ACTIVE_PROFILE",
        CLEAR_ACTIVE_PROFILE: "CLEAR_ACTIVE_PROFILE",
        SET_PROFILES_LIST: "SET_PROFILES_LIST"
    }
end function
```

**Actions.bs** — Add action dispatchers:
```brightscript
sub SetActiveProfile(profileData as object)
    RedokuDispatch({
        type: ActionTypes().SET_ACTIVE_PROFILE,
        payload: profileData
    })
end sub

sub ClearActiveProfile()
    RedokuDispatch({
        type: ActionTypes().CLEAR_ACTIVE_PROFILE
    })
end sub

sub SetProfilesList(profiles as object)
    RedokuDispatch({
        type: ActionTypes().SET_PROFILES_LIST,
        payload: profiles
    })
end sub
```

**Reducers.bs** — Register profile reducers:
```brightscript
' In init/register section:
RedokuRegisterReducer("profile", Redoku_ProfileReducer)

function Redoku_ProfileReducer(state as dynamic, action as object) as object
    actionType = action.type

    if (actionType = ActionTypes().SET_ACTIVE_PROFILE)
        newState = {
            profile_id: action.payload.profile_id,
            profile_name: action.payload.profile_name,
            profile_avatar: action.payload.profile_avatar,
            profile_type: action.payload.profile_type,
            content_category_ages: action.payload.content_category_ages,
            profile_blocked_collections: action.payload.profile_blocked_collections
        }
        registryWriteJson("profile", newState)
        return newState
    else if (actionType = ActionTypes().CLEAR_ACTIVE_PROFILE)
        registryWriteJson("profile", {})
        return {}
    else if (actionType = ActionTypes().SET_PROFILES_LIST)
        return {
            ...state,
            profilesList: action.payload
        }
    end if

    return state
end function
```

**main.bs** — Add profile to initial state and load from registry:
```brightscript
' In initialState:
profile: {
    profile_id: invalid,
    profile_name: invalid,
    profile_avatar: invalid,
    profile_type: invalid,
    content_category_ages: [],
    profile_blocked_collections: [],
    profilesList: []
}

' After other registry loads:
profileFromRegistry = registryReadJson("profile")
if (profileFromRegistry <> invalid and profileFromRegistry.profile_id <> invalid)
    initialState.profile = profileFromRegistry
end if
```

### 1.2 Add Profile Header to HTTP Requests
**File:** `src/components/utils/request/RequestUtils.bs`

```brightscript
function addGlobalHeaders(headers as object) as object
    appInfo = CreateObject("roAppInfo")
    headers["duriooplus-app-version"] = appInfo.GetVersion()
    headers["duriooplus-recommendation-version"] = "v2"
    headers["duriooplus-user-device"] = "ROKU"

    ' ... existing country code logic unchanged ...

    ' Add profile ID header when multi-profile is enabled and profile is active
    if (m.global.state.profile <> invalid and m.global.state.profile.profile_id <> invalid and m.global.state.profile.profile_id <> "")
        headers["duriooplus-subscriber-profile-id"] = m.global.state.profile.profile_id
    end if

    return headers
end function
```

### 1.3 Feature Flag Toggle
**File:** `src/source/main.bs` (or a new constants file)

```brightscript
' In main.bs or a shared constants location:
m.global.addField("multiProfileEnabled", "boolean", true)
m.global.multiProfileEnabled = true
```

> **Note**: For staging/production control, tie this to the existing `envMode` or a new registry flag.

---

## Phase 2: API Service Layer (2-3h)

### 2.1 GetProfilesTask
**Files:**
- `src/components/services/profile/GetProfilesTask.xml`
- `src/components/services/profile/GetProfilesTask.bs`

```brightscript
' GetProfilesTask.bs
import "pkg:/source/roku_modules/promises/promises.brs"
import "pkg:/source/roku_modules/rokurequests/Requests.brs"

sub init()
    m.top.functionName = "execute"
end sub

function getProfiles() as dynamic
    m.promise = promises.create()
    m.top.control = "RUN"
    return m.promise
end function

sub execute()
    payload = {
        "headers": {
            "Authorization": "Bearer " + m.global.state.login.accessToken
        }
    }

    requestUtils = createObject("roSGNode", "RequestUtils")
    payload.headers = requestUtils.callFunc("addGlobalHeaders", payload.headers)

    response = rokurequests_Requests().get(
        m.global.state.baseURL + "/me/my-profiles",
        payload
    )

    if (response.statusCode > 201)
        ? "[GetProfilesTask] status not ok:", response.statusCode
        promises.resolve(invalid, m.promise)
        return
    end if

    profiles = response.json.data
    promises.resolve(profiles, m.promise)
end sub
```

### 2.2 CreateProfileTask
**Files:**
- `src/components/services/profile/CreateProfileTask.xml`
- `src/components/services/profile/CreateProfileTask.bs`

```brightscript
' CreateProfileTask.bs
sub execute()
    payload = {
        "headers": {
            "Authorization": "Bearer " + m.global.state.login.accessToken
        },
        "json": m.top.profileData
    }

    requestUtils = createObject("roSGNode", "RequestUtils")
    payload.headers = requestUtils.callFunc("addGlobalHeaders", payload.headers)

    response = rokurequests_Requests().post(
        m.global.state.baseURL + "/me/create-profile",
        payload
    )

    if (response.statusCode > 201)
        ? "[CreateProfileTask] status not ok:", response.statusCode
        promises.resolve(invalid, m.promise)
        return
    end if

    result = response.json.data
    promises.resolve(result, m.promise)
end sub
```

### 2.3 UpdateProfileTask
**Files:**
- `src/components/services/profile/UpdateProfileTask.xml`
- `src/components/services/profile/UpdateProfileTask.bs`

Similar to CreateProfileTask but uses `PATCH /me/update-profile/{profileId}`.

### 2.4 DeleteProfileTask
**Files:**
- `src/components/services/profile/DeleteProfileTask.xml`
- `src/components/services/profile/DeleteProfileTask.bs`

Uses `PATCH /me/delete-profile/{profileId}`.

### 2.5 GetProfileAvatarsTask
**Files:**
- `src/components/services/profile/GetProfileAvatarsTask.xml`
- `src/components/services/profile/GetProfileAvatarsTask.bs`

Fetches `GET /profile-avatars?isParents={bool}`.

### 2.6 GetContentAgeCategoriesTask
**Files:**
- `src/components/services/profile/GetContentAgeCategoriesTask.xml`
- `src/components/services/profile/GetContentAgeCategoriesTask.bs`

Fetches `GET /content-category-ages`.

### 2.7 SetActiveProfileTask (Optional)
**Files:**
- `src/components/services/profile/SetActiveProfileTask.xml`
- `src/components/services/profile/SetActiveProfileTask.bs`

Calls `POST /me/set-active-profile/{profileId}` — may be optional if the backend derives active profile from the header.

---

## Phase 3: ProfileSelectScreen (4-5h)

### 3.1 Screen Layout
**File:** `src/components/screens/profile-select/ProfileSelectScreen.xml`

```xml
<?xml version="1.0" encoding="UTF-8"?>
<component name="ProfileSelectScreen" extends="Group">
    <script type="text/brightscript" uri="ProfileSelectScreen.bs" />
    <interface>
        <field id="profiles" type="assocarray" value="[]" />
        <field id="isLoading" type="boolean" value="true" />
    </interface>
    <children>
        <!-- Background -->
        <Rectangle
            width="1920"
            height="1080"
            color="#0D0D0D"
        />

        <!-- Title -->
        <Label
            id="titleLabel"
            text="Who's Watching?"
            translation="[960, 120]"
            horizAlign="center"
            color="#FFFFFF"
        >
            <Font role="font" uri="pkg:/source/fonts/Nunito-Bold.ttf" size="52" />
        </Label>

        <!-- Profile Grid -->
        <PosterGrid
            id="profileGrid"
            translation="[260, 250]"
            numColumns="4"
            numRows="2"
            itemSpacing="[40, 40]"
            itemSize="[320, 380]"
        />

        <!-- Loading indicator -->
        <BusySpinner
            id="loadingSpinner"
            translation="[910, 490]"
        />

        <!-- Footer: VIP button (free users) + Parents Area -->
        <Group id="footerGroup" translation="[0, 900]">
            <RegularButton
                id="becomeVipButton"
                text="Become a VIP"
                translation="[560, 0]"
                visible="false"
            />
            <RegularButton
                id="parentsAreaButton"
                text="Parents Area"
                translation="[960, 0]"
            />
        </Group>

        <!-- Error dialog -->
        <Group id="errorGroup" visible="false">
            <Label id="errorText" text="" />
            <RegularButton id="retryButton" text="Try Again" />
        </Group>
    </children>
</component>
```

### 3.2 Screen Logic
**File:** `src/components/screens/profile-select/ProfileSelectScreen.bs`

Key behaviors:
- On `visible` → fetch profiles via `GetProfilesTask`
- On profiles loaded → build `ContentNode` for `PosterGrid` with avatar URIs + names
- On profile selected → `SetActiveProfile(data)` → navigate to HomeScreen
- Handle "Add Family" card (last item in grid, shown only when under limit)
- Footer buttons: "Become a VIP" (free users), "Parents Area"
- Profile limits derived from subscriber state

**Profile limits logic:**
```brightscript
function getProfileLimits() as object
    isFreeUser = true
    subscriberData = m.global.state.subscriber?.data
    if (subscriberData <> invalid and subscriberData.status = "ACTIVE")
        isFreeUser = false
    end if

    if (isFreeUser)
        return { maxTotal: 3, maxParents: 1, maxChildren: 2 }
    else
        return { maxTotal: 6, maxParents: 2, maxChildren: 4 }
    end if
end function
```

### 3.3 Profile Card Rendering

Since SceneGraph `PosterGrid` renders fixed-size posters from `ContentNode` data, we can use a MarkupGrid or render flat items with custom components:

**Recommended approach:** Use `MarkupGrid` with `ContentNode` items where each item's `HDPosterUrl` is the avatar, and overlay text using the grid's label capabilities or a separate info label that updates on focus.

**Alternative simpler approach:** Build a flat horizontal list using `RowList` or `MarkupList` since profile count is small (max 6).
- Each item: Poster (avatar) + Label (name) + Label (type/age badge)
- "Add Family" as last item with circular "+" design

This is simpler to implement and navigate, matching the Samsung horizontal-scrolling profile layout.

---

## Phase 4: ProfileFormScreen (5-6h)

### 4.1 AddProfileTypeScreen
**Files:**
- `src/components/screens/profile-form/AddProfileTypeScreen.xml`
- `src/components/screens/profile-form/AddProfileTypeScreen.bs`

Simple two-option screen: "Add Parent" and "Add Child".
- Back button in top-left
- Title: "Who are you adding?"
- Two large buttons (parent/child)
- Each disabled if at limit

### 4.2 ProfileFormScreen Layout
**File:** `src/components/screens/profile-form/ProfileFormScreen.xml`

```xml
<?xml version="1.0" encoding="UTF-8"?>
<component name="ProfileFormScreen" extends="Group">
    <script type="text/brightscript" uri="ProfileFormScreen.bs" />
    <interface>
        <field id="mode" type="string" value="create" />      <!-- "create" or "edit" -->
        <field id="profileType" type="string" value="children" /> <!-- "parents" or "children" -->
        <field id="profileId" type="string" value="" />        <!-- for edit mode -->
    </interface>
    <children>
        <Rectangle width="1920" height="1080" color="#0D0D0D" />

        <!-- Header: Back + Title -->
        <Group translation="[60, 60]">
            <ButtonClose id="backButton" />
            <Label id="formTitle" text="Create Profile" translation="[100, 0]"
                color="#FFFFFF">
                <Font role="font" uri="pkg:/source/fonts/Nunito-Bold.ttf" size="42" />
            </Label>
        </Group>

        <!-- Form Content -->
        <Group translation="[360, 180]">
            <!-- Name Input Row -->
            <Label text="Name" color="#CCCCCC">
                <Font role="font" uri="pkg:/source/fonts/Nunito-Regular.ttf" size="28" />
            </Label>
            <Keyboard id="nameKeyboard" translation="[0, 40]" />

            <!-- Avatar Selector Row -->
            <Label text="Choose Avatar" translation="[0, 120]" color="#CCCCCC">
                <Font role="font" uri="pkg:/source/fonts/Nunito-Regular.ttf" size="28" />
            </Label>
            <RowList
                id="avatarRowList"
                translation="[0, 160]"
                itemSize="[120, 120]"
                itemSpacing="[20, 0]"
                numRows="1"
            />

            <!-- Content Age (Children Only) -->
            <Group id="ageGroup" translation="[0, 320]" visible="false">
                <Label text="Content Age Range" color="#CCCCCC">
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Regular.ttf" size="28" />
                </Label>
                <RadioButtonList
                    id="ageRadioList"
                    translation="[0, 40]"
                    itemSize="[400, 50]"
                />
            </Group>

            <!-- Form Actions -->
            <Group translation="[0, 550]">
                <RegularButton id="saveButton" text="Save" translation="[0, 0]" />
                <RegularButton id="cancelButton" text="Cancel" translation="[300, 0]" />
                <!-- Delete Button (edit mode only) -->
                <RegularButton
                    id="deleteButton"
                    text="Delete Profile"
                    translation="[0, 80]"
                    visible="false"
                />
            </Group>
        </Group>

        <!-- Loading -->
        <BusySpinner id="loadingSpinner" translation="[910, 490]" visible="false" />
    </children>
</component>
```

### 4.3 ProfileFormScreen Logic
**File:** `src/components/screens/profile-form/ProfileFormScreen.bs`

Key behaviors:
- On `visible` → determine mode from props (`mode`, `profileType`, `profileId`)
- Fetch avatars via `GetProfileAvatarsTask` with `isParents` filter
- Fetch content age categories if child profile via `GetContentAgeCategoriesTask`
- Name input: Use Roku `roKeyboardScreen` dialog or on-screen keyboard
- Avatar selector: `RowList` of Poster nodes, highlight selected
- Content age: `RadioButtonList` or focus-managed list
- Save: Validate fields → call `CreateProfileTask` or `UpdateProfileTask` → on success, navigate back
- Delete (edit mode only): Confirm dialog → `DeleteProfileTask` → navigate to ProfileSelectScreen

### 4.4 Keyboard Input for Name
Roku's SceneGraph doesn't have a built-in TextEditBox like mobile. Use `roKeyboardScreen` dialog:

```brightscript
sub showNameKeyboard()
    keyboard = CreateObject("roKeyboardScreen")
    keyboard.SetTitle("Enter Profile Name")
    keyboard.SetText(m.profileName)
    keyboard.SetMaxLength(30)
    keyboard.SetDisplayText("Profile name")
    keyboard.AddButton(1, "Done")
    keyboard.AddButton(2, "Cancel")
    keyboard.Show()

    if (keyboard.IsButtonPressed())
        if (keyboard.GetButton() = 1)
            m.profileName = keyboard.GetText()
            m.nameLabel.text = m.profileName
        end if
    end if
end sub
```

---

## Phase 5: SplashScreen Integration (1-2h)

### 5.1 Conditional Profile Selection Flow
**File:** `src/components/screens/splash/SplashScreen.bs`

Modify `navigateToHomeScreen` (or add a new routing step):

```brightscript
sub navigateAfterSubscriberCheck(res as object)
    if (m.global.multiProfileEnabled = true)
        ' Check if profile is already selected
        currentProfile = m.global.state.profile
        if (currentProfile <> invalid and currentProfile.profile_id <> invalid and currentProfile.profile_id <> "")
            ' Profile already selected, go straight to Home
            m.global.navigation.callFunc("resetToScreen", "HomeScreen", { inited: true })
        else
            ' No profile selected, show profile selection
            m.global.navigation.callFunc("resetToScreen", "ProfileSelectScreen")
        end if
    else
        ' Multi-profile disabled, go to Home as before
        m.global.navigation.callFunc("resetToScreen", "HomeScreen", { inited: true })
    end if
end sub
```

Replace `navigateToHomeScreen` callback with `navigateAfterSubscriberCheck`.

### 5.2 Guest User Exclusion
Guest users skip profile selection entirely:

```brightscript
' In getGuestToken success callback:
if (loginData <> invalid)
    m.token = loginData.token.access_token
    SetGuestToken(loginData)
    getHomeButtonsData()
    ' navigateToHomeScreen is called from getHomeButtonsData callback
    ' Profile selection is skipped for guests
end if
```

---

## Phase 6: SettingScreen & SideMenu (1-2h)

### 6.1 Add "Switch Profile" to Settings
**File:** `src/components/screens/setting/SettingScreen.xml`

Add a new row or button:
```xml
<LabelList id="settingsList" ...>
    <!-- existing items -->
    <ContentNode id="switchProfile" />
</LabelList>
```

**File:** `src/components/screens/setting/SettingScreen.bs`

```brightscript
' In item selection handler:
if (selectedItem.id = "switchProfile")
    m.global.navigation.callFunc("resetToScreen", "ProfileSelectScreen")
end if
```

### 6.2 SideMenu Updates
**File:** `src/source/SideMenu.bs`

Optionally add profile-related menu items (e.g., "Switch Profile" when logged in).

---

## Phase 7: StackNavigator Registration (0.5h)

### 7.1 Register New Screens
**File:** `src/components/utils/navigation/StackNavigator.bs`

```brightscript
registerScreen("ProfileSelectScreen")
registerScreen("ProfileFormScreen")
registerScreen("AddProfileTypeScreen")
```

---

## Phase 8: Content Filtering Integration (1-2h)

### 8.1 Profile-Aware Recommender
The `GetHomeRecommendationTask` and `GetHomeSectionTask` already use `addGlobalHeaders()`, which now includes the profile ID. No additional changes needed — the backend handles content filtering based on the header.

### 8.2 Profile-Aware Content Queries
If any API calls pass `content_category_ages` explicitly (matching Samsung's `getContentByProfile`), the header approach handles this implicitly. For calls that need explicit age filtering in the URL:

```brightscript
' Example: when fetching content with age filter
if (m.global.state.profile <> invalid and m.global.state.profile.content_category_ages.Count() > 0)
    for each age in m.global.state.profile.content_category_ages
        url = url + "content_category_ages=" + age + "&"
    end for
end if
```

This is handled by the backend via the profile header — no URL changes needed.

---

## Phase 9: Asset Generation (0.5h)

### 9.1 Required Assets
| Asset | Description | Source |
|-------|-------------|--------|
| Profile avatars | Circle-cropped PNGs for profile selection | Sampled from API response, or bundled default avatars |
| Parent badge icon | Shield SVG/PNG for parent badge | Extract from Samsung assets |
| Add profile "+" icon | Circular add button with "+" | Generate or use simple Label |
| VIP button icon | Crown/diamond icon | Extract from Samsung assets |
| Default avatar placeholder | Fallback when avatar URL fails | Solid color circle with first letter |

---

## Phase 10: Analytics (1h)

### 10.1 Mixpanel Events
**File:** Uses existing `MixpanelTrack()` from `src/source/MixpanelTrack.bs`

Events to add at appropriate trigger points:
- `profile_selection_viewed` — in ProfileSelectScreen `onVisibleChange`
- `profile_selected` — when user selects a profile
- `add_profile_tapped` — when "Add Family" is tapped
- `add_profile_type_selected` — in AddProfileTypeScreen
- `profile_form_viewed` — in ProfileFormScreen init
- `profile_created` — on successful profile creation
- `profile_updated` — on successful profile update
- `profile_deleted` — on successful profile deletion
- `setting_switch_profile_clicked` — in SettingScreen handler

---

## Phase 11: Testing Checklist (2-3h)

### Manual Test Cases

1. **Profile Selection — First Time**
   - [ ] Fresh install/login → ProfileSelectScreen shown
   - [ ] Profiles fetched and displayed correctly
   - [ ] Select profile → Home loads → content is profile-scoped

2. **Profile Selection — Returning User**
   - [ ] Cold start with saved profile → splash → Home directly
   - [ ] Profile state survives force-quit

3. **Add Child Profile**
   - [ ] Tap "Add Family" → AddProfileTypeScreen
   - [ ] Select "Child" → ProfileFormScreen
   - [ ] Enter name, select avatar, select age range
   - [ ] Save → returns to ProfileSelectScreen → new profile visible

4. **Add Parent Profile**
   - [ ] Same flow, parent type
   - [ ] No age range selector shown
   - [ ] Limited to max parents count

5. **Edit Profile**
   - [ ] Edit button on existing profile → ProfileFormScreen (edit mode)
   - [ ] Pre-filled with existing data
   - [ ] Modify name → Save → updated in list

6. **Delete Profile**
   - [ ] Edit mode → "Delete Profile" button → confirm dialog
   - [ ] Profile removed from list

7. **Profile Limits — Free User**
   - [ ] "Add Family" disabled at 3 profiles
   - [ ] Parent option disabled at 1 parent

8. **Profile Limits — VIP**
   - [ ] "Add Family" disabled at 6 profiles
   - [ ] Parent option disabled at 2 parents

9. **Switch Profile from Settings**
   - [ ] Settings → "Switch Profile" → ProfileSelectScreen
   - [ ] Select different profile → Home with different filtered content

10. **Guest User**
    - [ ] Logout → guest flow → no profile selection → Home directly

11. **API Header Verification**
    - [ ] Profile selected → check API calls include `duriooplus-subscriber-profile-id` header
    - [ ] No profile selected → header not sent

12. **Error Handling**
    - [ ] No network → profile fetch fails → error message + retry
    - [ ] Profile creation fails → field-level error message
    - [ ] Avatar images fail → fallback placeholder shown

---

## Effort Estimate

| Phase | Tasks | Est. Hours |
|-------|-------|------------|
| 1. Foundation | State + headers + feature flag | 3-4h |
| 2. API Layer | 7 task nodes (profiles, CRUD, avatars, ages) | 2-3h |
| 3. ProfileSelectScreen | Grid layout + selection logic | 4-5h |
| 4. ProfileFormScreen | Create/edit form + keyboard + avatars | 5-6h |
| 5. Splash Integration | Conditional routing | 1-2h |
| 6. Settings + SideMenu | Switch profile entry point | 1-2h |
| 7. StackNavigator | Screen registration | 0.5h |
| 8. Content Filtering | Profile-aware content queries | 1-2h |
| 9. Assets | Avatars, badges, icons | 0.5h |
| 10. Analytics | Mixpanel events | 1h |
| 11. Testing | Manual test cases | 2-3h |
| **Total** | | **~21-30h** |

---

## Dependencies & Prerequisites

- [ ] Backend APIs `/me/my-profiles`, `/me/create-profile`, `/me/update-profile/{id}`, `/me/delete-profile/{id}` are live (shared with Samsung)
- [ ] `/profile-avatars` and `/content-category-ages` endpoints are live
- [ ] Backend honors `duriooplus-subscriber-profile-id` header for content filtering
- [ ] `RegistryUtils.bs` and `registryWriteJson`/`registryReadJson` are stable
- [ ] `RequestUtils.addGlobalHeaders()` is called by all API tasks

---

## References

- Samsung Profile Page: `/Users/urip/durioo/duriooplus-web-samsung/src/pages/profile/index.tsx`
- Samsung Profile Form: `/Users/urip/durioo/duriooplus-web-samsung/src/pages/profile/form/index.tsx`
- Samsung Add Family: `/Users/urip/durioo/duriooplus-web-samsung/src/pages/profile/add-family/index.tsx`
- Samsung Profile Services: `/Users/urip/durioo/duriooplus-web-samsung/src/services/profile/`
- Samsung Profile Reducer: `/Users/urip/durioo/duriooplus-web-samsung/src/stores/reducers/profileReducer/`
- Samsung Feature Flags: `/Users/urip/durioo/duriooplus-web-samsung/src/configs/featureFlags.ts`
- Samsung Constants (limits): `/Users/urip/durioo/duriooplus-web-samsung/src/configs/constants.ts`
- Samsung HTTP Interceptor: `/Users/urip/durioo/duriooplus-web-samsung/src/utils/httpClient.ts`
- Roku SplashScreen: `src/components/screens/splash/SplashScreen.bs`
- Roku StackNavigator: `src/components/utils/navigation/StackNavigator.bs`
- Roku RequestUtils: `src/components/utils/request/RequestUtils.bs`
- Roku main.bs: `src/source/main.bs`
- Roku Actions/Reducers: `src/source/Actions.bs`, `src/source/Reducers.bs`, `src/source/ActionTypes.bs`
- Roku SettingScreen: `src/components/screens/setting/`
