# PRD: QR Login for Roku (Durioo+ TV)

## Problem Statement

The Roku Durioo+ app currently only supports traditional email/password login (and OTP via email). On TV devices, typing credentials with a remote control is a poor user experience — slow, error-prone, and frustrating. The Samsung TV version (`duriooplus-web-samsung`) solves this with a QR-based "Sign in with your phone" flow that:

1. Displays a QR code and a short verification code on the TV screen
2. The user scans the QR (or visits `duriooplus.com/tv` and enters the code) on their phone
3. The phone authenticates the user, and the TV receives the login credentials via real-time push (Socket.io)
4. The TV logs the user in automatically — no typing on the remote

This PRD adapts the same QR login flow for Roku, working within SceneGraph/BrighterScript constraints (no React, no Socket.io WebSockets directly, no client-side QR generation).

---

## User Flow

```
┌─────────────────────────────────────────────────────┐
│  LoginIntroScreen (existing)                        │
│  ┌──────────────────────────────────────────────┐   │
│  │  "Your 7 Days Free Trial Is Waiting!"        │   │
│  │  "Create your Durioo+ account by             │   │
│  │   scanning the QR code now"                  │   │
│  │                                              │   │
│  │  [QR placeholder image]                      │   │
│  │                                              │   │
│  │  "Already have an account?"   [Login]        │   │
│  │        ← NEW → [Sign in with Phone]          │   │
│  └──────────────────────────────────────────────┘   │
│                         │                            │
│                         ▼                            │
│  LoginQRCodeScreen (NEW)                             │
│  ┌──────────────────────────────────────────────┐   │
│  │  ┌────────────┐   Scan with your phone       │   │
│  │  │            │   or go to                   │   │
│  │  │  QR Code   │   duriooplus.com/tv          │   │
│  │  │  (image)   │                              │   │
│  │  │            │   Enter the code             │   │
│  │  └────────────┘   1234-5678                  │   │
│  │                                              │   │
│  │  [Waiting...]  ← auto-refreshes on verify   │   │
│  │  [Back/Close]                                │   │
│  └──────────────────────────────────────────────┘   │
│                         │                            │
│                (user scans on phone)                 │
│                         │                            │
│                         ▼                            │
│              Auto-login → HomeScreen                 │
└─────────────────────────────────────────────────────┘
```

---

## Architecture: How QR Login Works (Samsung → Roku Adaptation)

### Samsung Flow (reference)

| Step | Component | What Happens |
|------|-----------|-------------|
| 1 | `useRequestCode` hook | POST `/auth/request-code` → returns `verification_code` |
| 2 | `buildTvLoginOneLinkUrl` | Builds OneLink deep-link URL with code + device info |
| 3 | `PhoneLoginPanel` + `QRCodeSVG` | Renders QR from URL + shows formatted code |
| 4 | Socket.io `code-verified-{deviceId}` | Server pushes login payload when phone completes auth |
| 5 | `handleAuthSuccessPayload` | Same as regular login: set token, get subscriber, go home |

### Roku Adaptation

| Step | Component | What Happens |
|------|-----------|-------------|
| 1 | `RequestCodeTask` (new) | POST `/auth/request-code` → returns `verification_code` |
| 2 | `buildQrImageUrl` (helper) | Builds external QR image URL via `api.qrserver.com` |
| 3 | `LoginQRCodeScreen` (new) | Shows QR as a `Poster` image + formatted code text |
| 4 | `QRCodePollingTask` (new) | Polls `/auth/check-code-status?device_id={id}` every 2–3s |
| 5 | `Actions.Login()` (existing) | Same `Login()` action dispatcher on success |

### Key Differences from Samsung

| Concern | Samsung | Roku |
|---------|---------|------|
| **QR Generation** | Client-side `qrcode.react` library | External QR image via `api.qrserver.com/v1/create-qr-code` |
| **Real-time push** | Socket.io WebSocket | HTTP polling (`/auth/check-code-status`) |
| **State management** | React Query + Redux | Redoku (SceneGraph Redux pattern) |
| **UI framework** | React + styled-components | SceneGraph XML + BrighterScript |
| **Code refresh** | `refetchInterval: 10min` via React Query | Custom timer in polling task |
| **Feature flag** | `NEXT_PUBLIC_FEATURE_TV_QR_LOGIN` env var | App-level config constant or env mode |

---

## Acceptance Criteria

### AC-1: LoginIntroScreen Changes
- [ ] The `LoginIntroScreen` XML already has QR copy and a `qr_login_intro.png` image — repurpose into a new **"Sign in with Phone"** button
- [ ] Add a new `RegularButton` labeled **"Sign in with Phone"** below the existing "Login" button
- [ ] On press, navigate to `LoginQRCodeScreen`
- [ ] Track event: `auth_scan_code_option_clicked` (same as Samsung)

### AC-2: LoginQRCodeScreen — QR Display
- [ ] New screen component `LoginQRCodeScreen` (XML + .bs)
- [ ] On init/show: call `RequestCodeTask` to get a verification code from `/auth/request-code`
- [ ] While loading: show a spinner/loading indicator with "Getting your code…" text
- [ ] On success: 
  - Build QR image URL from the OneLink URL using Google Charts API
  - Display QR as a `Poster` node (`uri` pointing to the chart API)
  - Display the formatted verification code (e.g., `1234-5678`)
  - Display instructions: "Scan with your phone or go to duriooplus.com/tv"
- [ ] On error: show "We could not get a sign-in code. Check your connection and try again." with a "Try again" button
- [ ] QR image must be at least 400×400px for scannability on TV

### AC-3: QR Code Polling for Verification
- [ ] Start `QRCodePollingTask` after code is received
- [ ] Poll endpoint `POST /auth/check-code-status` (or `GET /auth/request-code?device_id={id}`) every 2–3 seconds
- [ ] **Success**: response contains `{ user: {...}, token: {...} }` → call `Login(loginData)` → navigate to HomeScreen
- [ ] **Expired**: response contains `{ expired: true }` → refetch a new verification code → update QR display
- [ ] **Error/message**: response contains `{ message: "..." }` → show error dialog → refetch code
- [ ] **Timeout**: stop polling after 10 minutes (when code naturally expires) → refetch code
- [ ] Track event: `login_success` with `auth_type: "qr"` on successful QR login

### AC-4: OneLink URL Construction
- [ ] Implement `buildTvLoginOneLinkUrl` equivalent:
  ```
  https://durioo.onelink.me/IbX3?
    pid=TV_Login
    &c=TV_Login
    &deep_link_value=tv_verification
    &verification_code=<stripped code>
    &device_brand=Roku
    &device_model=<model>
    &af_web_dp=https://duriooplus.com/tv
  ```
- [ ] Strip spaces/hyphens from verification code for the URL
- [ ] Format display code as `XXXX-XXXX`

### AC-5: QR Image URL Generation
- [ ] Use `api.qrserver.com` to generate QR image:
  ```
  https://api.qrserver.com/v1/create-qr-code/?
    size=400x400
    &data=<url_encoded_onelink_url>
  ```
- [ ] The service returns a PNG image directly; no API key required
- [ ] The QR image `uri` must be a URL that Roku's `Poster`/`roBitmap` can load

### AC-6: State Management
- [ ] Add QR login tracking fields to Redoku login state:
  - `qrCodeRequested: boolean`
  - `qrVerificationPending: boolean`
- [ ] Login flow uses the same `Login()` action and `Redoku_LoginReducer` as existing login

### AC-7: Back Navigation & Cleanup
- [ ] Pressing "Back" on `LoginQRCodeScreen` returns to `LoginIntroScreen`
- [ ] Screen teardown must:
  - Stop the polling timer
  - Clear the QR poster URI
  - Cancel any pending HTTP requests
- [ ] If user navigates away while polling, polling must stop

### AC-8: Error Handling
- [ ] No network → show error state with retry
- [ ] `/auth/request-code` returns 4xx/5xx → show error state with retry
- [ ] Polling endpoint returns 4xx/5xx → silently retry next poll cycle
- [ ] Device ID is missing → show "Unable to load device ID" message
- [ ] Code expired during polling → auto-refetch (transparent)

### AC-9: Device Limit Handling
- [ ] If login response indicates "Maximum allowed devices reached", navigate to `MaxDeviceReachedScreen` with appropriate payload (same as existing password/OTP login)

### AC-10: Analytics (Mixpanel)
- [ ] `auth_scan_code_option_clicked` — when user selects "Sign in with Phone"
- [ ] `auth_scan_code_option_selected` — when QR option receives focus
- [ ] `login_success` with `auth_type: "qr"` — on successful QR login

---

## API Endpoints

### Used from Samsung (shared backend)

```
POST /auth/request-code
  Body: { device_id, device_brand, device_model }
  Response: { data: { code: "12345678" } or { data: { verification_code: "12345678" } } }
  
POST /auth/check-code-status   (NEW — to be confirmed with backend team)
  Body: { device_id }
  Response (pending): { status: "pending" }
  Response (verified): { user: {...}, token: {...} }
  Response (expired): { expired: true }
  Response (error): { message: "..." }
```

> **Note**: If `/auth/check-code-status` doesn't exist yet, the polling can use the Socket.io events exposed as a REST endpoint, or we poll `/auth/request-code` itself to detect when the response shape changes from `{ code }` to `{ user, token }`. Confirm with backend team before implementation.

---

## Files to Create

| File | Purpose |
|------|---------|
| `src/components/screens/login-qrcode/LoginQRCodeScreen.xml` | SceneGraph layout for QR screen |
| `src/components/screens/login-qrcode/LoginQRCodeScreen.bs` | Screen logic: code fetch, polling, timer |
| `src/components/services/login/RequestCodeTask.xml` | Task node for `/auth/request-code` |
| `src/components/services/login/RequestCodeTask.bs` | Task logic |
| `src/components/services/login/QRCodePollingTask.xml` | Task node for polling verification status |
| `src/components/services/login/QRCodePollingTask.bs` | Task logic with timer |
| `src/components/utils/qr/QrUtils.bs` | Utility: OneLink URL builder, QR image URL builder, code formatter |

## Files to Modify

| File | Change |
|------|--------|
| `src/components/screens/login-intro/LoginIntroScreen.xml` | Add "Sign in with Phone" button |
| `src/components/screens/login-intro/LoginIntroScreen.bs` | Handle new button press + navigation |
| `src/components/utils/navigation/StackNavigator.bs` | Register `LoginQRCodeScreen` |
| `src/source/ActionTypes.bs` | Optionally add QR-specific action types |
| `src/source/Actions.bs` | Optionally add QR-specific action |
| `src/source/Reducers.bs` | Optionally add QR state fields to login reducer |
| `src/source/main.bs` | No changes needed unless adding QR state to initialState |

---

## Risks & Mitigations

| Risk | Impact | Mitigation |
|------|--------|-----------|
| `api.qrserver.com` becomes unavailable or rate-limited | QR image won't render | Use server-provided QR endpoint as fallback; `api.qrserver.com` is a well-established free service |
| `/auth/check-code-status` endpoint doesn't exist | Can't poll for verification | Fall back to polling `/auth/request-code` and detecting payload shape change |
| Roku `Poster` can't load HTTPS QR image URL | QR won't display | Verify TLS support; use `roUrlTransfer` to download image and set as bitmap |
| Polling latency (2-3s intervals) feels slow | 2-3s delay after phone auth | Acceptable UX for TV; reduce interval if backend supports it |
| Roku memory constraints | QR image + polling timer overhead | Use reasonable QR size (400px); clean up tasks properly |

---

## Test Plan

1. **Happy Path**: Select "Sign in with Phone" → QR displays → scan on phone → login succeeds → HomeScreen
2. **Code Entry**: Visit `duriooplus.com/tv` on phone → enter displayed code → login succeeds
3. **Code Expired**: Wait for code expiry → auto-refresh → new QR appears
4. **Back Navigation**: On QR screen, press Back → returns to LoginIntroScreen → polling stops
5. **No Network**: Disconnect network → error state with retry → reconnect → retry works
6. **Device Limit**: Login on 10+ devices → MaxDeviceReachedScreen shown
7. **Existing Login**: User already logged in → side menu shows "Login / Subscribe" for logout, not QR flow
8. **Error Recovery**: Server returns 500 → error state → retry works

## Implementation Snippet References

### QR OneLink URL Builder (BrighterScript)
```brightscript
function buildTvLoginOneLinkUrl(verificationCode as string, deviceBrand as string, deviceModel as string) as string
    code = stripVerificationCode(verificationCode)
    brand = deviceBrand
    if (brand = "" or lCase(brand) = "unknown")
        brand = "Roku"
    end if
    
    params = [
        "pid=TV_Login",
        "c=TV_Login",
        "deep_link_value=tv_verification",
        "verification_code=" + code,
        "device_brand=" + brand,
        "device_model=" + encodeUriComponent(deviceModel),
        "af_web_dp=" + encodeUriComponent("https://duriooplus.com/tv")
    ]
    
    return "https://durioo.onelink.me/IbX3?" + params.join("&")
end function
```

### QR Image URL Builder (BrighterScript)
```brightscript
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
```

### Verification Code Formatting (BrighterScript)
```brightscript
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
```

### RequestCodeTask (BrighterScript)
```brightscript
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)
        promises.resolve(invalid, m.promise)
        return
    end if
    
    resJson = response.json
    codeData = resJson.data
    
    ' Extract code from various possible response shapes
    code = extractVerificationCode(codeData)
    
    if (code = "")
        promises.resolve(invalid, m.promise)
        return
    end if
    
    promises.resolve({ verificationCode: code }, m.promise)
end sub
```

### QRCodePollingTask (BrighterScript)
```brightscript
sub execute()
    pollInterval = 3  ' seconds
    maxPolls = 200     ' ~10 minutes at 3s intervals
    pollCount = 0
    
    while (pollCount < maxPolls)
        response = rokurequests_Requests().post(
            m.global.state.baseURL + "/auth/check-code-status",
            { "json": { "device_id": m.global.state.deviceId } }
        )
        
        if (response.statusCode <= 201 and response.json <> invalid)
            data = response.json.data
            if (data.user <> invalid and data.token <> invalid)
                ' Login success!
                promises.resolve(data, m.promise)
                return
            else if (data.expired = true)
                ' Code expired
                promises.resolve({ expired: true }, m.promise)
                return
            else if (data.message <> invalid)
                ' Error message from server
                promises.resolve({ message: data.message }, m.promise)
                return
            end if
        end if
        
        pollCount++
        sleep(pollInterval * 1000)
    end while
    
    ' Timeout - code expired
    promises.resolve({ expired: true }, m.promise)
end sub
```
