Note: This example demonstrates the technical implementation of an in-app interactive toast mechanism for testing and illustration purposes. In a production environment, this component should be integrated with a backend service that intelligently triggers toast messages based on user preferences and viewing patterns.
Introduction
TV applications face unique challenges in promoting in-app content discovery, where static banners are often ignored and traditional notifications can disrupt the viewing experience. This guide demonstrates how to implement an engaging toast message that alerts users to available content and allows them to hold the play/pause button to directly start streaming.
This solution demonstrates how to modify the Vega Video Sample App, our reference implementation for video streaming applications, with “Beautiful Whale Tail Uvita Costa Rica” video as our example content.

Solution
Prerequisites
// Required dependencies in package.json
{
"dependencies": {
"react-native-toast-message": "^2.2.0",
}
}
Step 1: Custom Toast Component
A custom React component that implements a TV-optimized interactive toast notification. Features a clean rectangular design positioned for optimal visibility with real-time progress feedback during user interaction.
File: src/components/ProgressToast.tsx
/**
* Simplified Progress Toast Component for react-native-toast-message
*
* This component displays a simplified toast notification positioned on the right
* side of the screen with a clean rectangular design for TV viewing.
*
* Features:
* - Simple rectangular layout with text above and progress bar below
* - Positioned on right side of screen, 2% from top
* - Minimal design optimized for TV screens
* - Real-time progress bar that fills during button hold
*/
import React from 'react';
import { View, Text, StyleSheet, Animated } from 'react-native';
import { BaseToast, ToastProps } from 'react-native-toast-message';
/**
* Props interface extending the base ToastProps from react-native-toast-message
*
* @param progress - Current progress value from 0 to 1 (0% to 100%)
* @param isHolding - Boolean indicating if user is actively holding the play button
*/
interface ProgressToastProps extends ToastProps {
props?: {
progress?: number; // Progress value: 0.0 = 0%, 1.0 = 100%
isHolding?: boolean; // true when user is actively pressing play button
};
}
/**
* Main ProgressToast functional component
*
* Renders a simplified toast with:
* 1. Text at top: "Hold ⏯️ to start..."
* 2. Progress bar at bottom showing fill progress
*
* @param props - Component props including progress and holding state
* @param rest - All other standard ToastProps passed through to BaseToast
* @returns JSX.Element - The rendered simplified progress toast component
*/
export const ProgressToast: React.FC<ProgressToastProps> = ({ props, text1, text2, ...rest }) => {
// Extract progress and holding state from props with safe defaults
const progress = props?.progress ?? 0; // Default to 0% progress if not provided
const isHolding = props?.isHolding ?? false; // Default to not holding if not provided
return (
<View style={styles.toastContainer}>
{/* Main text display - uses text1 from Toast.show() or fallback */}
<Text style={styles.instructionText}>
{text1 || 'Hold ⏯️ to start...'}
</Text>
{/* Secondary text display - shows text2 if provided */}
{text2 && (
<Text style={styles.secondaryText}>
{text2}
</Text>
)}
{/* Progress bar container */}
<View style={styles.progressBackground}>
{/* Animated progress fill - width changes dynamically based on progress */}
<Animated.View
style={[
styles.progressFill,
{
// Dynamic width: converts 0-1 progress to 0%-100% width
width: `${progress * 100}%`,
// Dynamic color: blue when idle, green when actively holding
backgroundColor: isHolding ? '#00ff00' : '#007bff',
}
]}
/>
</View>
</View>
);
};
/**
* StyleSheet for the simplified ProgressToast component
*
* Positioned on the right side of screen, 20% from top with a clean rectangular
* update setting here to optimize for TV viewing distances.
*/
const styles = StyleSheet.create({
// Main toast container - simple rectangle positioned on right side
toastContainer: {
position: 'absolute', // Absolute positioning for screen placement
top: '20%', // 2% from top of screen as requested
right: '2%', // 2% from right edge of screen
width: 480, // Fixed width for consistent appearance
backgroundColor: 'rgba(26, 26, 26, 0.95)', // Semi-transparent dark background
borderRadius: 8, // Subtle rounded corners
padding: 16, // Internal padding for content spacing
shadowColor: '#000', // Black shadow for depth
shadowOffset: { width: 2, height: 2 }, // Shadow positioning for visibility
shadowOpacity: 0.8, // Strong shadow for TV visibility
shadowRadius: 6, // Moderate shadow blur
elevation: 10, // Android elevation for material depth
borderWidth: 1, // Thin border for definition
borderColor: 'rgba(0, 123, 255, 0.3)', // Subtle blue border matching theme
},
// Instruction text - displays text1 parameter from Toast.show()
instructionText: {
fontSize: 22, // Large text for TV readability
fontWeight: '600', // Semi-bold for prominence
color: '#ffffff', // Pure white for maximum contrast
textAlign: 'center', // Center the text horizontally
marginBottom: 8, // Space between main text and secondary text
textShadowColor: 'rgba(0, 0, 0, 0.8)', // Text shadow for readability
textShadowOffset: { width: 1, height: 1 }, // Shadow positioning
textShadowRadius: 2, // Shadow blur radius
},
// Secondary text - displays text2 parameter from Toast.show()
secondaryText: {
fontSize: 20, // Smaller secondary text
fontWeight: '500', // Medium weight for readability
color: '#e0e0e0', // Light gray for secondary information
textAlign: 'center', // Center the text horizontally
marginBottom: 12, // Space between secondary text and progress bar
textShadowColor: 'rgba(0, 0, 0, 0.8)', // Text shadow for readability
textShadowOffset: { width: 1, height: 1 }, // Shadow positioning
textShadowRadius: 2, // Shadow blur radius
},
// Progress bar background - the gray track behind the progress fill
progressBackground: {
width: '100%', // Full width of container
height: 12, // TV-optimized height for visibility
backgroundColor: '#333333', // Dark gray background for contrast
borderRadius: 6, // Rounded ends matching half the height
overflow: 'hidden', // Clips progress fill to container bounds
marginTop: 4, // Small space above progress bar
},
// Progress bar fill - the colored indicator that shows progress
progressFill: {
height: '100%', // Fill entire height of background container
borderRadius: 6, // Match container border radius
// Note: width and backgroundColor are set dynamically in the component
// Width ranges from 0% to 100% based on progress value
// Background color changes from blue to green when holding
},
});
/**
* Toast configuration object for react-native-toast-message library
*
* This configuration maps the custom 'progress' toast type to our ProgressToast component.
* When Toast.show({ type: 'progress', ... }) is called anywhere in the app,
* it will render this custom component instead of the default toast style.
*
* Usage: Toast.show({ type: 'progress', text1: 'Title', text2: 'Description', props: { progress: 0.5 } })
*/
export const toastConfig = {
progress: (props: ProgressToastProps) => <ProgressToast {...props} />,
};
Step 2: Business Logic
A custom hook that manages the complete lifecycle of play/pause button interactions and toast visibility. The hook handles all aspects of this interaction, including automatic toast display timing, smooth progress animations, completion detection with callbacks, and proper cleanup of all timers and intervals. Using Vega’s TVEventHandler, the hook provides reliable remote control event management and precise long press detection for the toast’s hold-to-start interaction.
File: src/hooks/useLongPressToast.tsx
/**
* Custom hook for handling long press interactions with progress toast
*
* User Flow:
* 1. 20 seconds after navigating to screen → Toast appears
* 2. If user presses target button and continues holding → Progress bar fills over configured duration with real-time updates
* 3. If user releases early → Toast disappears, no action triggered
* 4. If user holds for full duration → Toast disappears, action is triggered
* 5. After timeout period if user does not interact → Toast disappears
* 6. Toast appears every time user visits the screen (not just once per session)
*/
import { useState, useRef, useCallback, useEffect } from 'react';
import { useTVEventHandler } from '@amazon-devices/react-native-kepler';
import Toast from 'react-native-toast-message';
/**
* Supported TV remote button types for long press interactions
*/
export type TVButtonType = 'playpause' | 'select' | 'menu' | 'back' | 'up' | 'down' | 'left' | 'right';
/**
* Configuration interface for the useLongPressToast hook
*
* @param onLongPressComplete - Callback function executed when user completes the long press gesture
* @param targetButton - Which TV remote button to listen for (default: 'playpause')
* @param autoShowDelay - Time in ms before toast appears automatically (default: 20000ms)
* @param longPressDuration - Time in ms required to complete the long press gesture (default: 2000ms)
* @param toastTimeout - Time in ms before toast auto-hides if no interaction (default: 5000ms)
* @param initialText - Primary text shown when toast first appears
* @param holdingText - Primary text shown while user is pressing button
* @param secondaryText - Secondary/descriptive text (optional)
* @param completionText - Text shown when long press completes (optional)
* @param isScreenFocused - Whether the screen is currently focused/visible (prevents toast on background screens)
*/
interface UseLongPressToastProps {
onLongPressComplete: () => void;
targetButton?: TVButtonType;
autoShowDelay?: number;
longPressDuration?: number;
toastTimeout?: number;
initialText?: string;
holdingText?: string;
secondaryText?: string;
completionText?: string;
isScreenFocused?: boolean;
}
/**
* Internal state interface for tracking all aspects of the long press interaction
*
* @param isHolding - Whether user is currently holding the target button
* @param progress - Progress value from 0.0 to 1.0 representing completion percentage
* @param toastVisible - Whether the toast is currently displayed on screen
* @param hasAutoShown - Whether the toast has been shown in this session (resets on navigation)
* @param hasCompleted - Whether user has successfully completed a long press (permanent until navigation)
*/
interface LongPressState {
isHolding: boolean;
progress: number;
toastVisible: boolean;
hasAutoShown: boolean;
hasCompleted: boolean;
}
/**
* Main useLongPressToast hook
*
* Manages the complete lifecycle of long press interactions including:
* - Automatic toast display based on timing
* - TV remote event handling for specified button
* - Progress tracking with smooth animations
* - Completion detection and callback execution
* - Proper cleanup of all timers and intervals
*
* @param config - Configuration object with callbacks and timing settings
* @returns Object containing current state and manual control functions
*/
export const useLongPressToast = ({
onLongPressComplete,
targetButton = 'playpause',
autoShowDelay = 5000,
longPressDuration = 2000,
toastTimeout = 5000,
initialText = 'Hold Button to Continue',
holdingText = 'Keep Holding...',
secondaryText,
completionText = 'Action Completed',
isScreenFocused = true
}: UseLongPressToastProps) => {
// Main state management for all interaction aspects
const [state, setState] = useState<LongPressState>({
isHolding: false,
progress: 0,
toastVisible: false,
hasAutoShown: false,
hasCompleted: false,
});
// Ref management for cleanup and timing - all intervals and timeouts
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const autoShowTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const toastTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const holdStartTimeRef = useRef<number>(0);
const isProgressCompleteRef = useRef<boolean>(false);
// Use ref to track toast visibility for TV event handler to avoid stale closure
const toastVisibleRef = useRef<boolean>(false);
/**
* Shows the initial toast notification
*
* Only shows if toast is not already visible and hasn't been shown yet.
* Sets up auto-hide timer to remove toast after configured timeout.
*/
const showInitialToast = useCallback(() => {
if (state.toastVisible || state.hasAutoShown || state.hasCompleted) return;
Toast.show({
type: 'progress',
text1: initialText,
text2: secondaryText,
visibilityTime: toastTimeout,
autoHide: false,
props: {
progress: 0,
isHolding: false,
},
});
setState(prev => ({
...prev,
toastVisible: true,
hasAutoShown: true,
}));
// Update ref to keep in sync
toastVisibleRef.current = true;
// Set timeout to hide toast if no interaction
toastTimeoutRef.current = setTimeout(() => {
hideToast();
}, toastTimeout);
}, [state.toastVisible, state.hasAutoShown, state.hasCompleted, toastTimeout, initialText, secondaryText]);
/**
* Hides the toast notification and cleans up associated timers
*
* Safely clears any existing toast timeout and calls Toast.hide() to remove
* the toast from the screen. Updates state to reflect toast is no longer visible.
*/
const hideToast = useCallback(() => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
toastTimeoutRef.current = null;
}
Toast.hide();
setState(prev => ({
...prev,
toastVisible: false,
}));
// Update ref to keep in sync
toastVisibleRef.current = false;
}, []);
/**
* Starts the progress tracking when target button is pressed
*
* Initializes progress tracking with smooth 50ms updates for 60fps animation.
* Clears any existing toast timeout to prevent auto-hide during interaction.
* Updates toast display in real-time to show progress and holding state.
*/
const startProgress = useCallback(() => {
if (state.isHolding) return; // Prevent multiple starts
// Clear any existing toast timeout since user is now interacting
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
toastTimeoutRef.current = null;
}
holdStartTimeRef.current = Date.now();
isProgressCompleteRef.current = false;
setState(prev => ({
...prev,
isHolding: true,
progress: 0,
toastVisible: true,
}));
// Update ref to keep in sync
toastVisibleRef.current = true;
// Start progress interval - updates every 50ms for smooth animation
progressIntervalRef.current = setInterval(() => {
const elapsed = Date.now() - holdStartTimeRef.current;
const newProgress = Math.min(elapsed / longPressDuration, 1);
setState(prev => ({
...prev,
progress: newProgress,
}));
// Update toast with current progress and holding state
Toast.show({
type: 'progress',
text1: holdingText,
text2: secondaryText,
autoHide: false,
props: {
progress: newProgress,
isHolding: true,
},
});
// Check if progress is complete (reached 100%)
if (newProgress >= 1 && !isProgressCompleteRef.current) {
isProgressCompleteRef.current = true;
completeProgress();
}
}, 50); // Update every 50ms for smooth 60fps-like animation
}, [state.isHolding, longPressDuration, holdingText, secondaryText]);
/**
* Stops the progress tracking when target button is released early
*
* Cleans up the progress interval and hides the toast if the long press gesture
* wasn't completed. Resets progress and holding state.
*/
const stopProgress = useCallback(() => {
if (!state.isHolding) return;
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
progressIntervalRef.current = null;
}
// Hide toast if not completed (user released early)
if (!isProgressCompleteRef.current) {
hideToast();
}
setState(prev => ({
...prev,
isHolding: false,
progress: 0,
}));
}, [state.isHolding, hideToast]);
/**
* Completes the progress and triggers the configured action
*
* Called when user successfully holds the target button for the full duration.
* Cleans up progress interval, shows brief completion message, and triggers
* the onLongPressComplete callback after a short delay.
*/
const completeProgress = useCallback(() => {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
progressIntervalRef.current = null;
}
// Show brief completion message with consistent custom styling
if (completionText) {
Toast.show({
type: 'progress', // Use 'progress' type to get our custom ProgressToast styling
text1: completionText,
//text2: secondaryText,
visibilityTime: 2000,
props: {
progress: 1, // Show completed state (100%)
isHolding: false, // No longer holding
},
});
}
setState(prev => ({
...prev,
isHolding: false,
progress: 1,
toastVisible: false,
hasCompleted: true, // Mark as completed
}));
// Update ref to prevent further interactions
toastVisibleRef.current = false;
// Trigger action callback after short delay for smooth UX
setTimeout(() => {
onLongPressComplete();
}, 500);
}, [onLongPressComplete, completionText, secondaryText]);
/**
* TV Event Handler for remote button events
*
* Processes remote control events from the TV platform. Only responds to
* the specified target button events when the toast is visible. Handles both button
* press (start progress) and release (stop progress) events.
*
* @param evt - TV event object containing eventType and eventKeyAction
*/
const handleTVEvent = useCallback((evt: any) => {
// Only handle target button events when toast is visible - use ref to avoid stale closure
if (evt.eventType !== targetButton || !toastVisibleRef.current) {
return;
}
console.log(`TV Event - Type: ${evt.eventType}, Action: ${evt.eventKeyAction}, Toast Visible: ${toastVisibleRef.current}`);
if (evt.eventKeyAction === 0) {
// Button pressed (start of hold) - eventKeyAction 0 means button down
startProgress();
} else if (evt.eventKeyAction === 1) {
// Button released (end of hold) - eventKeyAction 1 means button up
stopProgress();
}
}, [startProgress, stopProgress, targetButton]);
// Register TV event handler with the Kepler framework
useTVEventHandler(handleTVEvent);
/**
* Auto-show toast effect - triggers toast appearance after delay
*
* Only activates when the screen is focused to prevent toasts on background screens.
* Resets the hasAutoShown flag every time the hook initializes (every screen visit)
* and sets up a timer to show the toast after the configured delay.
*/
useEffect(() => {
// Only proceed if screen is currently focused
if (!isScreenFocused) {
return;
}
// Reset hasAutoShown flag when component mounts (every time user visits screen)
setState(prev => ({ ...prev, hasAutoShown: false }));
// Set up timer to show toast after delay
autoShowTimeoutRef.current = setTimeout(() => {
showInitialToast();
}, autoShowDelay);
return () => {
if (autoShowTimeoutRef.current) {
clearTimeout(autoShowTimeoutRef.current);
}
};
}, [autoShowDelay, showInitialToast, isScreenFocused]);
/**
* Cleanup effect - ensures all timers and intervals are properly cleaned up
*
* Critical for preventing memory leaks and ensuring clean component unmounting.
* Clears all three types of timers used by the hook.
*/
useEffect(() => {
return () => {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
if (autoShowTimeoutRef.current) {
clearTimeout(autoShowTimeoutRef.current);
}
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
}, []);
// Return current state and manual control functions for external use
return {
isHolding: state.isHolding,
progress: state.progress,
toastVisible: state.toastVisible,
hasAutoShown: state.hasAutoShown,
showInitialToast,
hideToast,
};
};
Step 2: App/HomeScreen Integration
File: src/App.tsx
App.tsx: Added one line to register our custom toast component so the app knows to use our ProgressToast design instead of the default toast styling.
// Add to existing App.tsx imports
import Toast from 'react-native-toast-message';
import { toastConfig } from './src/components/ProgressToast';
const App = () => {
return (
<Provider store={store}>
<ThemeProvider theme={theme}>
<NavigationContainer>
<AppStack />
</NavigationContainer>
{/* Toast provider with custom progress toast configuration */}
<Toast config={toastConfig} />
</ThemeProvider>
</Provider>
);
};
export default App;
File: src/screens/HomeScreen.tsx (Integration Example)
Please modify logic for your specific implementation for surfacing Toast message
HomeScreen.tsx: Added the useLongPressToast hook with isScreenFocused: isFocused to ensure the toast only appears when the HomeScreen is actually visible, preventing it from showing up on other screens like the video player.
// Add to existing HomeScreen.tsx imports
import { useLongPressToast } from '../hooks/useLongPressToast';
// Add inside the HomeScreen component
const HomeScreen = ({ navigation }: AppStackScreenProps<Screens.HOME_SCREEN>) => {
// Track if this screen is currently focused to prevent background toasts
const isFocused = useIsFocused();
// ... existing HomeScreen code ...
/**
* Navigates to the player screen with titleData content
* Triggered by the long press toast hook when user completes hold gesture
*/
const navigateToStream = useCallback(() => {
console.log('Navigating to stream');
// Prepare navigation parameters with titleData
const params = {
data: tileData,
sendDataOnBack: () => {}, // Callback for when user returns from player
};
try {
// Navigate to player screen
navigation.navigate(Screens.PLAYER_SCREEN, params);
} catch (error) {
console.error('Failed to navigate to stream:', error);
// If navigation fails, go back to previous screen
navigation.goBack();
}
}, [navigation]);
// Initialize the long press toast hook with dynamic titleData
// Only activate when HomeScreen is focused to prevent background toasts
useLongPressToast({
onLongPressComplete: navigateToStream,
initialText: tileData.title + ' starting soon!',
holdingText: 'Keep Holding...',
secondaryText: 'Hold ⏯️ to start',
completionText: 'Starting stream',
isScreenFocused: isFocused, // Prevents toast on background screens
});
// ... rest of existing HomeScreen code ...
};
Conclusion
Whether implementing this for live events, premium content, or personalized recommendations, this solution offers an approach to capturing viewer attention and driving engagement in your application. The code provided serves as a foundation that can be built upon to create engaging, user-friendly TV experiences that drive content discovery and user satisfaction.
To create a more personalized, user-respectful experience, consider implementing user opt-in mechanisms for specific content types (such as favorite sports teams or TV shows), smart notification scheduling that respects viewing sessions, and rate limiting to prevent notification fatigue. It’s crucial to avoid interrupting active viewing and to prioritize notifications based on user interests and viewing history. The implementation provided serves as a foundation that can be customized to integrate with your content management and notification systems, while maintaining a balance between user engagement and viewing experience.*