Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 40 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ This project demonstrates how to share React Native code across multiple TV plat

### Supported Platforms

| Platform | Target Devices |
|----------|----------------|
| Vega (Kepler) | Fire TV |
| Expo TV | Android TV, Apple TV |
| Expo Web | Browser |
| Platform | Target Devices |
| ------------- | -------------------- |
| Vega (Kepler) | Fire TV |
| Expo TV | Android TV, Apple TV |
| Expo Web | Browser |

## Project Structure

Expand Down Expand Up @@ -52,6 +52,7 @@ This project demonstrates how to share React Native code across multiple TV plat
## Prerequisites

### Core Requirements

- [Node.js](https://nodejs.org/) (v18 or higher)
- [Yarn](https://yarnpkg.com/) (v4.5.0 or higher)
- [Git](https://git-scm.com/)
Expand Down Expand Up @@ -80,6 +81,15 @@ yarn
# Build the Vega/Fire TV app (debug)
yarn vega:build

# Run on Vega Virtual Device (Mac M-series)
yarn vega:vvd:mseries

# Run on Vega Virtual Device (Intel Mac)
yarn vega:vvd:intel

# Run on a Fire TV Stick (pass DSN)
yarn vega:firetv <DSN>

# Prebuild Expo TV native projects
yarn expotv:prebuild

Expand Down Expand Up @@ -114,19 +124,32 @@ Run on a Vega virtual device:
```bash
vega virtual-device start

# Mac M-series (aarch64) - using yarn script
yarn vega:vvd:mseries

# Intel Mac (x86_64) - using yarn script
yarn vega:vvd:intel

# Or directly with the Vega CLI
# Mac M-series (aarch64)
vega run-app packages/vega/build/aarch64-debug/vega_aarch64.vpkg
vega run-app packages/vega/build/aarch64-debug/vega_aarch64.vpkg com.amazondeveloper.hellosharedworkspace.main -d VirtualDevice

# Intel Mac (x86_64)
vega run-app packages/vega/build/x86_64-debug/vega_x86_64.vpkg
vega run-app packages/vega/build/x86_64-debug/vega_x86_64.vpkg com.amazondeveloper.hellosharedworkspace.main -d VirtualDevice
```

Run on a Fire TV Stick:
Run on a Fire TV Stick (replace `<DSN>` with your device serial number):

```bash
vega run-app packages/vega/build/armv7-release/vega_armv7.vpkg
# Using the yarn script
yarn vega:firetv <DSN>

# Or directly
vega run-app packages/vega/build/armv7-release/vega_armv7.vpkg com.amazondeveloper.hellosharedworkspace.main -d <DSN>
```

The `vega run-app` command takes the form `vega run-app <Vpkg path> <App ID> -d <device>`. The App ID is the interactive component id from `manifest.toml` (here, `com.amazondeveloper.hellosharedworkspace.main`). Use `VirtualDevice` for the VVD or the device serial number (DSN) for a Fire TV Stick. See the [Vega CLI reference](https://developer.amazon.com/docs/vega/0.22/cli-tools.html) for details.

[Fast Refresh](https://reactnative.dev/docs/fast-refresh) is available in debug builds. See [Set Up Fast Refresh](https://developer.amazon.com/docs/vega/latest/fast-refresh.html) for configuration.

### Expo TV
Expand Down Expand Up @@ -154,14 +177,15 @@ yarn expotv:web

## Tech Stack

| | Expo TV | Vega (Fire TV) |
|---|---------|----------------|
| Framework | Expo SDK 54 | Kepler (@amazon-devices/react-native-kepler ^2.0.0) |
| React | 19.1.0 | 18.2.0 |
| React Native | react-native-tvos 0.81-stable | 0.72.0 |
| TypeScript | ~5.9.2 | 4.8.4 |
| | Expo TV | Vega (Fire TV) |
| ------------ | ----------------------------- | --------------------------------------------------- |
| Framework | Expo SDK 54 | Kepler (@amazon-devices/react-native-kepler ^2.0.0) |
| React | 19.1.0 | 18.2.0 |
| React Native | react-native-tvos 0.81-stable | 0.72.0 |
| TypeScript | ~5.9.2 | 4.8.4 |

The shared package (`@multitv/shared`) provides:

- UI components: Header, HeaderLogo (with platform-specific variants), Tile, ApiDemo, IconReactNativeAnimated
- HomeScreen with tile-based navigation and focus management
- Scaling utilities for TV display dimensions (1920x1080 base). The scaling approach used here is simple and works for a demo, but for production apps you may want a more robust solution like responsive layouts or a design system.
Expand Down Expand Up @@ -194,6 +218,7 @@ See [Vega CLI Installation](https://developer.amazon.com/docs/vega/latest/instal
### Fast Refresh Not Working

Fast Refresh only works with debug builds:

- `vega_aarch64.vpkg` from `aarch64-debug/` for M-series Mac
- `vega_x86_64.vpkg` from `x86_64-debug/` for Intel Mac
- `vega_armv7.vpkg` from `armv7-debug/` for Fire TV Stick
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"scripts": {
"clean": "yarn workspaces foreach --all run clean ; rm -rf node_modules",
"vega:build": "yarn workspace @multitv/vega run build:debug",
"vega:vvd:mseries": "yarn workspace @multitv/vega run vvd:mseries",
"vega:vvd:intel": "yarn workspace @multitv/vega run vvd:intel",
"vega:firetv": "yarn workspace @multitv/vega run firetv",
"expotv": "yarn workspace @multitv/expotv",
"expotv:prebuild": "yarn workspace @multitv/expotv run prebuild",
"expotv:android": "yarn workspace @multitv/expotv run android",
Expand Down
70 changes: 38 additions & 32 deletions packages/shared/src/components/Tile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {memo, useCallback} from 'react';
import {
Image,
ImageSourcePropType,
Expand All @@ -10,45 +10,51 @@ import {
import {scaleFontSize, scaleWidth} from '../utils/scaling';

export interface TileProps {
id: string;
label: string;
icon: ImageSourcePropType;
isFocused: boolean;
onFocus: () => void;
onFocus: (id: string) => void;
onBlur: () => void;
testID?: string;
accessibilityLabel?: string;
hasTVPreferredFocus?: boolean;
}

export const Tile = ({
label,
icon,
isFocused,
onFocus,
onBlur,
testID,
accessibilityLabel,
hasTVPreferredFocus,
}: TileProps) => {
return (
<TouchableOpacity
style={[styles.tile, isFocused ? styles.focused : styles.default]}
onFocus={onFocus}
onBlur={onBlur}
testID={testID}
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
hasTVPreferredFocus={hasTVPreferredFocus}
activeOpacity={0.8}>
<View style={styles.topHalf}>
<Image source={icon} style={styles.icon} accessible={false} />
</View>
<View style={styles.bottomHalf}>
<Text style={styles.label}>{label}</Text>
</View>
</TouchableOpacity>
);
};
export const Tile = memo(
({
id,
label,
icon,
isFocused,
onFocus,
onBlur,
testID,
accessibilityLabel,
hasTVPreferredFocus,
}: TileProps) => {
const handleFocus = useCallback(() => onFocus(id), [id, onFocus]);

return (
<TouchableOpacity
style={[styles.tile, isFocused ? styles.focused : styles.default]}
onFocus={handleFocus}
onBlur={onBlur}
testID={testID}
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
hasTVPreferredFocus={hasTVPreferredFocus}
activeOpacity={0.8}>
<View style={styles.topHalf}>
<Image source={icon} style={styles.icon} accessible={false} />
</View>
<View style={styles.bottomHalf}>
<Text style={styles.label}>{label}</Text>
</View>
</TouchableOpacity>
);
},
);

const styles = StyleSheet.create({
tile: {
Expand Down Expand Up @@ -88,6 +94,6 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
textAlign: 'center',
lineHeight: scaleFontSize(52),
includeFontPadding: false,
width: '100%',
},
});
9 changes: 7 additions & 2 deletions packages/shared/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const HomeScreen = () => {
setFocusedTileId(tileId);
}, []);

const handleTileBlur = useCallback(() => {
setFocusedTileId('home');
}, []);

const renderFocusedContent = () => {
if (focusedTileId === 'home') {
return <Header />;
Expand Down Expand Up @@ -49,11 +53,12 @@ export const HomeScreen = () => {
{tiles.map(tile => (
<Tile
key={tile.id}
id={tile.id}
label={tile.label}
icon={tile.icon}
isFocused={focusedTileId === tile.id}
onFocus={() => handleTileFocus(tile.id)}
onBlur={() => setFocusedTileId('home')}
onFocus={handleTileFocus}
onBlur={handleTileBlur}
testID={`tile-${tile.id}`}
accessibilityLabel={tile.accessibilityLabel}
hasTVPreferredFocus={tile.id === 'home'}
Expand Down
7 changes: 5 additions & 2 deletions packages/vega/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"scripts": {
"clean": "rm -rf node_modules buildinfo.json dist build",
"start": "react-native start --port 8083",
"start": "react-native start",
"test": "jest --colors ",
"test:snapshot": "jest --colors --updateSnapshot",
"lint": "eslint src test",
Expand All @@ -16,7 +16,10 @@
"build:debug": "react-native build-kepler --build-type Debug",
"build:app": "npm-run-all build:release",
"build": "npm-run-all build:release",
"release": "npm-run-all lint test build:app"
"release": "npm-run-all lint test build:app",
"vvd:mseries": "vega run-app build/aarch64-debug/vega_aarch64.vpkg com.amazondeveloper.hellosharedworkspace.main -d VirtualDevice",
"vvd:intel": "vega run-app build/x86_64-debug/vega_x86_64.vpkg com.amazondeveloper.hellosharedworkspace.main -d VirtualDevice",
"firetv": "vega run-app build/armv7-release/vega_armv7.vpkg com.amazondeveloper.hellosharedworkspace.main -d"
},
"dependencies": {
"@amazon-devices/kepler-module-resolver-preset": "^0.1.15",
Expand Down