Practice and reinforce the concepts from Lesson 13
Master push notifications by:
Time Limit: 10 minutes
Install Expo Notifications:
npx expo install expo-notifications expo-device expo-constants
Basic Notification Setup:
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
// Configure notification behavior
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
export async function registerForPushNotificationsAsync() {
let token;
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
token = (await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig.extra.eas.projectId,
})).data;
console.log('Push token:', token);
} else {
alert('Must use physical device for Push Notifications');
}
return token;
}
✅ Checkpoint: Successfully get push token on device!
Time Limit: 5 minutes
Send your first local notification:
import { useState, useEffect } from 'react';
export default function NotificationDemo() {
const [expoPushToken, setExpoPushToken] = useState('');
useEffect(() => {
registerForPushNotificationsAsync().then(token => {
setExpoPushToken(token);
});
}, []);
const sendLocalNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "Hello! 👋",
body: 'Your first notification is working!',
data: { screen: 'Home' },
},
trigger: { seconds: 2 },
});
};
return (
<View style={styles.container}>
<Button title="Send Test Notification" onPress={sendLocalNotification} />
</View>
);
}
✅ Checkpoint: Notification appears on your device!
Build a fitness app with smart notification system:
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, TouchableOpacity, ScrollView, Switch } from 'react-native';
import * as Notifications from 'expo-notifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Notification categories with different strategies
const NOTIFICATION_CATEGORIES = {
WORKOUT_REMINDER: {
id: 'workout_reminder',
title: 'Workout Reminder',
description: 'Daily workout reminders',
defaultEnabled: true,
frequency: 'daily',
bestTimes: ['09:00', '18:00'] // 9 AM and 6 PM
},
WATER_REMINDER: {
id: 'water_reminder',
title: 'Hydration Alert',
description: 'Stay hydrated reminders',
defaultEnabled: true,
frequency: 'hourly',
bestTimes: ['08:00', '10:00', '12:00', '14:00', '16:00', '18:00']
},
ACHIEVEMENT: {
id: 'achievement',
title: 'Achievement Unlocked',
description: 'Celebrate your progress',
defaultEnabled: true,
frequency: 'event-based',
bestTimes: ['immediately']
},
MOTIVATIONAL: {
id: 'motivational',
title: 'Daily Motivation',
description: 'Inspiring messages',
defaultEnabled: false,
frequency: 'daily',
bestTimes: ['07:00', '20:00']
}
};
class SmartNotificationManager {
constructor() {
this.userPreferences = {};
this.notificationHistory = [];
this.engagementData = {};
}
async loadUserPreferences() {
try {
const prefs = await AsyncStorage.getItem('notification_preferences');
this.userPreferences = prefs ? JSON.parse(prefs) : {};
// Set defaults for new categories
Object.keys(NOTIFICATION_CATEGORIES).forEach(categoryId => {
if (this.userPreferences[categoryId] === undefined) {
this.userPreferences[categoryId] = {
enabled: NOTIFICATION_CATEGORIES[categoryId].defaultEnabled,
bestTime: NOTIFICATION_CATEGORIES[categoryId].bestTimes[0],
frequency: NOTIFICATION_CATEGORIES[categoryId].frequency
};
}
});
await this.saveUserPreferences();
} catch (error) {
console.log('Error loading notification preferences:', error);
}
}
async saveUserPreferences() {
try {
await AsyncStorage.setItem(
'notification_preferences',
JSON.stringify(this.userPreferences)
);
} catch (error) {
console.log('Error saving notification preferences:', error);
}
}
// Smart timing based on user behavior
getBestNotificationTime(categoryId, userActivity = {}) {
const category = NOTIFICATION_CATEGORIES[categoryId];
const userPref = this.userPreferences[categoryId];
// Use machine learning-like logic to optimize timing
if (userActivity.mostActiveHours && userActivity.mostActiveHours.length > 0) {
// Find intersection between best times and user's active hours
const optimalTime = category.bestTimes.find(time => {
const hour = parseInt(time.split(':')[0]);
return userActivity.mostActiveHours.includes(hour);
});
if (optimalTime) return optimalTime;
}
return userPref.bestTime || category.bestTimes[0];
}
async scheduleWorkoutReminder(workoutData = {}) {
if (!this.userPreferences.workout_reminder?.enabled) return;
const motivationalMessages = [
"💪 Your body can do it. It's time to convince your mind!",
"🔥 Every workout brings you closer to your goals!",
"⚡ 30 minutes of movement can change your entire day!",
"🏆 Champions are made in the gym. Are you ready?",
"✨ Your future self will thank you for this workout!"
];
const message = motivationalMessages[Math.floor(Math.random() * motivationalMessages.length)];
const bestTime = this.getBestNotificationTime('workout_reminder');
await Notifications.scheduleNotificationAsync({
content: {
title: "Workout Time! 💪",
body: message,
data: {
category: 'workout_reminder',
action: 'start_workout',
workoutType: workoutData.type || 'general'
},
sound: 'default',
},
trigger: {
hour: parseInt(bestTime.split(':')[0]),
minute: parseInt(bestTime.split(':')[1]),
repeats: true,
},
});
}
async scheduleWaterReminder() {
if (!this.userPreferences.water_reminder?.enabled) return;
const waterMessages = [
"💧 Time for a water break! Stay hydrated!",
"🚰 Your body needs water. Drink up!",
"💙 Hydration checkpoint: How's your water intake?",
"⚡ Boost your energy with some H2O!",
"🌊 Keep the water flowing for optimal performance!"
];
// Schedule multiple water reminders throughout the day
const waterTimes = ['09:00', '11:00', '13:00', '15:00', '17:00'];
for (const time of waterTimes) {
const message = waterMessages[Math.floor(Math.random() * waterMessages.length)];
await Notifications.scheduleNotificationAsync({
content: {
title: "Hydration Time! 💧",
body: message,
data: {
category: 'water_reminder',
action: 'log_water'
},
},
trigger: {
hour: parseInt(time.split(':')[0]),
minute: parseInt(time.split(':')[1]),
repeats: true,
},
});
}
}
async sendAchievementNotification(achievement) {
if (!this.userPreferences.achievement?.enabled) return;
const achievementMessages = {
first_workout: "🎉 Congratulations on your first workout! You're on your way!",
week_streak: "🔥 7-day streak unlocked! You're building an amazing habit!",
monthly_goal: "🏆 Monthly goal achieved! You're absolutely crushing it!",
personal_record: "⭐ New personal record! You're stronger than you think!",
consistency: "💎 Consistency champion! Your dedication is inspiring!"
};
const message = achievementMessages[achievement.type] ||
`🎊 Achievement unlocked: ${achievement.title}!`;
await Notifications.scheduleNotificationAsync({
content: {
title: "Achievement Unlocked! 🏆",
body: message,
data: {
category: 'achievement',
action: 'view_achievement',
achievementId: achievement.id
},
sound: 'default',
badge: 1,
},
trigger: null, // Send immediately
});
}
// Track notification engagement
trackNotificationOpen(notificationData) {
const category = notificationData.category;
if (!this.engagementData[category]) {
this.engagementData[category] = {
sent: 0,
opened: 0,
actions: 0
};
}
this.engagementData[category].opened++;
// Optimize future notifications based on engagement
this.optimizeNotificationTiming(category);
}
optimizeNotificationTiming(category) {
const engagement = this.engagementData[category];
const openRate = engagement.opened / engagement.sent;
// If open rate is low, try different timing
if (openRate < 0.3 && engagement.sent > 5) {
const currentTime = this.userPreferences[category].bestTime;
const availableTimes = NOTIFICATION_CATEGORIES[category].bestTimes;
const nextTimeIndex = (availableTimes.indexOf(currentTime) + 1) % availableTimes.length;
this.userPreferences[category].bestTime = availableTimes[nextTimeIndex];
this.saveUserPreferences();
console.log(`Optimizing ${category} notification time to ${availableTimes[nextTimeIndex]}`);
}
}
}
// Main component
export default function FitnessNotificationApp() {
const [notificationManager] = useState(() => new SmartNotificationManager());
const [preferences, setPreferences] = useState({});
const [pushToken, setPushToken] = useState('');
useEffect(() => {
const setupNotifications = async () => {
// Register for notifications
const token = await registerForPushNotificationsAsync();
setPushToken(token);
// Load user preferences
await notificationManager.loadUserPreferences();
setPreferences(notificationManager.userPreferences);
// Set up notification handlers
const subscription = Notifications.addNotificationResponseReceivedListener(response => {
const notificationData = response.notification.request.content.data;
notificationManager.trackNotificationOpen(notificationData);
// Handle notification actions
handleNotificationAction(notificationData);
});
return () => subscription.remove();
};
setupNotifications();
}, []);
const handleNotificationAction = (data) => {
switch (data.action) {
case 'start_workout':
// Navigate to workout screen
console.log('Starting workout:', data.workoutType);
break;
case 'log_water':
// Open water logging
console.log('Logging water intake');
break;
case 'view_achievement':
// Show achievement details
console.log('Viewing achievement:', data.achievementId);
break;
}
};
const toggleNotificationCategory = async (categoryId, enabled) => {
notificationManager.userPreferences[categoryId].enabled = enabled;
await notificationManager.saveUserPreferences();
setPreferences({...notificationManager.userPreferences});
if (enabled) {
// Schedule notifications for this category
switch (categoryId) {
case 'workout_reminder':
await notificationManager.scheduleWorkoutReminder();
break;
case 'water_reminder':
await notificationManager.scheduleWaterReminder();
break;
}
} else {
// Cancel notifications for this category
await Notifications.cancelAllScheduledNotificationsAsync();
// Re-schedule other enabled categories
await scheduleAllEnabledNotifications();
}
};
const scheduleAllEnabledNotifications = async () => {
for (const [categoryId, pref] of Object.entries(preferences)) {
if (pref.enabled) {
switch (categoryId) {
case 'workout_reminder':
await notificationManager.scheduleWorkoutReminder();
break;
case 'water_reminder':
await notificationManager.scheduleWaterReminder();
break;
}
}
}
};
const testNotification = async (categoryId) => {
switch (categoryId) {
case 'workout_reminder':
await notificationManager.scheduleWorkoutReminder();
break;
case 'achievement':
await notificationManager.sendAchievementNotification({
type: 'first_workout',
id: 'test_achievement',
title: 'Test Achievement'
});
break;
}
};
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Notification Preferences</Text>
{Object.entries(NOTIFICATION_CATEGORIES).map(([categoryId, category]) => (
<View key={categoryId} style={styles.categoryCard}>
<View style={styles.categoryHeader}>
<Text style={styles.categoryTitle}>{category.title}</Text>
<Switch
value={preferences[categoryId]?.enabled || false}
onValueChange={(enabled) => toggleNotificationCategory(categoryId, enabled)}
/>
</View>
<Text style={styles.categoryDescription}>{category.description}</Text>
<TouchableOpacity
style={styles.testButton}
onPress={() => testNotification(categoryId)}
>
<Text style={styles.testButtonText}>Test Notification</Text>
</TouchableOpacity>
</View>
))}
</ScrollView>
);
}
Your Mission:
Add interactive notifications and rich content:
// Interactive notification actions
await Notifications.setNotificationCategoryAsync('workout', [
{
identifier: 'start_now',
buttonTitle: 'Start Now',
options: { opensAppToForeground: true }
},
{
identifier: 'remind_later',
buttonTitle: 'Remind in 1 hour',
options: { opensAppToForeground: false }
},
{
identifier: 'skip_today',
buttonTitle: 'Skip Today',
options: { opensAppToForeground: false }
}
]);
// Rich media notifications
const scheduleRichNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "New Workout Available! 🏋️♀️",
body: "Try this 15-minute HIIT workout",
data: { workoutId: '123' },
categoryIdentifier: 'workout',
attachments: [{
identifier: 'workout-preview',
url: 'https://example.com/workout-preview.jpg',
options: {
typeHint: 'public.jpeg'
}
}]
},
trigger: { seconds: 5 }
});
};
Track and optimize notification performance:
class NotificationAnalytics {
constructor() {
this.events = [];
}
track(eventType, data) {
const event = {
id: Date.now().toString(),
type: eventType,
timestamp: new Date().toISOString(),
data: data
};
this.events.push(event);
this.saveEvents();
// Send to analytics service
this.sendToAnalytics(event);
}
async saveEvents() {
try {
await AsyncStorage.setItem('notification_events', JSON.stringify(this.events));
} catch (error) {
console.log('Error saving events:', error);
}
}
sendToAnalytics(event) {
// Integration with analytics service
console.log('Analytics event:', event);
}
getEngagementRate(category, timeframe = '7d') {
const now = new Date();
const cutoff = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); // 7 days ago
const recentEvents = this.events.filter(event =>
new Date(event.timestamp) > cutoff &&
event.data.category === category
);
const sent = recentEvents.filter(e => e.type === 'notification_sent').length;
const opened = recentEvents.filter(e => e.type === 'notification_opened').length;
return sent > 0 ? (opened / sent * 100).toFixed(1) : 0;
}
getBestPerformingTime(category) {
const openEvents = this.events.filter(e =>
e.type === 'notification_opened' &&
e.data.category === category
);
const hourCounts = {};
openEvents.forEach(event => {
const hour = new Date(event.timestamp).getHours();
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
});
const bestHour = Object.keys(hourCounts).reduce((a, b) =>
hourCounts[a] > hourCounts[b] ? a : b
);
return `${bestHour}:00`;
}
}
Completed Successfully If:
Time Investment: 60 minutes total Difficulty Level: Intermediate Prerequisites: React Native basics, async operations, device permissions Tools Needed: Physical device, Expo push notification service, analytics setup