# Implementation Plan: QR Login for Roku

> **Backend endpoint designed**: See `docs/qr-login/plan-polling.md` for the backend plan. The `POST /auth/check-code-status` endpoint returns `{ status: "pending" }`, `{ user, token }`, or `{ expired: true }`.

---

## Phase 1: Foundation (API Layer)

### 1.1 Create `RequestCodeTask`
**Files:**
- `src/components/services/login/RequestCodeTask.xml`
- `src/components/services/login/RequestCodeTask.bs`

**Purpose:** POSTs to `/auth/request-code` to obtain a verification code.

**Key behaviors:**
- Accepts `device_id`, `device_brand` ("Roku"), `device_model` as inputs
- Returns `{ verificationCode: "12345678" }` or `invalid` on failure
- Extracts code from various response shapes (same logic as Samsung's `extractVerificationCodeFromBody`)
- Reuses existing `rokurequests_Requests` pattern

**Snippet:**
```brightscript
' src/components/services/login/RequestCodeTask.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 requestCode() as dynamic
    m.promise = promises.create()
    m.top.control = "RUN"
    return m.promise
end function

sub execute()
    payload = {
        "device_id": m.global.state.deviceId,
        "device_brand": "Roku",
        "device_model": m.global.state.deviceModel
    }
    
    response = rokurequests_Requests().post(
        m.global.state.baseURL + "/auth/request-code",
        { "json": payload }
    )
    
    if (response.statusCode > 201)
        ? "[RequestCodeTask] status not ok:", response.statusCode
        promises.resolve(invalid, m.promise)
        return
    end if
    
    data = response.json.data
    code = extractVerificationCode(data)
    
    if (code = "")
        ? "[RequestCodeTask] no code in response"
        promises.resolve(invalid, m.promise)
        return
    end if
    
    promises.resolve({ verificationCode: code }, m.promise)
end sub

function extractVerificationCode(body as dynamic) as string
    if (body = invalid)
        return ""
    end if
    if (type(body) = "roString" or type(body) = "String")
        return body.Trim()
    end if
    if (type(body) = "roAssociativeArray")
        keys = ["data", "code", "verification_code", "verificationCode", "verification"]
        for each key in keys
            if (body.DoesExist(key))
                val = body[key]
                if (type(val) = "roString" or type(val) = "String")
                    return val.Trim()
                end if
            end if
        end for
    end if
    return ""
end function
```

```xml
<!-- src/components/services/login/RequestCodeTask.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<component name="RequestCodeTask" extends="Task">
    <script type="text/brightscript" uri="RequestCodeTask.bs" />
</component>
```

---

### 1.2 Create `QRCodePollingTask`
**Files:**
- `src/components/services/login/QRCodePollingTask.xml`
- `src/components/services/login/QRCodePollingTask.bs`

**Purpose:** Polls `/auth/check-code-status` every 3 seconds until the user completes phone auth, the code expires, or 10 minutes pass.

**Key behaviors:**
- Polls with exponential backoff (3s interval, max ~10 minutes)
- On success: returns `{ user, token }` to trigger login
- On expired: returns `{ expired: true }` to trigger code refresh
- On error/message: returns `{ message: "..." }` to display to user
- Cleanup: uses a `shouldStop` field on the node to allow external cancellation

**Snippet:**
```brightscript
' src/components/services/login/QRCodePollingTask.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 startPolling() as dynamic
    m.promise = promises.create()
    m.top.control = "RUN"
    return m.promise
end function

sub execute()
    pollInterval = 3000  ' 3 seconds in ms
    maxAttempts = 200      ' ~10 min
    attempts = 0
    
    while (attempts < maxAttempts)
        response = rokurequests_Requests().post(
            m.global.state.baseURL + "/auth/check-code-status",
            { "json": { "device_id": m.global.state.deviceId } }
        )
        
        if (response <> invalid and response.statusCode <= 201)
            resJson = response.json
            if (resJson <> invalid)
                data = resJson.data
                if (data <> invalid)
                    if (data.user <> invalid and data.token <> invalid)
                        ? "[QRCodePollingTask] login detected"
                        promises.resolve(data, m.promise)
                        return
                    else if (data.expired = true)
                        ? "[QRCodePollingTask] code expired"
                        promises.resolve({ expired: true }, m.promise)
                        return
                    else if (data.message <> invalid)
                        ? "[QRCodePollingTask] server message:", data.message
                        promises.resolve({ message: data.message }, m.promise)
                        return
                    end if
                end if
            end if
        end if
        
        attempts++
        
        ' Sleep in 100ms increments so we can be interrupted
        for i = 1 to 30
            if (m.top.shouldStop = true)
                ? "[QRCodePollingTask] stopped externally"
                promises.resolve(invalid, m.promise)
                return
            end if
            sleep(100)
        end for
    end while
    
    ' Timeout
    ? "[QRCodePollingTask] timeout"
    promises.resolve({ expired: true }, m.promise)
end sub
```

```xml
<!-- src/components/services/login/QRCodePollingTask.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<component name="QRCodePollingTask" extends="Task">
    <script type="text/brightscript" uri="QRCodePollingTask.bs" />
    <interface>
        <field id="shouldStop" type="boolean" value="false" />
    </interface>
</component>
```

---

### 1.3 Create `QrUtils.bs`
**File:** `src/components/utils/qr/QrUtils.bs`

**Purpose:** Shared utility functions for QR code formatting and URL building.

**Snippet:**
```brightscript
' src/components/utils/qr/QrUtils.bs

function stripVerificationCode(code as string) as string
    if (code = invalid or code = "")
        return ""
    end if
    ' Remove spaces and hyphens
    result = code.Trim()
    result = result.Replace(" ", "")
    result = result.Replace("-", "")
    return result
end function

function formatVerificationCode(code as string) as string
    s = stripVerificationCode(code)
    if (s.Len() <= 4)
        return s
    end if
    return s.Left(4) + "-" + s.Mid(4)
end function

function simpleUrlEncode(text as string) as string
    result = text
    result = result.Replace("%", "%25")
    result = result.Replace("&", "%26")
    result = result.Replace(" ", "%20")
    result = result.Replace("+", "%2B")
    result = result.Replace("#", "%23")
    result = result.Replace("?", "%3F")
    result = result.Replace("=", "%3D")
    result = result.Replace("@", "%40")
    result = result.Replace("/", "%2F")
    result = result.Replace(":", "%3A")
    return result
end function

function buildTvLoginOneLinkUrl(verificationCode as string, deviceModel as string) as string
    code = stripVerificationCode(verificationCode)
    brand = "Roku"
    
    encodedModel = simpleUrlEncode(deviceModel)
    encodedWebDp = simpleUrlEncode("https://duriooplus.com/tv")
    
    url = "https://durioo.onelink.me/IbX3?"
    url = url + "pid=TV_Login"
    url = url + "&c=TV_Login"
    url = url + "&deep_link_value=tv_verification"
    url = url + "&verification_code=" + code
    url = url + "&device_brand=" + brand
    url = url + "&device_model=" + encodedModel
    url = url + "&af_web_dp=" + encodedWebDp
    
    return url
end function

function buildQrImageUrl(oneLinkUrl as string, size = 400 as integer) as string
    encoded = simpleUrlEncode(oneLinkUrl)
    sizeStr = size.ToStr()
    return "https://api.qrserver.com/v1/create-qr-code/?size=" + sizeStr + "x" + sizeStr + "&data=" + encoded
end function
```

---

## Phase 2: UI — LoginQRCodeScreen

### 2.1 Create Screen Component

**Files:**
- `src/components/screens/login-qrcode/LoginQRCodeScreen.xml`
- `src/components/screens/login-qrcode/LoginQRCodeScreen.bs`

**Layout (XML):**
- Background: dark color (#0D0D0D or similar)
- Close button (top-right)
- QR code `Poster` (center, ~400×400)
- Formatted verification code text (large, white, monospace-like)
- Instruction text: "Scan with your phone or go to duriooplus.com/tv" + "Enter the code"
- Loading indicator (while fetching code)
- Error state: error text + "Try again" button

**Logic (.bs):**
- On `visible` change → call `RequestCodeTask`
- On code received → build QR URL → set poster uri → start `QRCodePollingTask`
- On poll success → call `Login()` + `MixpanelLoginSuccess()` with `auth_type: "qr"`
- On poll expired → retry code fetch
- On poll message → show error dialog → retry code fetch
- On back → stop polling → go to previous screen
- Handle device limit (same as existing login)

```xml
<!-- src/components/screens/login-qrcode/LoginQRCodeScreen.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<component name="LoginQRCodeScreen" extends="Group">
    <script type="text/brightscript" uri="LoginQRCodeScreen.bs" />
    
    <interface>
        <field id="isLoading" type="boolean" value="false" />
        <field id="error" type="string" value="" />
    </interface>
    
    <children>
        <!-- Background -->
        <Rectangle
            width="1920"
            height="1080"
            color="#0D0D0D"
        />
        
        <!-- Main container -->
        <Group translation="[0, 0]">
            <!-- Left side: QR code area -->
            <Group translation="[360, 180]">
                <!-- QR frame background -->
                <Poster
                    id="qrFrameBg"
                    uri=""
                    width="400"
                    height="400"
                    translation="[0, 0]"
                />
                
                <!-- QR code image -->
                <Poster
                    id="qrCodePoster"
                    uri=""
                    width="400"
                    height="400"
                    translation="[0, 0]"
                    loadSync="true"
                />
                
                <!-- Loading overlay -->
                <Group id="loadingGroup" visible="false" translation="[100, 150]">
                    <BusySpinner
                        id="qrLoadingSpinner"
                        translation="[100, 0]"
                    />
                    <Label
                        id="loadingText"
                        text="Getting your code…"
                        translation="[0, 100]"
                        width="400"
                        color="#FFFFFF"
                        horizAlign="center"
                    >
                        <Font role="font" uri="pkg:/source/fonts/Nunito-Light.ttf" size="32" />
                    </Label>
                </Group>
                
                <!-- Error overlay -->
                <Group id="errorGroup" visible="false" translation="[0, 0]">
                    <Label
                        id="errorText"
                        text=""
                        width="400"
                        color="#FF6B6B"
                        horizAlign="center"
                        wrap="true"
                    >
                        <Font role="font" uri="pkg:/source/fonts/Nunito-Light.ttf" size="28" />
                    </Label>
                    <RegularButton
                        id="retryButton"
                        text="Try Again"
                        translation="[100, 200]"
                    />
                </Group>
                
                <!-- Instruction text below QR -->
                <Label
                    id="instructionTop"
                    text="Scan with your phone or go to"
                    translation="[0, 420]"
                    width="400"
                    color="#F5C542"
                    horizAlign="center"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Bold.ttf" size="32" />
                </Label>
                
                <Label
                    id="instructionUrl"
                    text="duriooplus.com/tv"
                    translation="[0, 460]"
                    width="400"
                    color="#FFFFFF"
                    horizAlign="center"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Bold.ttf" size="36" />
                </Label>
                
                <Label
                    id="instructionCode"
                    text="Enter the code"
                    translation="[0, 510]"
                    width="400"
                    color="#F5C542"
                    horizAlign="center"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Bold.ttf" size="32" />
                </Label>
                
                <!-- Verification code display -->
                <Label
                    id="verificationCodeText"
                    text="----"
                    translation="[0, 560]"
                    width="400"
                    color="#FFFFFF"
                    horizAlign="center"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Bold.ttf" size="56" />
                </Label>
            </Group>
            
            <!-- Right side: Instructions -->
            <Group translation="[860, 200]" width="800">
                <Label
                    id="titleText"
                    text="Sign in with your phone"
                    color="#FFFFFF"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Bold.ttf" size="52" />
                </Label>
                
                <Label
                    id="step1Text"
                    text="1. Open the camera app on your phone"
                    translation="[0, 100]"
                    color="#CCCCCC"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Regular.ttf" size="34" />
                </Label>
                
                <Label
                    id="step2Text"
                    text="2. Scan the QR code shown on screen"
                    translation="[0, 150]"
                    color="#CCCCCC"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Regular.ttf" size="34" />
                </Label>
                
                <Label
                    id="step3Text"
                    text="3. Sign in or create your account"
                    translation="[0, 200]"
                    color="#CCCCCC"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Regular.ttf" size="34" />
                </Label>
                
                <Label
                    id="step4Text"
                    text="4. You will be signed in automatically"
                    translation="[0, 250]"
                    color="#CCCCCC"
                >
                    <Font role="font" uri="pkg:/source/fonts/Nunito-Regular.ttf" size="34" />
                </Label>
            </Group>
        </Group>
        
        <!-- Close button -->
        <ButtonClose id="closeButton" translation="[1800, 40]" />
    </children>
</component>
```

```brightscript
' src/components/screens/login-qrcode/LoginQRCodeScreen.bs
import "pkg:/source/Actions.bs"
import "pkg:/source/MixpanelTrack.bs"
import "pkg:/source/roku_modules/promises/promises.brs"
import "pkg:/components/utils/qr/QrUtils.bs"

sub init()
    m.closeButton = m.top.findNode("closeButton")
    m.qrCodePoster = m.top.findNode("qrCodePoster")
    m.verificationCodeText = m.top.findNode("verificationCodeText")
    m.loadingGroup = m.top.findNode("loadingGroup")
    m.errorGroup = m.top.findNode("errorGroup")
    m.errorText = m.top.findNode("errorText")
    m.retryButton = m.top.findNode("retryButton")
    m.instructionTop = m.top.findNode("instructionTop")
    m.instructionUrl = m.top.findNode("instructionUrl")
    m.instructionCode = m.top.findNode("instructionCode")
    m.qrFrameBg = m.top.findNode("qrFrameBg")
    
    m.top.observeField("visible", "onVisibleChange")
    
    m.currentCode = ""
    m.pollTask = invalid
end sub

sub onVisibleChange()
    if (m.top.visible)
        m.closeButton.observeField("pressed", "onCloseButtonPressed")
        m.retryButton.observeField("pressed", "onRetryButtonPressed")
        m.qrFrameBg.uri = "pkg:/images/qr_frame_bg.png"
        showLoading(true)
        showError(false)
        fetchVerificationCode()
        MixpanelTrack("auth_scan_code_option_clicked", {})
    else
        m.closeButton.unobserveField("pressed")
        m.retryButton.unobserveField("pressed")
        stopPolling()
        cleanup()
    end if
end sub

sub cleanup()
    m.qrCodePoster.uri = ""
    m.verificationCodeText.text = "----"
end sub

sub showLoading(show as boolean)
    m.loadingGroup.visible = show
    if (show)
        m.qrCodePoster.visible = false
        m.verificationCodeText.visible = false
        m.instructionTop.visible = false
        m.instructionUrl.visible = false
        m.instructionCode.visible = false
    end if
end sub

sub showError(show as boolean)
    m.errorGroup.visible = show
end sub

sub showQrContent(show as boolean)
    m.qrCodePoster.visible = show
    m.verificationCodeText.visible = show
    m.instructionTop.visible = show
    m.instructionUrl.visible = show
    m.instructionCode.visible = show
end sub

sub fetchVerificationCode()
    showLoading(true)
    showError(false)
    showQrContent(false)
    stopPolling()
    
    m.requestCodeTask = createObject("roSGNode", "RequestCodeTask")
    promise = m.requestCodeTask.callFunc("requestCode")
    
    promises.onThen(promise, sub (result as object)
        if (result = invalid or result.verificationCode = invalid or result.verificationCode = "")
            m.errorText.text = "We could not get a sign-in code. Check your connection and try again."
            showLoading(false)
            showError(true)
            showQrContent(false)
            return
        end if
        
        m.currentCode = result.verificationCode
        
        ' Build QR URL
        oneLinkUrl = buildTvLoginOneLinkUrl(m.currentCode, m.global.state.deviceModel)
        qrImageUrl = buildQrImageUrl(oneLinkUrl, 400)
        
        ' Set QR poster
        m.qrCodePoster.uri = qrImageUrl
        
        ' Set formatted code
        formattedCode = formatVerificationCode(m.currentCode)
        m.verificationCodeText.text = formattedCode
        
        showLoading(false)
        showError(false)
        showQrContent(true)
        
        ' Start polling
        startPolling()
    end sub)
end sub

sub startPolling()
    m.pollTask = createObject("roSGNode", "QRCodePollingTask")
    m.pollTask.shouldStop = false
    promise = m.pollTask.callFunc("startPolling")
    
    promises.onThen(promise, sub (result as object)
        if (result = invalid)
            ' Polling was stopped
            return
        end if
        
        if (result.user <> invalid and result.token <> invalid)
            ' Login success
            ? "[LoginQRCodeScreen] QR login success"
            MixpanelLoginSuccess(result)
            if (result.message = "Maximum allowed devices reached")
                result.isMaxDevice = true
                m.global.navigation.callFunc("goToScreen", "MaxDeviceReachedScreen", {
                    loginData: result,
                    type: "qr"
                })
            else
                Login(result)
            end if
        else if (result.expired = true)
            ' Code expired, fetch new one
            ? "[LoginQRCodeScreen] code expired, refetching"
            fetchVerificationCode()
        else if (result.message <> invalid)
            ' Server error message
            ? "[LoginQRCodeScreen] server message:", result.message
            showLoading(false)
            showError(false)
            showQrContent(true)
            fetchVerificationCode()
        end if
    end sub)
end sub

sub stopPolling()
    if (m.pollTask <> invalid)
        m.pollTask.shouldStop = true
        m.pollTask = invalid
    end if
end sub

sub onCloseButtonPressed()
    MixpanelTrack("auth_qr_close_clicked", {})
    m.global.navigation.callFunc("goToPreviousScreen")
end sub

sub onRetryButtonPressed()
    fetchVerificationCode()
end sub

function onKeyEvent(key as string, press as boolean) as boolean
    if (press)
        if (key = "back")
            stopPolling()
            m.global.navigation.callFunc("goToPreviousScreen")
            return true
        end if
        
        if (key = "up")
            if (m.retryButton.hasFocus())
                m.closeButton.setFocus(true)
                return true
            end if
        end if
        
        if (key = "down")
            if (m.closeButton.hasFocus() and m.retryButton.visible)
                m.retryButton.setFocus(true)
                return true
            end if
        end if
    end if
    return false
end function
```

---

## Phase 3: LoginIntroScreen Modifications

### 3.1 Add "Sign in with Phone" Button

**File:** `src/components/screens/login-intro/LoginIntroScreen.xml`

Add below the existing login section:
```xml
<RegularButton
    id="phoneLoginButton"
    text="Sign in with Phone"
    translation="[1100, 820]"
/>
```

### 3.2 Add Button Handler

**File:** `src/components/screens/login-intro/LoginIntroScreen.bs`

```brightscript
' In init():
m.phoneLoginButton = m.top.findNode("phoneLoginButton")

' In onVisibleChange():
m.phoneLoginButton.observeField("pressed", "onPhoneLoginButtonPressed")

' Else:
m.phoneLoginButton.unobserveField("pressed")

' New handler:
sub onPhoneLoginButtonPressed()
    MixpanelTrack("auth_scan_code_option_clicked", {})
    m.global.navigation.callFunc("goToScreen", "LoginQRCodeScreen")
end sub

' On key event for focus navigation — add phoneLoginButton to the navigation chain:
' In onKeyEvent down:
if (m.loginButton.hasFocus() and m.phoneLoginButton.visible)
    m.phoneLoginButton.setFocus(true)
    return true
end if

' In onKeyEvent up:
if (m.phoneLoginButton.hasFocus())
    m.loginButton.setFocus(true)
    return true
end if
```

---

## Phase 4: StackNavigator Registration

### 4.1 Register the New Screen

**File:** `src/components/utils/navigation/StackNavigator.bs`

Add in `initialize()`:
```brightscript
registerScreen("LoginQRCodeScreen")
```

---

## Phase 5: State & Analytics

### 5.1 Mixpanel Events Reference

| Event | Trigger | Properties |
|-------|---------|------------|
| `auth_scan_code_option_clicked` | User clicks "Sign in with Phone" | `{}` |
| `auth_qr_close_clicked` | User closes QR screen | `{}` |
| `login_success` | QR login completes | `user_id`, `email`, `status`, `auth_type: "qr"`, `country_code` |

> Note: `MixpanelLoginSuccess` in `MixpanelTrack.bs` already fires `login_success`. The `auth_type` from the server response (`loginData.user.auth_type`) will be used automatically.

### 5.2 No Redoku Changes Required

The existing `Login()` action in `Actions.bs` and `Redoku_LoginReducer` handle the login payload identically regardless of auth method. No changes to state management needed — the QR flow reuses the same login pipeline.

---

## Phase 6: Assets

### 6.1 Required Images
- `qr_login_intro.png` — already exists at `src/images/qr_login_intro.png` (used on intro screen)
- `qr_frame_bg.png` — **NEW** — White/transparent frame background for QR code area (~420×420px). This is a subtle border/background to make the QR code visually stand out on the dark screen.

---

## Phase 7: Testing Checklist

### Manual Test Cases

1. **Happy path: QR scan**
   - [ ] Go to LoginIntroScreen → tap "Sign in with Phone"
   - [ ] QR code displays correctly (scannable, correct URL)
   - [ ] Verification code shows as `XXXX-XXXX`
   - [ ] Scan QR on phone → sign in → Roku auto-logs in
   - [ ] Landing on HomeScreen with logged-in menu

2. **Happy path: URL entry**
   - [ ] Visit `duriooplus.com/tv` on phone browser
   - [ ] Enter the code shown on TV
   - [ ] Roku auto-logs in

3. **Error: No network**
   - [ ] Disconnect Roku from network
   - [ ] Navigate to QR screen → error state shown
   - [ ] "Try Again" button visible
   - [ ] Reconnect → tap retry → works

4. **Code expiry**
   - [ ] Wait for code to expire (10 min) → new QR auto-loads
   - [ ] Verification code changes

5. **Back navigation**
   - [ ] On QR screen, press Back → returns to LoginIntroScreen
   - [ ] Confirm polling is stopped (no more network requests)

6. **Device limit**
   - [ ] Login on an already-full device → MaxDeviceReachedScreen shown
   - [ ] Correct payload passed for "qr" type

7. **Server error during polling**
   - [ ] Simulate server error → code is refetched transparently

8. **Quick back-and-forth**
   - [ ] Enter QR screen, press back, re-enter quickly → no duplicate tasks

---

## Phase 8: Backend Changes Required

See full backend plan at `docs/qr-login/plan-polling.md`.

**Summary** — 3 files changed (~1.5 hours):

1. **New endpoint**: `POST /auth/check-code-status` — accepts `{ device_id }`, returns `{ status: "pending" }`, `{ user, token }`, or `{ expired: true }`
2. **Modified `verifyCode()`**: After signIn, stores result in Redis (`code-result:{device_id}`, TTL 30s) for polling clients
3. **New DTO**: `CheckCodeStatusDto` with `device_id` field

**No infrastructure changes needed** — reuses existing Redis, no new env vars, fully backward compatible with Samsung's Socket.io flow.

---

## Effort Estimate

| Phase | Tasks | Est. Hours |
|-------|-------|------------|
| 1. API Layer | RequestCodeTask + QRCodePollingTask + QrUtils | 3-4h |
| 2. UI Screen | LoginQRCodeScreen XML + logic | 4-5h |
| 3. Intro Mod | LoginIntroScreen changes | 1h |
| 4. Navigation | StackNavigator registration | 0.5h |
| 5. Analytics | Mixpanel events | 0.5h |
| 6. Assets | QR frame background image | 0.5h |
| 7. Testing | Manual test cases | 2-3h |
| 8. Backend | Confirmation + potential endpoint | Depends |
| **Total** | | **~12-15h** |

---

## References

- Samsung QR Login: `/Users/urip/durioo/duriooplus-web-samsung/src/utils/tvLoginQr.ts`
- Samsung Login Layout: `/Users/urip/durioo/duriooplus-web-samsung/src/layout/LoginLayout/index.tsx`
- Samsung Phone Login Panel: `/Users/urip/durioo/duriooplus-web-samsung/src/layout/LoginLayout/PhoneLoginPanel.tsx`
- Samsung Request Code Hook: `/Users/urip/durioo/duriooplus-web-samsung/src/services/auth/hooks/useRequestCode.ts`
- Samsung Auth Service: `/Users/urip/durioo/duriooplus-web-samsung/src/services/auth/index.ts`
- Samsung Feature Flags: `/Users/urip/durioo/duriooplus-web-samsung/src/configs/featureFlags.ts`
- Roku Login Intro: `src/components/screens/login-intro/LoginIntroScreen.bs`
- Roku Login Task: `src/components/services/login/LoginTask.bs`
- Roku Actions: `src/source/Actions.bs`
- Roku Reducers: `src/source/Reducers.bs`
- Roku State/Main: `src/source/main.bs`
