npx create-expo-app my-amplify-rn-app cd my-amplify-rn-app
# Choose one: Sandbox (local dev) OR Deployed Env npx amplify sandbox # For rapid local development (auto-deploys on save) # --- OR --- npx amplify init # To create a cloud environment (requires `amplify push`)
# Core Amplify & UI for React Native npm install aws-amplify @aws-amplify/ui-react-native # Media Playback (Expo AV) npx expo install expo-av # Navigation (React Navigation) npm install @react-navigation/native @react-navigation/native-stack npx expo install react-native-screens react-native-safe-area-context
// App.tsx (or your main entry point)
import React from 'react';
import { Amplify } from 'aws-amplify';
// Adjust path if amplify_outputs.json is elsewhere
import outputs from './amplify_outputs.json';
import { Authenticator } from '@aws-amplify/ui-react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
// Import your screen components (we'll create these later)
import HomeScreen from './screens/HomeScreen';
import ProtectedMediaScreen from './screens/ProtectedMediaScreen';
// import MyMediaScreen from './screens/MyMediaScreen'; // For private files later
// import UploadScreen from './screens/UploadScreen'; // For uploads later
// Configure Amplify ONCE at the root
Amplify.configure(outputs);
// Define the navigation stack
const Stack = createNativeStackNavigator();
// Component shown AFTER authentication
function AppContent() {
return (
<NavigationContainer>
<Stack.Navigator>
{/* Define screens accessible after login */}
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'Dashboard' }}
/>
<Stack.Screen
name="ProtectedMedia"
component={ProtectedMediaScreen}
options={{ title: 'Protected Media' }}
/>
{/* Add other screens like MyMediaScreen, UploadScreen here */}
</Stack.Navigator>
</NavigationContainer>
);
}
// Main App Component using Authenticator
export default function App() {
return (
// Authenticator handles login UI automatically
<Authenticator.Provider>
<Authenticator>
{/* AppContent is rendered only after successful login */}
<AppContent />
</Authenticator>
</Authenticator.Provider>
);
}
// screens/HomeScreen.tsx
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { useAuthenticator } from '@aws-amplify/ui-react-native'; // Hook to get user/signOut
// Type navigation prop for type safety (adjust 'ProtectedMedia' based on your screen names)
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
type RootStackParamList = { Home: undefined; ProtectedMedia: undefined; /* Add other screens */ };
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
export default function HomeScreen({ navigation }: Props) {
// Get user details and signOut function from Authenticator context
const { user, signOut } = useAuthenticator((context) => [context.user]);
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome, {user?.username || 'User'}!</Text>
<View style={styles.buttonContainer}>
<Button
title="View Protected Media"
onPress={() => navigation.navigate('ProtectedMedia')}
/>
{/* Add buttons for other screens later */}
{/* <Button title="View My Media" onPress={() => navigation.navigate('MyMedia')} /> */}
{/* <Button title="Upload File" onPress={() => navigation.navigate('Upload')} /> */}
</View>
<Button title="Sign Out" onPress={signOut} color="red" />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20 },
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
buttonContainer: { marginVertical: 20, width: '80%' },
});
// components/ProtectedMediaDisplay.tsx
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, Image, Button, StyleSheet, ActivityIndicator, Pressable } from 'react-native';
import { getUrl } from 'aws-amplify/storage';
import { ResizeMode, Video } from 'expo-av'; // Import Expo AV components
import { Audio } from 'expo-av';
interface ProtectedMediaDisplayProps {
path: string;
alt?: string; // Used as accessibility label
style?: object; // Pass custom styles
}
function getMediaType(filePath: string): 'image' | 'video' | 'audio' | 'unknown' {
// (Keep the same getMediaType function from the web version)
const extension = filePath.split('.').pop()?.toLowerCase();
if (!extension) return 'unknown';
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) return 'image'; // Removed svg for simplicity
if (['mp4', 'webm', 'mov'].includes(extension)) return 'video'; // Simplified list
if (['mp3', 'wav', 'm4a', 'aac'].includes(extension)) return 'audio'; // Simplified list
return 'unknown';
}
export default function ProtectedMediaDisplay({ path, alt = "Protected Media", style }: ProtectedMediaDisplayProps) {
const [mediaUrl, setMediaUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mediaType, setMediaType] = useState<'image' | 'video' | 'audio' | 'unknown'>('unknown');
// Specific state/refs for media players
const videoRef = useRef<Video>(null);
const [sound, setSound] = useState<Audio.Sound | null>(null);
const [isPlayingAudio, setIsPlayingAudio] = useState(false);
useEffect(() => {
// --- Cleanup audio on unmount or path change ---
return () => {
sound?.unloadAsync();
};
}, [sound]);
useEffect(() => {
setMediaUrl(null); setLoading(true); setError(null); setMediaType('unknown'); setIsPlayingAudio(false);
// Ensure previous sound is unloaded if path changes
sound?.unloadAsync().then(() => setSound(null)).catch(e => console.log("Error unloading previous sound:", e));
if (!path) {
setError('No media path provided.'); setLoading(false); return;
}
const fetchMediaUrl = async () => {
try {
const type = getMediaType(path);
setMediaType(type);
const result = await getUrl({ path, options: { validateObjectExistence: true } });
setMediaUrl(result.url.toString());
setError(null);
// Pre-configure Audio Session (Good Practice for iOS/Android)
if (type === 'audio') {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
playsInSilentModeIOS: true, // Allow playing even if phone is on silent
staysActiveInBackground: false, // Adjust if background play needed
shouldDuckAndroid: true, // Lower other audio on Android
playThroughEarpieceAndroid: false,
});
}
} catch (err: any) {
console.error(`Error getting URL for ${path}:`, err);
setError(err.name === 'NotFound' ? `Media not found: ${path}` : `Failed to load: ${err.message || err.name}`);
setMediaUrl(null);
} finally {
setLoading(false);
}
};
fetchMediaUrl();
}, [path]); // Re-run if path changes
// --- Audio Playback Logic ---
async function playSound() {
if (!mediaUrl) return;
console.log('Loading Sound');
setIsPlayingAudio(true); // Indicate loading/playing attempt
try {
// Check if sound is already loaded
if(sound) {
console.log('Playing existing sound');
await sound.replayAsync();
} else {
console.log('Loading new sound from:', mediaUrl)
const { sound: newSound } = await Audio.Sound.createAsync(
{ uri: mediaUrl },
{ shouldPlay: true } // Start playing immediately
);
setSound(newSound);
console.log('Playing Sound');
newSound.setOnPlaybackStatusUpdate((status) => {
if (status.isLoaded && status.didJustFinish) {
// Reset button state when finished
setIsPlayingAudio(false);
console.log("Playback finished");
// Optionally unload or prepare for replay
// sound?.setPositionAsync(0); // Rewind
} else if (status.isLoaded && !status.isPlaying && !status.didJustFinish) {
// Handle pausing if pause functionality is added
setIsPlayingAudio(false);
} else if (!status.isLoaded && status.error){
console.error("Playback Error:", status.error);
setError(`Playback Error: ${status.error}`)
setIsPlayingAudio(false);
}
});
}
} catch (error) {
console.error('Failed to load or play sound', error);
setError(`Failed to play audio: ${error}`);
setIsPlayingAudio(false);
}
}
// --- Rendering Logic ---
if (loading) return <ActivityIndicator size="large" color="#007AFF" style={[styles.centered, style]} />;
if (error) return <Text style={[styles.errorText, style]}>{error}</Text>;
if (!mediaUrl) return null;
switch (mediaType) {
case 'image':
return (
<Image
source={{ uri: mediaUrl }}
style={[styles.image, style]} // Combine default and passed styles
resizeMode="contain" // Or 'cover', 'stretch'
accessibilityLabel={alt}
/>
);
case 'video':
return (
<Video
ref={videoRef}
style={[styles.video, style]}
source={{ uri: mediaUrl }}
useNativeControls // Use platform-native video controls
resizeMode={ResizeMode.CONTAIN} // Or COVER
isLooping={false} // Optional
// onPlaybackStatusUpdate={status => setStatus(() => status)} // For custom controls
onError={(e) => { console.error("Video Error:", e); setError(`Video playback error.`);}}
accessibilityLabel={alt}
/>
);
case 'audio':
return (
<View style={[styles.audioContainer, style]}>
<Text style={styles.audioLabel}>{alt}: </Text>
<Button
title={isPlayingAudio ? "Playing..." : "Play Audio"}
onPress={playSound}
disabled={isPlayingAudio && sound != null} // Disable if currently playing/loading
/>
</View>
);
default:
return (
<View style={style}>
<Text style={styles.errorText}>Unsupported type for: {path}</Text>
{/* Optionally add a button to open URL in browser */}
</View>
);
}
}
// --- Basic Styles ---
const styles = StyleSheet.create({
centered: { alignSelf: 'center', marginVertical: 20 },
errorText: { color: 'red', textAlign: 'center', marginVertical: 10 },
image: { width: '100%', aspectRatio: 16/9, marginBottom: 10, alignSelf: 'center' }, // Default aspect ratio
video: { width: '100%', aspectRatio: 16/9, marginBottom: 10, alignSelf: 'center', backgroundColor: 'black'},
audioContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 10, borderWidth: 1, borderColor: '#ccc', borderRadius: 5, marginVertical: 10 },
audioLabel: { marginRight: 10 },
});
// screens/ProtectedMediaScreen.tsx
import React from 'react';
import { ScrollView, View, Text, StyleSheet } from 'react-native';
import ProtectedMediaDisplay from '../components/ProtectedMediaDisplay'; // Adjust path
export default function ProtectedMediaScreen() {
// TODO: Upload these files to the 'protected/' prefix in your S3 bucket
const imageFile = "protected/gs.jpg";
const videoFile = "protected/002.mp4";
const audioFile = "protected/Aws-Amplify-Al-tool-kit.wav"; // Use the EXACT name from S3
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Protected Media</Text>
<View style={styles.mediaItem}>
<Text style={styles.mediaLabel}>Image:</Text>
<ProtectedMediaDisplay path={imageFile} alt="Protected JPG" />
</View>
<View style={styles.mediaItem}>
<Text style={styles.mediaLabel}>Video:</Text>
<ProtectedMediaDisplay path={videoFile} alt="Protected MP4" />
</View>
<View style={styles.mediaItem}>
<Text style={styles.mediaLabel}>Audio:</Text>
<ProtectedMediaDisplay path={audioFile} alt="Protected WAV" />
</View>
{/* Add <ProtectedMediaDisplay> for private files here later, similar to web */}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 15 },
title: { fontSize: 22, fontWeight: 'bold', marginBottom: 20, textAlign: 'center' },
mediaItem: { marginBottom: 30 },
mediaLabel: { fontSize: 16, fontWeight: '600', marginBottom: 8 },
});