diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index ca721b4..15bebc9 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -43,7 +43,6 @@ env:
# Expose commit hash to the app (used by the About page)
EXPO_PUBLIC_GIT_COMMIT_HASH: ${{ github.sha }}
EXPO_PUBLIC_GITHUB_RELEASES_URL: ${{ github.server_url }}/${{ github.repository }}/releases
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
jobs:
test:
diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md
deleted file mode 100644
index cc345cf..0000000
--- a/TROUBLESHOOTING.md
+++ /dev/null
@@ -1,919 +0,0 @@
-# DodoStream Performance Troubleshooting Guide
-
-This document catalogs known performance issues, prioritized fixes, and a debugging setup guide for TV and mobile platforms.
-
----
-
-## Table of Contents
-
-1. [Architecture Overview](#architecture-overview)
-2. [Priority 1 — Replace FlashList with Legend List + Eliminate Nested Virtualization](#priority-1--replace-flashlist-with-legend-list--eliminate-nested-virtualization)
-3. [Priority 2 — Reduce Focusable Re-renders](#priority-2--reduce-focusable-re-renders)
-4. [Priority 3 — Stabilize renderItem References](#priority-3--stabilize-renderitem-references)
-5. [Priority 4 — Strip Debug Logging from Production](#priority-4--strip-debug-logging-from-production)
-6. [Priority 5 — Remove Image Tracking Overhead](#priority-5--remove-image-tracking-overhead)
-7. [Priority 6 — Extract useTheme from List Items](#priority-6--extract-usetheme-from-list-items)
-8. [MCP Debugging Setup Guide](#mcp-debugging-setup-guide)
-9. [Performance Profiling Workflow](#performance-profiling-workflow) — the full build → deploy → profile → fix loop
-10. [Nuclear Options (If Still Not Enough)](#nuclear-options-if-still-not-enough)
-
----
-
-## Architecture Overview
-
-The home screen uses a **nested virtualized list pattern**:
-
-- **Outer list** (vertical FlashList): Renders section headers, continue-watching rows, and catalog rows.
-- **Inner lists** (horizontal FlashList per row): Each catalog section and the continue-watching row is its own FlashList.
-
-With 8 addon catalogs, that's **~10 FlashList instances** mounted simultaneously—each with its own recycling pool, scroll handler, layout manager, and viewability tracking. On TV, every `Focusable` in every mounted inner list is a native `Pressable` with focus/blur handlers, creating hundreds of focusable nodes for the TV focus engine.
-
-### Player Controls
-
-`PlayerControls` is a ~600-line component with 20+ `useCallback` wrappers. The `currentTime` prop updates at ~4Hz from the player engine, causing cascading re-render checks. The `useTVEventHandler` fires on every D-pad press globally.
-
----
-
-## Priority 1 — Replace FlashList with Legend List + Eliminate Nested Virtualization ✅ DONE
-
-**Status: COMPLETED** — All FlashList usages have been migrated to LegendList (`@legendapp/list`). The `@shopify/flash-list` dependency has been removed.
-
-**Impact: HIGH | Effort: MEDIUM-HIGH**
-
-The core performance problem is N+1 nested virtualized lists. With 8 addon catalogs, that's ~10 list instances each with their own recycling pool, scroll handler, and layout manager.
-
-Rather than prescribing one approach, we benchmark multiple options on a real TV device and pick the winner based on measured FPS, CPU, and memory.
-
-### Option A: Legend List (outer) + ScrollView (inner)
-
-**Hypothesis:** Inner rows don't need virtualization (10–50 lightweight cards each). A plain `ScrollView` has zero overhead — no recycling pool, no layout manager, no viewability tracking. The outer list controls which rows are mounted.
-
-```
-┌─────────────────────────────────────────┐
-│ LegendList (vertical, single instance) │
-│ ├── SectionHeader │
-│ ├── ScrollView (horizontal) │
-│ │ ├── MediaCard MediaCard ... │
-│ ├── SectionHeader │
-│ ├── ScrollView (horizontal) │
-│ │ ├── MediaCard MediaCard ... │
-│ └── ... │
-└─────────────────────────────────────────┘
-```
-
-**Pros:**
-
-- Eliminates ALL nested list overhead — one virtualized list total
-- Simplest code, easiest to debug
-- No recycling state management needed for inner cards
-- Plain `key` prop works normally (no FlashList/Legend List recycling concerns)
-
-**Cons:**
-
-- If a catalog has 100+ items, all cards mount at once in the ScrollView (though outer list unmounts the whole row when off-screen)
-- No item-level recycling within a row — may matter on low-memory devices
-- Horizontal scroll position is lost when row is unmounted/remounted by outer list
-
-**Implementation:**
-
-```tsx
-// Inner row (MediaList.tsx)
-import { ScrollView } from 'react-native';
-
-
- {data.map((item) => (
-
- ))}
-;
-```
-
----
-
-### Option B: Legend List (outer) + Legend List (inner)
-
-**Hypothesis:** Legend List nested inside Legend List. Inner lists use `recycleItems` and `getFixedItemSize` for zero-measure recycling. Legend List may handle nesting better than FlashList due to lower per-instance overhead.
-
-```
-┌─────────────────────────────────────────┐
-│ LegendList (vertical) │
-│ ├── SectionHeader │
-│ ├── LegendList (horizontal) │
-│ │ ├── MediaCard MediaCard ... │
-│ ├── SectionHeader │
-│ ├── LegendList (horizontal) │
-│ │ ├── MediaCard MediaCard ... │
-│ └── ... │
-└─────────────────────────────────────────┘
-```
-
-**Pros:**
-
-- Recycling within rows — only visible cards are mounted (good for 100+ item catalogs)
-- `getFixedItemSize` skips all measuring (card sizes are constant from theme)
-- `keyExtractor` layout caching across data changes
-- Legend List claims lower CPU/memory than FlashList per instance
-
-**Cons:**
-
-- Still N+1 list instances (one per visible row + outer)
-- Each inner list still has its own scroll handler and layout tracking
-- Need `useRecyclingState` in Focusable for focus state management
-
-**Implementation:**
-
-```tsx
-// Inner row (MediaList.tsx)
-import { LegendList } from '@legendapp/list';
-
- item.id}
- recycleItems
- getFixedItemSize={() => theme.cardSizes.media.width + theme.spacing.m}
- drawDistance={isTV ? TV_HORIZONTAL_DRAW_DISTANCE : MOBILE_HORIZONTAL_DRAW_DISTANCE}
- waitForInitialLayout={false}
- ItemSeparatorComponent={HorizontalSpacer}
- contentContainerStyle={{
- paddingHorizontal: theme.spacing.m,
- paddingVertical: theme.spacing.s,
- }}
-/>;
-```
-
----
-
-### Option C: Legend List (outer) + FlashList (inner) — Keep Current Inner Lists
-
-**Hypothesis:** Only replace the outer list with Legend List. Keep FlashList for the inner horizontal rows (smallest change surface). Tests whether the outer list is the bottleneck.
-
-```
-┌─────────────────────────────────────────┐
-│ LegendList (vertical) │
-│ ├── SectionHeader │
-│ ├── FlashList (horizontal) — existing │
-│ │ ├── MediaCard MediaCard ... │
-│ ├── SectionHeader │
-│ ├── FlashList (horizontal) — existing │
-│ │ ├── MediaCard MediaCard ... │
-│ └── ... │
-└─────────────────────────────────────────┘
-```
-
-**Pros:**
-
-- Smallest migration effort — only outer list changes
-- Inner rows keep existing FlashList recycling/focus behavior
-- Good as a control: isolates whether outer vs. inner list is the bottleneck
-
-**Cons:**
-
-- Still has FlashList instances per visible row with full overhead
-- Mixed dependencies (both libraries)
-- Doesn't solve nested list overhead if inner lists are the problem
-
-**Implementation:**
-
-```tsx
-// Outer list (index.tsx) — swap import, keep inner rows as-is
-import { LegendList } from '@legendapp/list';
-
-
- data={listData}
- renderItem={renderItem}
- keyExtractor={(item) => `${item.kind}-${item.sectionKey}`}
- drawDistance={Platform.isTV ? TV_DRAW_DISTANCE : MOBILE_DRAW_DISTANCE}
- getEstimatedItemSize={(item) => {
- switch (item.kind) {
- case 'section-header':
- return 50;
- case 'continue-watching-row':
- return theme.cardSizes.continueWatching.height + theme.spacing.s * 2;
- case 'catalog-row':
- return theme.cardSizes.media.height + theme.spacing.s * 2;
- }
- }}
- recycleItems={false}
-/>;
-// Inner FlashLists unchanged
-```
-
----
-
-### Option D: Single Legend List — Flattened Sections (No Nesting)
-
-**Hypothesis:** Eliminate nesting entirely by flattening all data into one list. Each media card becomes a row. Use visual grouping (headers + horizontal layout via `numColumns` or manual row chunking) instead of nested lists. This is the most radical change.
-
-```
-┌─────────────────────────────────────────┐
-│ LegendList (vertical, flat) │
-│ ├── SectionHeader "Trending" │
-│ ├── Row: [Card] [Card] [Card] [Card] │
-│ ├── Row: [Card] [Card] [Card] │
-│ ├── SectionHeader "Popular" │
-│ ├── Row: [Card] [Card] [Card] [Card] │
-│ └── ... │
-└─────────────────────────────────────────┘
-```
-
-**Pros:**
-
-- Single list instance — absolute minimum overhead
-- All cards virtualized in one pool
-- No nesting at all — no scroll-within-scroll interactions
-- Focus navigation is simpler (one list's focus domain)
-
-**Cons:**
-
-- Loses horizontal scrolling within rows (rows are now vertical or grid-based)
-- Fundamentally different UX — changes the home screen feel
-- Harder to implement "see all" horizontal scroll for a category
-- May need a completely different card layout
-
-**Note:** This changes the visual design. Only viable if horizontal scrolling per category isn't a hard requirement.
-
----
-
-### Benchmark Plan
-
-#### Setup
-
-1. Install Legend List alongside FlashList (temporary dual dependency):
-
- ```bash
- pnpx expo install @legendapp/list
- ```
-
-2. Create a feature flag or dev-menu toggle to switch between options at runtime:
-
- ```tsx
- // src/store/developer.store.ts or similar
- homeListMode: 'A' | 'B' | 'C' | 'D'; // default: 'C' (current behavior baseline)
- ```
-
-3. Build a **release** (not dev) build for Android TV:
- ```bash
- pnpm android --variant release
- # or: eas build --profile production_tv --local
- ```
-
-#### Test Script
-
-Perform the same interaction for each option:
-
-1. Launch app → navigate to home screen
-2. Wait 3 seconds (initial render settles)
-3. Scroll down through all catalog rows (steady speed, ~2 rows/second)
-4. Scroll back to top
-5. Scroll down fast (flick gesture / hold D-pad down)
-6. Total duration: ~30 seconds per option
-
-#### Metrics to Capture
-
-| Metric | Tool | Target |
-| -------------------------- | ------------------------- | ------------------ |
-| UI Thread FPS (sustained) | Flashlight / Perf Monitor | ≥55 fps |
-| JS Thread FPS (sustained) | Flashlight / Perf Monitor | ≥55 fps |
-| Total CPU % during scroll | Flashlight | <50% |
-| Peak memory during scroll | Flashlight | Stable (no growth) |
-| Frame drops (30s window) | Perfetto | <10 |
-| Time-to-interactive (home) | manual / onLoad callback | <1.5s |
-
-#### Decision Matrix
-
-| Option | UI FPS | JS FPS | CPU% | Memory | Effort | Winner? |
-| -------------------------------- | ------ | ------ | ------ | ------ | ------ | ------- |
-| Baseline (FlashList > FlashList) | \_\_\_ | \_\_\_ | \_\_\_ | \_\_\_ | 0 | — |
-| A: LegendList > ScrollView | \_\_\_ | \_\_\_ | \_\_\_ | \_\_\_ | Medium | |
-| B: LegendList > LegendList | \_\_\_ | \_\_\_ | \_\_\_ | \_\_\_ | Medium | |
-| C: LegendList > FlashList | \_\_\_ | \_\_\_ | \_\_\_ | \_\_\_ | Low | |
-| D: LegendList flat | \_\_\_ | \_\_\_ | \_\_\_ | \_\_\_ | High | |
-
-Fill in measured values. Pick the option with the best FPS/CPU tradeoff for acceptable effort. If A and B are close, prefer A (simpler).
-
----
-
-### Post-Benchmark: Full Migration
-
-Once the winning option is chosen, fully migrate:
-
-#### Install / Remove Dependencies
-
-```bash
-pnpx expo install @legendapp/list
-# After full migration:
-pnpm remove @shopify/flash-list
-```
-
-**Note:** `useRecyclingState` from FlashList is used in `Focusable.tsx`. Replace it with Legend List's `useRecyclingState` (same API) or remove it if inner rows use plain ScrollViews (Option A).
-
-#### Clean Up Dead FlashList Code
-
-- **`overrideItemLayout` callback** in `index.tsx` — sets `layout.size` which is dead in FlashList v2 (`layout.size` no longer read, only `layout.span`), and Legend List doesn't use it. Remove entirely.
-- **`getItemType` callback** — Legend List doesn't use type-based recycling pools. Remove.
-- **Perf debug scaffolding** (remove after profiling):
- - `renderItemRecreationCount` tracking
- - `prevActionsRef` + `continueWatchingActions` stability tracking
- - `prevRenderItemRef` tracking
- - `handleOuterListLoad` / `handleCWListLoad` — gate behind `__DEV__` or remove.
-- **`useRecyclingState` in `Focusable.tsx`** — simplify to plain `useState` if Option A wins.
-
-#### Migrate All Other FlashList Usages
-
-| File | Usage | Notes |
-| --------------------------------------------------- | -------------- | ------------------------------ |
-| `src/app/(app)/(tabs)/library.tsx` | Vertical list | Direct replacement |
-| `src/app/(app)/(tabs)/search.tsx` | Search results | Direct replacement |
-| `src/app/(app)/catalog.tsx` | Catalog grid | Use Legend List + `numColumns` |
-| `src/components/media/EpisodeList.tsx` | Episode list | Direct replacement |
-| `src/components/media/StreamList.tsx` | Stream list | Direct replacement |
-| `src/components/media/TrailersTab.tsx` | Trailer list | Direct replacement |
-| `src/components/media/CastTab.tsx` | Cast list | Direct replacement |
-| `src/components/basic/PickerModal.tsx` | Picker items | Direct replacement |
-| `src/components/settings/AddonsSettingsContent.tsx` | Addon list | Direct replacement |
-| `src/components/video/SubtitlePickerModal.tsx` | Subtitle list | Direct replacement |
-
-#### Legend List Configuration Tips
-
-From the [Legend List Performance docs](https://legendapp.com/open-source/list/v3/performance/):
-
-- **`keyExtractor`** — Always provide. Allows Legend List to reuse layout info across data changes.
-- **`getEstimatedItemSize`** — Provide per-item estimates. More accurate = less layout shifting. Slightly underestimate rather than overestimate.
-- **`getFixedItemSize`** — Use when items have known fixed sizes (skips all measuring).
-- **`recycleItems`** — Enable for lists where items have no local state (e.g., episode lists, stream lists). Disable for heterogeneous outer lists.
-- **`drawDistance`** — Keep current TV/mobile values. Experiment with lower values if items are expensive to render.
-- **`waitForInitialLayout`** — Set to `false` for fixed-size items to avoid a 1-frame delay.
-
----
-
-## Priority 2 — Reduce Focusable Re-renders ✅ DONE
-
-**Status: COMPLETED** — Focusable now uses Pressable's native `style={({focused}) => ...}` function for non-render-function children, eliminating React state updates and re-renders on focus/blur. Only render-function children still use `useState`.
-
-**Impact: HIGH | Effort: MEDIUM**
-
-**File:** `src/components/basic/Focusable.tsx`
-
-> **Note:** This is independent of the Legend List migration and can be done in parallel.
-
-Every focus/blur event triggers `setState` in the `Focusable` component, causing a React re-render even when children don't need `isFocused`. On TV, D-pad navigation fires focus/blur at high frequency — 2 re-renders per step across all visible items.
-
-### Suggested Fix: Use Pressable's Built-in Style Function
-
-For `variant="outline"` and `variant="background"` (non-render-function children), avoid React state entirely by using `Pressable`'s native `style` function:
-
-```tsx
-// Instead of tracking isFocused in useState and recomputing style:
- [
- flatStyle,
- focused ? computeFocusStyle(true, variant, theme, focusedStyle) : undefined,
- ]}>
- {children}
-
-```
-
-This moves focus styling to the native layer — no JS re-renders on focus change. Only keep `useState` for the render-function children path (`typeof children === 'function'`).
-
-### Caveat
-
-Verify that `Pressable`'s `style` function receives `focused` in react-native-tvos. If not available, consider using `onFocus`/`onBlur` with `setNativeProps` or Reanimated shared values to bypass React state.
-
----
-
-## Priority 3 — Stabilize renderItem References ✅ DONE
-
-**Status: COMPLETED** — All `renderItem` callbacks in list components are now wrapped in `useCallback` with proper dependency arrays.
-
-**Impact: MEDIUM | Effort: LOW**
-
-Legend List benefits from stable references, and plain `ScrollView` rows with `.map()` benefit from stable callbacks passed to children.
-
-### Files with inline `renderItem` (no useCallback)
-
-After the Priority 1 migration, `MediaList.tsx` and `ContinueWatchingSectionRow` become plain `ScrollView` + `.map()` — no `renderItem` prop needed. The remaining files still use Legend List and should have stable `renderItem`:
-
-| File | Line | Component |
-| --------------------------------------------------- | ---- | -------------- |
-| `src/components/media/TrailersTab.tsx` | ~50 | TrailerCard |
-| `src/components/media/EpisodeList.tsx` | ~126 | EpisodeItem |
-| `src/components/media/StreamList.tsx` | ~224 | StreamListItem |
-| `src/components/settings/AddonsSettingsContent.tsx` | ~132 | AddonCard |
-
-### Fix
-
-Wrap each `renderItem` in `useCallback`:
-
-```tsx
-// Before
-renderItem={({ item, index }) => (
-
-)}
-
-// After
-const renderItem = useCallback(
- ({ item, index }: { item: MetaPreview; index: number }) => (
-
- ),
- [onMediaPress]
-);
-
-// ...
-
-```
-
-### Files already correct (no change needed)
-
-- `src/components/media/CastTab.tsx` — useCallback ✓
-- `src/components/basic/PickerModal.tsx` — useCallback ✓
-- `src/app/(app)/(tabs)/index.tsx` (outer list) — useCallback ✓
-
----
-
-## Priority 4 — Strip Debug Logging from Production ✅ DONE
-
-**Status: COMPLETED** — `debug.ts` is gated on `__DEV__`. `babel-plugin-transform-remove-console` added for production builds as a safety net.
-
-**Impact: MEDIUM | Effort: LOW**
-
-### Problem
-
-`src/utils/debug.ts` runs `console.debug()` when `isSentryEnabled` is true — which is likely true in production. React Native docs explicitly warn that `console.*` is a major performance bottleneck.
-
-### Fix Option A: Gate on `__DEV__` only
-
-```tsx
-// src/utils/debug.ts
-export const createDebugLogger = (scope: string): DebugLogger => {
- return (...args: unknown[]) => {
- if (__DEV__) {
- console.debug(`[${scope}]`, ...args);
- }
- };
-};
-```
-
-### Fix Option B: Add babel-plugin-transform-remove-console
-
-```bash
-pnpm add -D babel-plugin-transform-remove-console
-```
-
-Then update `babel.config.js`:
-
-```js
-module.exports = function (api) {
- api.cache(true);
- let plugins = [];
- plugins.push('react-native-worklets/plugin');
- if (process.env.NODE_ENV === 'production') {
- plugins.push('transform-remove-console');
- }
- return {
- presets: ['babel-preset-expo'],
- plugins,
- };
-};
-```
-
-**Recommendation:** Do both. Option A is the narrowest fix; Option B is a safety net that catches third-party console calls too.
-
----
-
-## Priority 5 — Remove Image Tracking Overhead ✅ DONE
-
-**Status: COMPLETED** — No image tracking code exists in the codebase.
-
-**Impact: LOW-MEDIUM | Effort: LOW**
-
-**File:** `src/components/media/MediaCard.tsx` (lines ~20–81)
-
-Every MediaCard instance runs a `useEffect` to track image mount/unmount in a global `Set`. This is perf-debugging code that adds overhead in every card.
-
-### Fix
-
-Gate behind `__DEV__`:
-
-```tsx
-// Only track in development
-if (__DEV__) {
- useEffect(() => {
- if (imageUri) trackImageMount(imageUri);
- return () => {
- if (imageUri) trackImageUnmount(imageUri);
- };
- }, [imageUri]);
-}
-```
-
-Or remove entirely once image loading performance is satisfactory.
-
----
-
-## Priority 6 — Extract useTheme from List Items (DEFERRED)
-
-**Status: DEFERRED** — `useTheme()` is just `useContext()` which is very cheap per call. The theme can change dynamically (scaling + presets), so a static import isn't possible. Threading theme props through 6+ callers creates significant churn for marginal gain. Profile on device before investing here.
-
-**Impact: LOW | Effort: LOW**
-
-**Files affected:**
-
-- `src/components/media/MediaCard.tsx` — calls `useTheme()` per card instance
-- `src/components/media/ContinueWatchingCard.tsx` — calls `useTheme()` per card instance
-- `src/components/media/CatalogSectionHeader.tsx` — calls `useTheme()` per header
-
-### Fix
-
-Extract theme values at the parent list level and pass them as props:
-
-```tsx
-// In MediaList.tsx (parent)
-const theme = useTheme();
-const cardWidth = theme.cardSizes.media.width;
-const cardHeight = theme.cardSizes.media.height;
-
-// Pass to MediaCard
-
-```
-
-This eliminates context subscription overhead from hundreds of list items. The theme rarely changes, so the values can be passed as stable props.
-
-**Note:** This may be marginal. Profile before and after to confirm impact.
-
----
-
-## MCP Debugging Setup Guide
-
-MCP (Model Context Protocol) servers enable AI agents to interact with devices, network traces, and profiling tools directly during debugging sessions.
-
-### 1. Remote ADB for Android TV
-
-Connect to your Android TV over the network so all tools (Flashlight, Systrace, logcat) work from your dev machine:
-
-```bash
-# On Android TV: Settings → Developer options → Enable "ADB over network"
-# Note the IP address shown
-
-# From dev machine:
-adb connect :5555
-adb devices # Verify connection
-```
-
-Once connected, all ADB-based tools work as if the device were USB-connected.
-
-### 2. Flashlight — Performance Measurement
-
-[Flashlight](https://docs.flashlight.dev/) measures FPS, CPU usage, memory, and thread utilization on real Android devices. Generates HTML reports with flamegraphs.
-
-```bash
-# Install (macOS/Linux)
-curl https://get.flashlight.dev | bash
-
-# On Apple Silicon, also install Rosetta if needed:
-softwareupdate --install-rosetta --agree-to-license
-
-# Basic measurement (plug in device or use remote ADB):
-flashlight measure
-
-# Automated test with scripted scenarios:
-flashlight test --config flashlight.yaml
-```
-
-**Recommended workflow:**
-
-1. Connect to Android TV via remote ADB
-2. Run `flashlight measure`
-3. Perform the laggy scrolling interaction on the home screen
-4. Stop measurement → get HTML report
-5. Share report with agent for analysis
-
-### 3. mobile-mcp — Agent-Controlled Device Interaction
-
-The `mobile-mcp` MCP server (already configured in this workspace) can interact with Android/iOS devices:
-
-- Take screenshots
-- Tap/swipe at coordinates
-- List UI elements
-- Type text
-
-**Setup for Android TV:**
-
-1. Ensure ADB is connected (step 1 above)
-2. The MCP server auto-detects connected devices
-3. Agent can take screenshots, navigate, and capture state during profiling
-
-### 4. Limelight MCP — Runtime Diagnostics
-
-The `limelight` MCP server (already configured) provides:
-
-- Component render profiling
-- Error investigation
-- State snapshot inspection
-- Network query analysis
-- Timeline correlation
-
-**Usage:** Ask the agent to use Limelight to investigate specific components or errors.
-
-### 5. React DevTools Profiler
-
-```bash
-# Start standalone React DevTools
-npx react-devtools
-
-# In your app's dev menu, connect to the dev server
-# Use the Profiler tab to:
-# - Record interactions
-# - See component render times
-# - Identify "why did this render?" for each component
-# - View flame chart of component tree
-```
-
-Works with remote devices as long as they can reach the dev server on the same network.
-
-### 6. Perfetto / Systrace (Android)
-
-For native-level thread analysis (UI thread frame drops, JS thread blocking):
-
-```bash
-# Record a 10-second trace on connected Android TV:
-adb shell perfetto -o /data/misc/perfetto-traces/trace.pb \
- -t 10s sched freq idle am wm gfx view
-
-# Pull and analyze:
-adb pull /data/misc/perfetto-traces/trace.pb
-# Open at https://ui.perfetto.dev
-```
-
-### 7. React Native Perf Monitor
-
-Built into the dev menu — shows JS and UI thread FPS in real time:
-
-```bash
-# Open dev menu on Android TV:
-adb shell input keyevent 82
-
-# Then select "Show Perf Monitor"
-```
-
-### 8. Agent-Assisted Debugging Workflow
-
-For the most productive debugging loop with an AI agent:
-
-1. **Connect** Android TV via remote ADB (`adb connect :5555`)
-2. **Measure** with Flashlight while reproducing the lag
-3. **Capture** logcat during the interaction: `adb logcat -d > logcat.txt`
-4. **Profile** with React DevTools Profiler (record a session, export JSON)
-5. **Share** all artifacts with the agent:
- - Flashlight HTML report
- - logcat output
- - React DevTools profiler JSON
- - Perfetto trace (if needed)
-6. Agent analyzes and proposes targeted fixes
-7. **Re-measure** after applying fixes to confirm improvement
-
----
-
-## Performance Profiling Workflow
-
-### Why Release Builds Are Required
-
-**You cannot profile performance in the Expo dev client.** Dev mode adds:
-
-- React DevTools instrumentation (~2× slower renders)
-- LogBox / error overlay interception
-- Fast Refresh (Metro HMR) polling
-- Hermes debug mode (unoptimized bytecode)
-- `__DEV__` branches enabling extra logging, warnings, and Sentry debug output
-
-All of these add overhead that masks the real bottlenecks. Performance numbers from dev mode are meaningless.
-
-The Expo Perf Monitor (`Cmd+P` / dev menu → "Show Perf Monitor") works in dev builds and gives a rough FPS reading, but the absolute numbers are not representative of production. Use it only for quick relative comparisons between code changes, never for final benchmarks.
-
-### End-to-End Workflow: Build → Deploy → Profile → Fix → Repeat
-
-#### Step 0: Prerequisites (One-Time Setup)
-
-```bash
-# 1. Install Flashlight (macOS)
-curl https://get.flashlight.dev | bash
-# On Apple Silicon, also:
-softwareupdate --install-rosetta --agree-to-license
-
-# 2. Enable Developer Options on Android TV
-# Settings → About → click "Build Number" 7 times
-# Settings → Developer Options → enable:
-# - USB debugging
-# - ADB over network (note the IP:port shown, e.g. 192.168.1.100:5555)
-
-# 3. Connect ADB to the TV from your dev machine
-adb connect 192.168.1.100:5555
-adb devices # Should show the TV as "device"
-```
-
-#### Step 1: Make Code Changes
-
-Edit source files as normal. No need to run Metro or the dev client.
-
-#### Step 2: Build a Release APK for TV (Local)
-
-Use the EAS local build to produce a signed release APK targeting TV:
-
-```bash
-# Production TV APK (release, optimized, EXPO_TV=1):
-pnpm build:production_apk:tv:local
-# → Outputs: build-*.apk in project root
-```
-
-This runs `eas build --profile production_tv_apk --local`, which:
-
-- Sets `APP_VARIANT=prod` and `EXPO_TV=1`
-- Builds an APK (not AAB) so you can sideload it
-- Uses the production Gradle config (ProGuard/R8, Hermes bytecode compilation)
-
-**Alternative — faster iterative builds** if you've already prebuilt:
-
-```bash
-# Direct Gradle release build (faster, skips EAS overhead):
-cd android && ./gradlew :app:assembleRelease && cd ..
-# → Outputs: android/app/build/outputs/apk/release/app-release.apk
-```
-
-> **Note:** The direct Gradle build doesn't set `EXPO_TV=1` env var. For TV-specific code paths, use the EAS build. For general perf testing (list scrolling, etc.) the Gradle build is fine and significantly faster (~2-3 min vs ~8-10 min).
-
-#### Step 3: Install the APK on the TV
-
-```bash
-# Find the APK (EAS local build output):
-ls -la build-*.apk
-
-# Or Gradle output:
-ls -la android/app/build/outputs/apk/release/app-release.apk
-
-# Install on the connected TV (replaces existing install):
-adb install -r
-
-# If package conflict (dev vs prod variant), uninstall first:
-adb uninstall app.dodora.dodostream
-adb install
-```
-
-#### Step 4: Launch the App on the TV
-
-```bash
-# Launch the app:
-adb shell am start -n app.dodora.dodostream/.MainActivity
-
-# Or if you need to cold start (kill first):
-adb shell am force-stop app.dodora.dodostream
-adb shell am start -n app.dodora.dodostream/.MainActivity
-```
-
-#### Step 5: Profile with Flashlight
-
-```bash
-# Start Flashlight measurement (auto-detects ADB device):
-flashlight measure
-
-# Flashlight will:
-# 1. Prompt you to select the connected device
-# 2. Ask for the app package name: app.dodora.dodostream
-# 3. Start recording FPS, CPU, memory, thread utilization
-#
-# NOW: Perform the test interaction on the TV
-# (e.g., scroll through home screen for 30 seconds)
-#
-# 4. Press Ctrl+C to stop
-# 5. Opens an HTML report in your browser
-```
-
-**Test script** (perform consistently for each comparison):
-
-1. Wait 3 seconds after home screen loads (initial render settles)
-2. Scroll down through all catalog rows (steady speed, ~2 rows/sec)
-3. Scroll back to top
-4. Scroll down fast (hold D-pad down for 5 seconds)
-5. Total: ~30 seconds
-
-#### Step 6: Capture Additional Diagnostics (Optional)
-
-```bash
-# Logcat — capture JS errors, debug logs, native crashes:
-adb logcat -d > logcat_$(date +%Y%m%d_%H%M%S).txt
-
-# Perfetto trace — native thread analysis (UI thread stalls, JS thread blocking):
-adb shell perfetto -o /data/misc/perfetto-traces/trace.pb \
- -t 10s sched freq idle am wm gfx view
-adb pull /data/misc/perfetto-traces/trace.pb
-# Open at https://ui.perfetto.dev
-
-# Memory snapshot:
-adb shell dumpsys meminfo app.dodora.dodostream
-```
-
-#### Step 7: Analyze Results → Fix → Repeat
-
-1. Open the Flashlight HTML report
-2. Look for:
- - **JS FPS drops** → JS thread is blocked (component re-renders, heavy computation)
- - **UI FPS drops** → native thread is blocked (layout, image decoding, focus engine)
- - **CPU spikes during scroll** → too many components mounting/unmounting
- - **Memory growth** → components or closures not being cleaned up
-3. Make targeted code changes based on findings
-4. **Go back to Step 2** — build, deploy, profile again
-5. Compare Flashlight reports side-by-side (save each HTML report with a descriptive name)
-
-### Quick Reference: The Full Loop
-
-```
-┌─────────────────────────────────────────────────────┐
-│ Edit code │
-│ ↓ │
-│ pnpm build:production_apk:tv:local │
-│ ↓ │
-│ adb install -r build-*.apk │
-│ ↓ │
-│ adb shell am start -n │
-│ app.dodora.dodostream/.MainActivity │
-│ ↓ │
-│ flashlight measure │
-│ ↓ │
-│ Perform test interaction on TV (30s) │
-│ ↓ │
-│ Ctrl+C → review HTML report │
-│ ↓ │
-│ Compare with previous report → decide next change │
-│ ↓ │
-│ Repeat from top │
-└─────────────────────────────────────────────────────┘
-```
-
-### Profiling Checklist
-
-- [ ] ADB connected to production TV device (`adb devices` shows it)
-- [ ] Release APK built and installed (not dev client)
-- [ ] Flashlight installed and working (`flashlight --version`)
-- [ ] Baseline measurement taken (current code, before changes)
-- [ ] Test interaction scripted (same steps every time for fair comparison)
-- [ ] Flashlight reports saved with descriptive names (e.g., `flashlight_baseline.html`, `flashlight_option_a.html`)
-
-### Key Metrics to Track
-
-| Metric | Target | Tool |
-| ----------------------------- | ------------------ | ------------------------ |
-| UI Thread FPS | ≥55 fps sustained | Flashlight, Perf Monitor |
-| JS Thread FPS | ≥55 fps sustained | Flashlight, Perf Monitor |
-| Total CPU % during scroll | <50% | Flashlight |
-| Memory during scroll | Stable (no growth) | Flashlight |
-| Frame drop count (10s scroll) | <5 dropped | Perfetto |
-
-### Apple TV Profiling
-
-For tvOS profiling, the workflow is different since Flashlight/ADB don't apply:
-
-1. **Build for Apple TV:**
-
- ```bash
- pnpm build:production:tv:local
- # Or: npx expo run:ios --configuration Release --device
- ```
-
-2. **Profile with Xcode Instruments:**
- - Open Instruments → choose "Time Profiler" or "Core Animation" template
- - Select the Apple TV device
- - Record while performing the test interaction
- - Analyze the flame chart for main thread stalls
-
-3. **MetricKit** (passive): If integrated, collects hang reports and scroll hitch metrics automatically from real usage.
-
----
-
-## Nuclear Options (If Still Not Enough)
-
-If performance remains unacceptable after all the above changes, these are the escalation paths:
-
-### Option A: React Native Skia for Card Rendering
-
-Use `@shopify/react-native-skia` to render media cards as canvas elements, bypassing the React component tree for card rendering entirely. Each card becomes a single Skia drawing call instead of a deep tree of `View` > `Image` > `Text` > `Pressable` nodes.
-
-### Option B: Native Grid Modules
-
-Write the home screen grid as a native Android TV `Leanback` BrowseSupportFragment / tvOS `UICollectionView` and embed via a native module. The native TV frameworks are purpose-built for this exact use case (horizontal rows in a vertical list) and handle focus routing entirely in the UI thread.
-
-### Option C: Evaluate if react-native-tvos is the right choice
-
-React Native TVOS apps _can_ be performant, but the constraints are real:
-
-- **Focus engine is JS-driven** — every focus change crosses the native bridge.
-- **No native recycling for horizontal rows** — unlike Android TV's native RecyclerView.
-- **Pressable focus/blur events are chatty** — high-frequency D-pad navigation creates lots of bridge traffic.
-
-If the app's primary target is TV and the home screen is the core experience, a native TV implementation may ultimately be the right call. This is a last resort after exhausting all optimization paths above.
diff --git a/app.config.ts b/app.config.ts
index cea09b2..7dcd97e 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -79,13 +79,6 @@ export default ({ config }: ConfigContext): ExpoConfig => {
},
],
'expo-libvlc-player',
- [
- '@sentry/react-native/expo',
- {
- organization: process.env.SENTRY_ORG ?? 'dodora',
- project: process.env.SENTRY_PROJECT ?? 'dodostream',
- },
- ],
'./plugins/withRemoteUIBuild',
'./plugins/withReactNativeTVOSPnpmFix',
'./plugins/withAndroidBuildOptimizations',
diff --git a/metro.config.js b/metro.config.js
deleted file mode 100644
index b74591c..0000000
--- a/metro.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const { getSentryExpoConfig } = require('@sentry/react-native/metro');
-
-/** @type {import('expo/metro-config').MetroConfig} */
-
-const config = getSentryExpoConfig(__dirname);
-
-module.exports = config;
diff --git a/package.json b/package.json
index 4703168..6d0d756 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,6 @@
"@react-native-vector-icons/lucide": "^12.4.0",
"@react-navigation/drawer": "^7.5.0",
"@react-navigation/native": "^7.1.6",
- "@sentry/react-native": "^7.2.0",
"@shopify/restyle": "^2.4.5",
"@tanstack/react-query": "^5.90.12",
"drizzle-orm": "^0.45.1",
@@ -87,7 +86,6 @@
"devDependencies": {
"@babel/core": "^7.20.0",
"@react-native-tvos/config-tv": "^0.1.4",
- "@sentry/cli": "^3.0.3",
"@testing-library/react-native": "^13.3.3",
"@types/jest": "^29.5.14",
"@types/node": "^25.0.9",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f356a39..82ca838 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -46,9 +46,6 @@ importers:
'@react-navigation/native':
specifier: ^7.1.6
version: 7.1.25(react-native-tvos@0.81.5-1)(react@19.1.0)
- '@sentry/react-native':
- specifier: ^7.2.0
- version: 7.2.0(expo@54.0.34)(react-native-tvos@0.81.5-1)(react@19.1.0)
'@shopify/restyle':
specifier: ^2.4.5
version: 2.4.5(react-native-tvos@0.81.5-1)(react@19.1.0)
@@ -185,9 +182,6 @@ importers:
'@react-native-tvos/config-tv':
specifier: ^0.1.4
version: 0.1.4(expo@54.0.34)
- '@sentry/cli':
- specifier: ^3.0.3
- version: 3.0.3
'@testing-library/react-native':
specifier: ^13.3.3
version: 13.3.3(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@5.9.3)))(react-native-tvos@0.81.5-1)(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2265,159 +2259,6 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
- '@sentry-internal/browser-utils@10.12.0':
- resolution: {integrity: sha512-dozbx389jhKynj0d657FsgbBVOar7pX3mb6GjqCxslXF0VKpZH2Xks0U32RgDY/nK27O+o095IWz7YvjVmPkDw==}
- engines: {node: '>=18'}
-
- '@sentry-internal/feedback@10.12.0':
- resolution: {integrity: sha512-0+7ceO6yQPPqfxRc9ue/xoPHKcnB917ezPaehGQNfAFNQB9PNTG1y55+8mRu0Fw+ANbZeCt/HyoCmXuRdxmkpg==}
- engines: {node: '>=18'}
-
- '@sentry-internal/replay-canvas@10.12.0':
- resolution: {integrity: sha512-W/z1/+69i3INNfPjD1KuinSNaRQaApjzwb37IFmiyF440F93hxmEYgXHk3poOlYYaigl2JMYbysGPWOiXnqUXA==}
- engines: {node: '>=18'}
-
- '@sentry-internal/replay@10.12.0':
- resolution: {integrity: sha512-/1093gSNGN5KlOBsuyAl33JkzGiG38kCnxswQLZWpPpR6LBbR1Ddb18HjhDpoQNNEZybJBgJC3a5NKl43C2TSQ==}
- engines: {node: '>=18'}
-
- '@sentry/babel-plugin-component-annotate@4.3.0':
- resolution: {integrity: sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==}
- engines: {node: '>= 14'}
-
- '@sentry/browser@10.12.0':
- resolution: {integrity: sha512-lKyaB2NFmr7SxPjmMTLLhQ7xfxaY3kdkMhpzuRI5qwOngtKt4+FtvNYHRuz+PTtEFv4OaHhNNbRn6r91gWguQg==}
- engines: {node: '>=18'}
-
- '@sentry/cli-darwin@2.55.0':
- resolution: {integrity: sha512-jGHE7SHHzqXUmnsmRLgorVH6nmMmTjQQXdPZbSL5tRtH8d3OIYrVNr5D72DSgD26XAPBDMV0ibqOQ9NKoiSpfA==}
- engines: {node: '>=10'}
- os: [darwin]
-
- '@sentry/cli-darwin@3.0.3':
- resolution: {integrity: sha512-aSRtAc2BZQpxccPVykgxy6JFR/KcJ8P8szxBuu+9Sk+GHi/inrY7y5+yIHWiiZ8Zs3Vkq17c8Q+vumQMBiaKmA==}
- engines: {node: '>=18'}
- os: [darwin]
-
- '@sentry/cli-linux-arm64@2.55.0':
- resolution: {integrity: sha512-jNB/0/gFcOuDCaY/TqeuEpsy/k52dwyk1SOV3s1ku4DUsln6govTppeAGRewY3T1Rj9B2vgIWTrnB8KVh9+Rgg==}
- engines: {node: '>=10'}
- cpu: [arm64]
- os: [linux, freebsd, android]
-
- '@sentry/cli-linux-arm64@3.0.3':
- resolution: {integrity: sha512-knBC4IpSo23euWbp0PxiVXmwIZPpa8qP1mPrCWd4t0Is5wooBR2F/TgRm3dFZpTXU+YLuKSlCvzOCOLi1W7mNg==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [linux, freebsd, android]
-
- '@sentry/cli-linux-arm@2.55.0':
- resolution: {integrity: sha512-ATjU0PsiWADSPLF/kZroLZ7FPKd5W9TDWHVkKNwIUNTei702LFgTjNeRwOIzTgSvG3yTmVEqtwFQfFN/7hnVXQ==}
- engines: {node: '>=10'}
- cpu: [arm]
- os: [linux, freebsd, android]
-
- '@sentry/cli-linux-arm@3.0.3':
- resolution: {integrity: sha512-GuVXHrzvJCgCoR3tX/QUNyTL7OdUT6FuPqURQWppef23VNS3wkOSjYu4SKh/EO826jJMGzlK1ytflbETx/z2xw==}
- engines: {node: '>=18'}
- cpu: [arm]
- os: [linux, freebsd, android]
-
- '@sentry/cli-linux-i686@2.55.0':
- resolution: {integrity: sha512-8LZjo6PncTM6bWdaggscNOi5r7F/fqRREsCwvd51dcjGj7Kp1plqo9feEzYQ+jq+KUzVCiWfHrUjddFmYyZJrg==}
- engines: {node: '>=10'}
- cpu: [x86, ia32]
- os: [linux, freebsd, android]
-
- '@sentry/cli-linux-i686@3.0.3':
- resolution: {integrity: sha512-NIbjkNQdKG9gV3q9yb5Iz4+N9q7IHLdreumsALrlDUlM4X/HW5IQupfhYRwxqXVgqwOU/SiV5u6XXS59gIXX8A==}
- engines: {node: '>=18'}
- cpu: [x86, ia32]
- os: [linux, freebsd, android]
-
- '@sentry/cli-linux-x64@2.55.0':
- resolution: {integrity: sha512-5LUVvq74Yj2cZZy5g5o/54dcWEaX4rf3myTHy73AKhRj1PABtOkfexOLbF9xSrZy95WXWaXyeH+k5n5z/vtHfA==}
- engines: {node: '>=10'}
- cpu: [x64]
- os: [linux, freebsd, android]
-
- '@sentry/cli-linux-x64@3.0.3':
- resolution: {integrity: sha512-u2k3EEe+LmKFgF/v6QMvffg+prtny28G5mPBB4eHzJb3RnAt2V1+6dGJc/mDKdnltY7uyDfgIJTEZeGmiVyAWA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [linux, freebsd, android]
-
- '@sentry/cli-win32-arm64@2.55.0':
- resolution: {integrity: sha512-cWIQdzm1pfLwPARsV6dUb8TVd6Y3V1A2VWxjTons3Ift6GvtVmiAe0OWL8t2Yt95i8v61kTD/6Tq21OAaogqzA==}
- engines: {node: '>=10'}
- cpu: [arm64]
- os: [win32]
-
- '@sentry/cli-win32-arm64@3.0.3':
- resolution: {integrity: sha512-ZJ7eHkkAE6lRaLnvo17nPzFF3eBKNWURZ/BpSGJ7KsTmzLetUA6lU7XPYwlwRTWIVn49SdZFdgS5ThFmcs0pwA==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [win32]
-
- '@sentry/cli-win32-i686@2.55.0':
- resolution: {integrity: sha512-ldepCn2t9r4I0wvgk7NRaA7coJyy4rTQAzM66u9j5nTEsUldf66xym6esd5ZZRAaJUjffqvHqUIr/lrieTIrVg==}
- engines: {node: '>=10'}
- cpu: [x86, ia32]
- os: [win32]
-
- '@sentry/cli-win32-i686@3.0.3':
- resolution: {integrity: sha512-y1+Mdwrvm7P2HDqUzHmyBaMOtEA6U59OX/fVcZ9LfWXDg9vK4+UrFccNpbLZSV+uyfCI7q80op9v+uWrTelfTw==}
- engines: {node: '>=18'}
- cpu: [x86, ia32]
- os: [win32]
-
- '@sentry/cli-win32-x64@2.55.0':
- resolution: {integrity: sha512-4hPc/I/9tXx+HLTdTGwlagtAfDSIa2AoTUP30tl32NAYQhx9a6niUbPAemK2qfxesiufJ7D2djX83rCw6WnJVA==}
- engines: {node: '>=10'}
- cpu: [x64]
- os: [win32]
-
- '@sentry/cli-win32-x64@3.0.3':
- resolution: {integrity: sha512-+8TfjfJRiiRS1vaCCMpIKWj6fonQzJdQs3lWSvtjyYQyrzca4JlWcAFdGOmt+7h0HbVIF0dt0YznrAyXfoi6zg==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [win32]
-
- '@sentry/cli@2.55.0':
- resolution: {integrity: sha512-cynvcIM2xL8ddwELyFRSpZQw4UtFZzoM2rId2l9vg7+wDREPDocMJB9lEQpBIo3eqhp9JswqUT037yjO6iJ5Sw==}
- engines: {node: '>= 10'}
- hasBin: true
-
- '@sentry/cli@3.0.3':
- resolution: {integrity: sha512-JZiRmzAgAK1HCtmCBnb7Ppko3Lrxfk2Uqi+qxr2lBwOHdLtiNNJB0Jajnpd3tykRMfqufe5R1TfijSpE/I1Saw==}
- engines: {node: '>= 18'}
- hasBin: true
-
- '@sentry/core@10.12.0':
- resolution: {integrity: sha512-Jrf0Yo7DvmI/ZQcvBnA0xKNAFkJlVC/fMlvcin+5IrFNRcqOToZ2vtF+XqTgjRZymXQNE8s1QTD7IomPHk0TAw==}
- engines: {node: '>=18'}
-
- '@sentry/react-native@7.2.0':
- resolution: {integrity: sha512-rjqYgEjntPz1sPysud78wi4B9ui7LBVPsG6qr8s/htLMYho9GPGFA5dF+eqsQWqMX8NDReAxNkLTC4+gCNklLQ==}
- hasBin: true
- peerDependencies:
- expo: '>=49.0.0'
- react: '>=17.0.0'
- react-native: '>=0.65.0'
- peerDependenciesMeta:
- expo:
- optional: true
-
- '@sentry/react@10.12.0':
- resolution: {integrity: sha512-TpqgdoYbkf5JynmmW2oQhHQ/h5w+XPYk0cEb/UrsGlvJvnBSR+5tgh0AqxCSi3gvtp82rAXI5w1TyRPBbhLDBw==}
- engines: {node: '>=18'}
- peerDependencies:
- react: ^16.14.0 || 17.x || 18.x || 19.x
-
- '@sentry/types@10.12.0':
- resolution: {integrity: sha512-sKGj3l3V8ZKISh2Tu88bHfnm5ztkRtSLdmpZ6TmCeJdSM9pV+RRd6CMJ0RnSEXmYHselPNUod521t2NQFd4W1w==}
- engines: {node: '>=18'}
-
'@shopify/restyle@2.4.5':
resolution: {integrity: sha512-xV970ytoX0oLac9+fMOMxcXcV9nv5BtwdNk50ZoyhqSLyiGamLJMpL/lMZIQHsmfDT8sksbUx94zQnZBztMgww==}
peerDependencies:
@@ -5545,9 +5386,6 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
- proxy-from-env@1.1.0:
- resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
-
psl@1.15.0:
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
@@ -8981,147 +8819,6 @@ snapshots:
'@rtsao/scc@1.1.0': {}
- '@sentry-internal/browser-utils@10.12.0':
- dependencies:
- '@sentry/core': 10.12.0
-
- '@sentry-internal/feedback@10.12.0':
- dependencies:
- '@sentry/core': 10.12.0
-
- '@sentry-internal/replay-canvas@10.12.0':
- dependencies:
- '@sentry-internal/replay': 10.12.0
- '@sentry/core': 10.12.0
-
- '@sentry-internal/replay@10.12.0':
- dependencies:
- '@sentry-internal/browser-utils': 10.12.0
- '@sentry/core': 10.12.0
-
- '@sentry/babel-plugin-component-annotate@4.3.0': {}
-
- '@sentry/browser@10.12.0':
- dependencies:
- '@sentry-internal/browser-utils': 10.12.0
- '@sentry-internal/feedback': 10.12.0
- '@sentry-internal/replay': 10.12.0
- '@sentry-internal/replay-canvas': 10.12.0
- '@sentry/core': 10.12.0
-
- '@sentry/cli-darwin@2.55.0':
- optional: true
-
- '@sentry/cli-darwin@3.0.3':
- optional: true
-
- '@sentry/cli-linux-arm64@2.55.0':
- optional: true
-
- '@sentry/cli-linux-arm64@3.0.3':
- optional: true
-
- '@sentry/cli-linux-arm@2.55.0':
- optional: true
-
- '@sentry/cli-linux-arm@3.0.3':
- optional: true
-
- '@sentry/cli-linux-i686@2.55.0':
- optional: true
-
- '@sentry/cli-linux-i686@3.0.3':
- optional: true
-
- '@sentry/cli-linux-x64@2.55.0':
- optional: true
-
- '@sentry/cli-linux-x64@3.0.3':
- optional: true
-
- '@sentry/cli-win32-arm64@2.55.0':
- optional: true
-
- '@sentry/cli-win32-arm64@3.0.3':
- optional: true
-
- '@sentry/cli-win32-i686@2.55.0':
- optional: true
-
- '@sentry/cli-win32-i686@3.0.3':
- optional: true
-
- '@sentry/cli-win32-x64@2.55.0':
- optional: true
-
- '@sentry/cli-win32-x64@3.0.3':
- optional: true
-
- '@sentry/cli@2.55.0':
- dependencies:
- https-proxy-agent: 5.0.1
- node-fetch: 2.7.0
- progress: 2.0.3
- proxy-from-env: 1.1.0
- which: 2.0.2
- optionalDependencies:
- '@sentry/cli-darwin': 2.55.0
- '@sentry/cli-linux-arm': 2.55.0
- '@sentry/cli-linux-arm64': 2.55.0
- '@sentry/cli-linux-i686': 2.55.0
- '@sentry/cli-linux-x64': 2.55.0
- '@sentry/cli-win32-arm64': 2.55.0
- '@sentry/cli-win32-i686': 2.55.0
- '@sentry/cli-win32-x64': 2.55.0
- transitivePeerDependencies:
- - encoding
- - supports-color
-
- '@sentry/cli@3.0.3':
- dependencies:
- progress: 2.0.3
- proxy-from-env: 1.1.0
- undici: 6.22.0
- which: 2.0.2
- optionalDependencies:
- '@sentry/cli-darwin': 3.0.3
- '@sentry/cli-linux-arm': 3.0.3
- '@sentry/cli-linux-arm64': 3.0.3
- '@sentry/cli-linux-i686': 3.0.3
- '@sentry/cli-linux-x64': 3.0.3
- '@sentry/cli-win32-arm64': 3.0.3
- '@sentry/cli-win32-i686': 3.0.3
- '@sentry/cli-win32-x64': 3.0.3
-
- '@sentry/core@10.12.0': {}
-
- '@sentry/react-native@7.2.0(expo@54.0.34)(react-native-tvos@0.81.5-1)(react@19.1.0)':
- dependencies:
- '@sentry/babel-plugin-component-annotate': 4.3.0
- '@sentry/browser': 10.12.0
- '@sentry/cli': 2.55.0
- '@sentry/core': 10.12.0
- '@sentry/react': 10.12.0(react@19.1.0)
- '@sentry/types': 10.12.0
- react: 19.1.0
- react-native: react-native-tvos@0.81.5-1(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.28.5)(@types/react@19.1.17)(react-native-tvos@0.81.5-1)(react@19.1.0)
- optionalDependencies:
- expo: 54.0.34(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-tvos@0.81.5-1)(react@19.1.0)
- transitivePeerDependencies:
- - encoding
- - supports-color
-
- '@sentry/react@10.12.0(react@19.1.0)':
- dependencies:
- '@sentry/browser': 10.12.0
- '@sentry/core': 10.12.0
- hoist-non-react-statics: 3.3.2
- react: 19.1.0
-
- '@sentry/types@10.12.0':
- dependencies:
- '@sentry/core': 10.12.0
-
'@shopify/restyle@2.4.5(react-native-tvos@0.81.5-1)(react@19.1.0)':
dependencies:
react: 19.1.0
@@ -12832,8 +12529,6 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
- proxy-from-env@1.1.0: {}
-
psl@1.15.0:
dependencies:
punycode: 2.3.1
diff --git a/src/app/(app)/(tabs)/settings/_layout.tsx b/src/app/(app)/(tabs)/settings/_layout.tsx
index da32b01..804aac6 100644
--- a/src/app/(app)/(tabs)/settings/_layout.tsx
+++ b/src/app/(app)/(tabs)/settings/_layout.tsx
@@ -65,12 +65,6 @@ export default function SettingsLayout() {
title: t('menu.about.title'),
}}
/>
-
-
-
- );
-}
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index bb663a2..a2cefac 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -28,25 +28,8 @@ import { Container } from '@/components/basic/Container';
import { Button } from '@/components/basic/Button';
import { AppStartAnimation } from '@/components/basic/AppStartAnimation';
import { ToastContainer } from '@/components/basic/Toast';
-import * as Sentry from '@sentry/react-native';
-import { isSentryEnabled, SENTRY_DSN } from '@/utils/sentry';
-import { createDebugLogger } from '@/utils/debug';
import { sqliteDb, initializeDatabase, runSqliteDataMigration } from '@/db';
-const debug = createDebugLogger('layout');
-if (isSentryEnabled) {
- debug('Initializing Sentry with DSN:', SENTRY_DSN);
- Sentry.init({
- dsn: SENTRY_DSN,
- tracesSampleRate: 0.2,
- enableLogs: true,
- profilesSampleRate: 0.3,
- replaysOnErrorSampleRate: 1.0,
- replaysSessionSampleRate: 0,
- integrations: [Sentry.mobileReplayIntegration()],
- });
-}
-
SplashScreen.preventAutoHideAsync();
export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
@@ -153,5 +136,4 @@ function ThemedAppRoot() {
);
}
-const AppLayout = isSentryEnabled ? Sentry.wrap(Layout) : Layout;
-export default AppLayout;
+export default Layout;
diff --git a/src/components/settings/AboutSettingsContent.tsx b/src/components/settings/AboutSettingsContent.tsx
index 64016d9..82a5f0f 100644
--- a/src/components/settings/AboutSettingsContent.tsx
+++ b/src/components/settings/AboutSettingsContent.tsx
@@ -1,11 +1,10 @@
-import { FC, memo, useCallback, useMemo, useRef } from 'react';
+import { FC, memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView } from 'react-native-gesture-handler';
import { Ionicons } from '@expo/vector-icons';
import * as Linking from 'expo-linking';
import { showToast } from '@/store/toast.store';
import { useTheme } from '@shopify/restyle';
-import { useRouter } from 'expo-router';
import type { Theme } from '@/theme/theme';
import { Box, Text } from '@/theme/theme';
import { AppLogo } from '@/components/basic/AppLogo';
@@ -73,9 +72,6 @@ const AboutLinkRow: FC = memo(({ item, onPress }) => {
AboutLinkRow.displayName = 'AboutLinkRow';
-const DEVELOPER_TAP_COUNT = 5;
-const TAP_TIMEOUT_MS = 2000;
-
/**
* About settings content component
* Shows app metadata and useful support links
@@ -83,14 +79,9 @@ const TAP_TIMEOUT_MS = 2000;
export const AboutSettingsContent: FC = memo(() => {
const { t } = useTranslation('settings');
const theme = useTheme();
- const router = useRouter();
const debug = useDebugLogger('AboutSettingsContent');
const info = useAppInfo();
- // Developer mode tap counter
- const tapCountRef = useRef(0);
- const lastTapTimeRef = useRef(0);
-
const releaseCheckOnStartup = useAppSettingsStore((state) => state.releaseCheckOnStartup);
const setReleaseCheckOnStartup = useAppSettingsStore((state) => state.setReleaseCheckOnStartup);
@@ -148,35 +139,6 @@ export const AboutSettingsContent: FC = memo(() => {
[t]
);
- const handleVersionTap = useCallback(() => {
- const now = Date.now();
-
- // Reset counter if too much time has passed since last tap
- if (now - lastTapTimeRef.current > TAP_TIMEOUT_MS) {
- tapCountRef.current = 0;
- }
-
- lastTapTimeRef.current = now;
- tapCountRef.current += 1;
-
- debug('versionTap', { count: tapCountRef.current });
-
- const remaining = DEVELOPER_TAP_COUNT - tapCountRef.current;
-
- if (remaining > 0 && remaining <= 3) {
- showToast({
- title: t('about.developer_mode_tap', { count: remaining }),
- duration: TOAST_DURATION_SHORT,
- });
- }
-
- if (tapCountRef.current >= DEVELOPER_TAP_COUNT) {
- tapCountRef.current = 0;
- debug('developerModeActivated');
- router.push('/(app)/(tabs)/settings/developer');
- }
- }, [debug, router, t]);
-
const handleOpenLink = useCallback(
async (item: AboutLinkItem) => {
try {
@@ -238,11 +200,9 @@ export const AboutSettingsContent: FC = memo(() => {
-
-
- {info.appVersion}
-
-
+
+ {info.appVersion}
+
diff --git a/src/components/settings/DeveloperSettingsContent.tsx b/src/components/settings/DeveloperSettingsContent.tsx
deleted file mode 100644
index c0fa566..0000000
--- a/src/components/settings/DeveloperSettingsContent.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import { FC, memo, useCallback, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { ScrollView } from 'react-native';
-import { Ionicons } from '@expo/vector-icons';
-import { showToast } from '@/store/toast.store';
-import * as Updates from 'expo-updates';
-import { useTheme } from '@shopify/restyle';
-import type { Theme } from '@/theme/theme';
-import { Box, Text } from '@/theme/theme';
-import { Focusable } from '@/components/basic/Focusable';
-import { Input } from '@/components/basic/Input';
-import { SettingsCard } from '@/components/settings/SettingsCard';
-import { SettingsRow } from '@/components/settings/SettingsRow';
-import { TOAST_DURATION_SHORT } from '@/constants/ui';
-import { useDebugLogger } from '@/utils/debug';
-import { useDeveloperStore } from '@/store/developer.store';
-import { getFocusableBackgroundColor } from '@/utils/focus-colors';
-
-/**
- * Developer settings content component.
- * Provides access to developer-only settings like Sentry DSN configuration.
- * Only accessible by tapping the app version 5 times.
- */
-export const DeveloperSettingsContent: FC = memo(() => {
- const { t } = useTranslation('settings');
- const theme = useTheme();
- const debug = useDebugLogger('DeveloperSettingsContent');
-
- const sentryDsn = useDeveloperStore((state) => state.sentryDsn);
- const setSentryDsn = useDeveloperStore((state) => state.setSentryDsn);
- const resetSentryDsn = useDeveloperStore((state) => state.resetSentryDsn);
-
- const [dsnInput, setDsnInput] = useState(sentryDsn);
- const [isRestarting, setIsRestarting] = useState(false);
-
- const handleSaveAndRestart = useCallback(async () => {
- debug('saveAndRestart', { dsn: dsnInput });
-
- // Save the DSN
- setSentryDsn(dsnInput.trim());
-
- showToast({
- title: t('developer.sentry.dsn_saved'),
- message: t('developer.sentry.restarting'),
- duration: TOAST_DURATION_SHORT,
- });
-
- setIsRestarting(true);
-
- // Small delay to show the toast
- setTimeout(async () => {
- try {
- await Updates.reloadAsync();
- } catch (error) {
- debug('restartError', { error });
- // If expo-updates reload fails (dev mode), show a toast
- showToast({
- title: t('developer.sentry.restart_required'),
- message: t('developer.sentry.restart_required_desc'),
- preset: 'warning',
- });
- setIsRestarting(false);
- }
- }, 500);
- }, [dsnInput, setSentryDsn, debug, t]);
-
- const handleReset = useCallback(() => {
- debug('resetDsn');
- resetSentryDsn();
- setDsnInput(process.env.EXPO_PUBLIC_SENTRY_DSN ?? '');
-
- showToast({
- title: t('developer.sentry.dsn_reset'),
- message: t('developer.sentry.reset_desc'),
- duration: TOAST_DURATION_SHORT,
- });
- }, [resetSentryDsn, debug, t]);
-
- const hasChanges = dsnInput.trim() !== sentryDsn;
-
- return (
-
-
-
-
- {t('developer.title')}
-
- {t('developer.desc')}
-
-
-
-
-
-
-
-
-
-
-
-
- {({ isFocused }) => (
-
-
- {t('developer.sentry.reset_to_default')}
-
- )}
-
-
-
-
-
- {({ isFocused }) => (
-
-
-
- {isRestarting
- ? t('developer.sentry.restarting_btn')
- : t('developer.sentry.save_and_restart')}
-
-
- )}
-
-
-
-
-
-
-
-
-
- {sentryDsn || t('developer.config.not_set')}
-
-
-
-
-
- {sentryDsn ? t('developer.config.yes') : t('developer.config.no')}
-
-
-
-
-
- );
-});
-
-DeveloperSettingsContent.displayName = 'DeveloperSettingsContent';
diff --git a/src/i18n/de/settings.json b/src/i18n/de/settings.json
index 41c98aa..336ffbe 100644
--- a/src/i18n/de/settings.json
+++ b/src/i18n/de/settings.json
@@ -116,7 +116,6 @@
"could_not_open_link": "Link konnte nicht geöffnet werden",
"could_not_open_release": "Release konnte nicht geöffnet werden",
"try_again_later": "Bitte versuchen Sie es später erneut.",
- "developer_mode_tap": "Noch {{count}} Klicks bis zum Entwicklermodus",
"repo_title": "GitHub Repository",
"repo_desc": "Quellcode und Releases",
"bug_title": "Fehler melden",
@@ -212,32 +211,6 @@
"sync": "Sync",
"last_synced": "Zuletzt synchronisiert"
},
- "developer": {
- "title": "Entwicklereinstellungen",
- "desc": "Diese Einstellungen dienen Entwicklungs- und Debugging-Zwecken.",
- "sentry": {
- "title": "Sentry",
- "dsn": "DSN",
- "dsn_desc": "Der Sentry Data Source Name für die Fehlerberichterstattung",
- "dsn_saved": "Sentry DSN gespeichert",
- "restarting": "App wird neu gestartet, um Änderungen zu übernehmen...",
- "restart_required": "Neustart erforderlich",
- "restart_required_desc": "Bitte schließe die App und öffne sie erneut, um die neue Sentry DSN zu übernehmen.",
- "dsn_reset": "DSN zurückgesetzt",
- "reset_desc": "Starte die App neu, um die Änderungen zu übernehmen.",
- "reset_to_default": "Auf Standard zurücksetzen",
- "save_and_restart": "Speichern & Neustarten",
- "restarting_btn": "Startet neu..."
- },
- "config": {
- "title": "Aktuelle Konfiguration",
- "stored_dsn": "Gespeicherte DSN",
- "sentry_enabled": "Sentry aktiviert",
- "not_set": "(nicht gesetzt)",
- "yes": "Ja",
- "no": "Nein"
- }
- },
"home": {
"catalogs": "Kataloge",
"continue_watching": "Weitersehen",
diff --git a/src/i18n/en/settings.json b/src/i18n/en/settings.json
index 827a1a0..c5c771d 100644
--- a/src/i18n/en/settings.json
+++ b/src/i18n/en/settings.json
@@ -38,10 +38,6 @@
"about": {
"title": "About",
"desc": "App info, version, and useful links"
- },
- "developer": {
- "title": "Developer",
- "desc": "Development and debugging tools"
}
},
"ui": {
@@ -120,7 +116,6 @@
"could_not_open_link": "Could not open link",
"could_not_open_release": "Could not open release",
"try_again_later": "Please try again later.",
- "developer_mode_tap": "{{count}} taps to developer mode",
"repo_title": "GitHub Repository",
"repo_desc": "Source code and releases",
"bug_title": "Report a Bug",
@@ -216,32 +211,6 @@
"sync": "Sync",
"last_synced": "Last synced"
},
- "developer": {
- "title": "Developer Settings",
- "desc": "These settings are for development and debugging purposes.",
- "sentry": {
- "title": "Sentry",
- "dsn": "DSN",
- "dsn_desc": "The Sentry Data Source Name for error reporting",
- "dsn_saved": "Sentry DSN saved",
- "restarting": "Restarting app to apply changes...",
- "restart_required": "Restart Required",
- "restart_required_desc": "Please close and reopen the app to apply the new Sentry DSN.",
- "dsn_reset": "DSN reset",
- "reset_desc": "Restart the app to apply changes.",
- "reset_to_default": "Reset to Default",
- "save_and_restart": "Save & Restart",
- "restarting_btn": "Restarting..."
- },
- "config": {
- "title": "Current Configuration",
- "stored_dsn": "Stored DSN",
- "sentry_enabled": "Sentry Enabled",
- "not_set": "(not set)",
- "yes": "Yes",
- "no": "No"
- }
- },
"home": {
"catalogs": "Catalogs",
"continue_watching": "Continue watching",
diff --git a/src/i18n/pt/settings.json b/src/i18n/pt/settings.json
index 416ff1b..e21b65d 100644
--- a/src/i18n/pt/settings.json
+++ b/src/i18n/pt/settings.json
@@ -38,10 +38,6 @@
"about": {
"title": "Sobre",
"desc": "Informação da app, versão e ligações úteis"
- },
- "developer": {
- "title": "Programador",
- "desc": "Ferramentas de desenvolvimento e depuração"
}
},
"ui": {
@@ -120,7 +116,6 @@
"could_not_open_link": "Não foi possível abrir a ligação",
"could_not_open_release": "Não foi possível abrir o lançamento",
"try_again_later": "Tenta novamente mais tarde.",
- "developer_mode_tap": "{{count}} toques para o modo de programador",
"repo_title": "Repositório GitHub",
"repo_desc": "Código fonte e lançamentos",
"bug_title": "Reportar um Bug",
@@ -216,32 +211,6 @@
"sync": "Sincronizar",
"last_synced": "Última sincronização"
},
- "developer": {
- "title": "Definições de Programador",
- "desc": "Estas definições são para desenvolvimento e depuração.",
- "sentry": {
- "title": "Sentry",
- "dsn": "DSN",
- "dsn_desc": "O Sentry Data Source Name para relatório de erros",
- "dsn_saved": "DSN do Sentry guardado",
- "restarting": "A reiniciar a app para aplicar alterações...",
- "restart_required": "Reinício Necessário",
- "restart_required_desc": "Fecha e reabre a app para aplicar o novo DSN do Sentry.",
- "dsn_reset": "DSN reposto",
- "reset_desc": "Reinicia a app para aplicar alterações.",
- "reset_to_default": "Repor Predefinição",
- "save_and_restart": "Guardar e Reiniciar",
- "restarting_btn": "A reiniciar..."
- },
- "config": {
- "title": "Configuração Atual",
- "stored_dsn": "DSN Armazenado",
- "sentry_enabled": "Sentry Ativo",
- "not_set": "(não definido)",
- "yes": "Sim",
- "no": "Não"
- }
- },
"home": {
"catalogs": "Catálogos",
"continue_watching": "Continuar a ver",
diff --git a/src/store/developer.store.ts b/src/store/developer.store.ts
deleted file mode 100644
index 8db1119..0000000
--- a/src/store/developer.store.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import { create } from 'zustand';
-import { createJSONStorage, persist } from 'zustand/middleware';
-
-const DEFAULT_SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN ?? '';
-
-interface DeveloperState {
- sentryDsn: string;
- setSentryDsn: (dsn: string) => void;
- resetSentryDsn: () => void;
-}
-
-export const useDeveloperStore = create()(
- persist(
- (set) => ({
- sentryDsn: DEFAULT_SENTRY_DSN,
- setSentryDsn: (dsn) => set({ sentryDsn: dsn }),
- resetSentryDsn: () => set({ sentryDsn: DEFAULT_SENTRY_DSN }),
- }),
- {
- name: 'developer-storage',
- storage: createJSONStorage(() => AsyncStorage),
- partialize: (state) => ({ sentryDsn: state.sentryDsn }),
- }
- )
-);
-
-/**
- * Get the current Sentry DSN synchronously.
- * This should be used during app initialization before React renders.
- */
-export function getSentryDsn(): string {
- const state = useDeveloperStore.getState();
- return state.sentryDsn || DEFAULT_SENTRY_DSN;
-}
-
-/**
- * Check if Sentry is enabled based on the current DSN.
- */
-export function isSentryEnabled(): boolean {
- return !!getSentryDsn();
-}
diff --git a/src/utils/sentry.ts b/src/utils/sentry.ts
deleted file mode 100644
index e0625e6..0000000
--- a/src/utils/sentry.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { getSentryDsn, isSentryEnabled as checkSentryEnabled } from '@/store/developer.store';
-
-/**
- * Get the Sentry DSN from the developer store.
- * Falls back to the env variable if the store value is empty.
- */
-export const SENTRY_DSN = getSentryDsn();
-
-/**
- * Check if Sentry is enabled based on the current DSN.
- * Only enabled when a Sentry DSN is explicitly set.
- */
-export const isSentryEnabled = checkSentryEnabled();