By the end of this lesson, you will be able to:
ℹ️ Info Definition: Push notifications are messages sent directly to users' devices that can appear even when the app isn't open. When done right, they're a powerful tool for engagement, retention, and conversion. When done wrong, they drive users to delete your app.
Push notifications can make or break your app:
High-Converting Notifications | App-Killing Notifications |
---|---|
Personalized content | Generic spam messages |
Perfect timing | Random timing |
Clear value proposition | Unclear purpose |
Actionable CTAs | Vague requests |
Relevant to user behavior | Irrelevant broadcasts |
💡 Engagement Insight: Users who enable notifications are 3x more likely to become power users and 5x more likely to make purchases!
# Expo notifications
npx expo install expo-notifications
npx expo install expo-device
npx expo install expo-constants
# Firebase for advanced features
npm install @react-native-firebase/app
npm install @react-native-firebase/messaging
# Analytics and personalization
npm install @react-native-async-storage/async-storage
npm install date-fns
# AI/ML capabilities
npm install @tensorflow/tfjs-react-native
// services/notificationService.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
// Configure notification behavior
Notifications.setNotificationHandler({
handleNotification: async (notification) => {
// AI-powered notification filtering
const shouldShow = await shouldShowNotification(notification);
return {
shouldShowAlert: shouldShow,
shouldPlaySound: shouldShow && isAppropriateTimeForSound(),
shouldSetBadge: true,
};
},
});
interface NotificationPreferences {
enabled: boolean;
categories: {
marketing: boolean;
social: boolean;
updates: boolean;
reminders: boolean;
};
quietHours: {
enabled: boolean;
start: string; // "22:00"
end: string; // "08:00"
};
frequency: 'high' | 'medium' | 'low';
}
class NotificationService {
private token: string | null = null;
private preferences: NotificationPreferences | null = null;
async initialize() {
if (!Device.isDevice) {
console.log('Notifications only work on physical devices');
return false;
}
// Request permissions
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Notification permission denied');
return false;
}
// Get push token
this.token = (await Notifications.getExpoPushTokenAsync()).data;
console.log('Push token:', this.token);
// Load user preferences
await this.loadPreferences();
// Set up notification channels (Android)
if (Platform.OS === 'android') {
await this.setupAndroidChannels();
}
return true;
}
private async setupAndroidChannels() {
await Notifications.setNotificationChannelAsync('social', {
name: 'Social Interactions',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
sound: 'social.wav',
});
await Notifications.setNotificationChannelAsync('marketing', {
name: 'Promotions & Updates',
importance: Notifications.AndroidImportance.DEFAULT,
vibrationPattern: [0, 250],
sound: 'default',
});
await Notifications.setNotificationChannelAsync('reminders', {
name: 'Reminders',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 500],
sound: 'reminder.wav',
});
}
private async loadPreferences(): Promise<void> {
try {
const stored = await AsyncStorage.getItem('notification_preferences');
this.preferences = stored ? JSON.parse(stored) : this.getDefaultPreferences();
} catch (error) {
this.preferences = this.getDefaultPreferences();
}
}
private getDefaultPreferences(): NotificationPreferences {
return {
enabled: true,
categories: {
marketing: true,
social: true,
updates: true,
reminders: true,
},
quietHours: {
enabled: true,
start: '22:00',
end: '08:00',
},
frequency: 'medium',
};
}
async updatePreferences(preferences: Partial<NotificationPreferences>) {
this.preferences = { ...this.preferences!, ...preferences };
try {
await AsyncStorage.setItem(
'notification_preferences',
JSON.stringify(this.preferences)
);
// Sync with server
await this.syncPreferencesWithServer();
} catch (error) {
console.error('Error saving notification preferences:', error);
}
}
private async syncPreferencesWithServer() {
if (!this.token || !this.preferences) return;
try {
await fetch('https://api.yourapp.com/notifications/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: this.token,
preferences: this.preferences,
}),
});
} catch (error) {
console.error('Error syncing preferences:', error);
}
}
// Schedule local notifications
async scheduleNotification(
title: string,
body: string,
data: any,
trigger: Notifications.NotificationTriggerInput
) {
if (!this.preferences?.enabled) return null;
try {
const id = await Notifications.scheduleNotificationAsync({
content: {
title,
body,
data,
sound: 'default',
},
trigger,
});
return id;
} catch (error) {
console.error('Error scheduling notification:', error);
return null;
}
}
// Cancel specific notification
async cancelNotification(notificationId: string) {
await Notifications.cancelScheduledNotificationAsync(notificationId);
}
// Cancel all scheduled notifications
async cancelAllNotifications() {
await Notifications.cancelAllScheduledNotificationsAsync();
}
// Get push token
getPushToken(): string | null {
return this.token;
}
// Get preferences
getPreferences(): NotificationPreferences | null {
return this.preferences;
}
}
// Helper functions
const shouldShowNotification = async (notification: any): Promise<boolean> => {
// AI-powered logic to determine if notification should be shown
// Consider user behavior, context, time, etc.
return true; // Simplified for demo
};
const isAppropriateTimeForSound = (): boolean => {
const now = new Date();
const hour = now.getHours();
// Don't play sounds during typical sleep hours
return hour >= 8 && hour <= 22;
};
export const notificationService = new NotificationService();
// services/smartNotifications.ts
import OpenAI from 'openai';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { format, addHours, isWithinInterval, parseISO } from 'date-fns';
interface UserBehavior {
mostActiveHours: number[];
preferredCategories: string[];
averageSessionLength: number;
lastActiveDate: Date;
conversionEvents: Array<{
type: string;
timestamp: Date;
notificationId?: string;
}>;
notificationHistory: Array<{
id: string;
type: string;
sentAt: Date;
opened: boolean;
converted: boolean;
}>;
}
interface NotificationTemplate {
id: string;
type: 'engagement' | 'conversion' | 'retention' | 'social';
title: string;
body: string;
cta: string;
personalizable: boolean;
abTestVariants?: Array<{
title: string;
body: string;
weight: number;
}>;
}
class SmartNotificationEngine {
private openai: OpenAI;
private userBehavior: Map<string, UserBehavior> = new Map();
constructor(apiKey: string) {
this.openai = new OpenAI({ apiKey });
this.loadUserBehavior();
}
private async loadUserBehavior() {
try {
const stored = await AsyncStorage.getItem('user_behavior');
if (stored) {
const data = JSON.parse(stored);
this.userBehavior = new Map(Object.entries(data));
}
} catch (error) {
console.error('Error loading user behavior:', error);
}
}
private async saveUserBehavior() {
try {
const data = Object.fromEntries(this.userBehavior);
await AsyncStorage.setItem('user_behavior', JSON.stringify(data));
} catch (error) {
console.error('Error saving user behavior:', error);
}
}
trackUserBehavior(userId: string, action: string, metadata?: any) {
const behavior = this.userBehavior.get(userId) || this.getDefaultBehavior();
switch (action) {
case 'app_open':
const hour = new Date().getHours();
if (!behavior.mostActiveHours.includes(hour)) {
behavior.mostActiveHours.push(hour);
behavior.mostActiveHours = behavior.mostActiveHours.slice(-10); // Keep last 10
}
behavior.lastActiveDate = new Date();
break;
case 'session_end':
if (metadata?.sessionLength) {
behavior.averageSessionLength =
(behavior.averageSessionLength + metadata.sessionLength) / 2;
}
break;
case 'notification_opened':
if (metadata?.notificationId) {
const notification = behavior.notificationHistory.find(n => n.id === metadata.notificationId);
if (notification) {
notification.opened = true;
}
}
break;
case 'conversion':
behavior.conversionEvents.push({
type: metadata?.type || 'unknown',
timestamp: new Date(),
notificationId: metadata?.notificationId,
});
break;
}
this.userBehavior.set(userId, behavior);
this.saveUserBehavior();
}
private getDefaultBehavior(): UserBehavior {
return {
mostActiveHours: [],
preferredCategories: [],
averageSessionLength: 0,
lastActiveDate: new Date(),
conversionEvents: [],
notificationHistory: [],
};
}
async generatePersonalizedNotification(
userId: string,
type: 'engagement' | 'conversion' | 'retention',
context?: any
): Promise<{ title: string; body: string; cta: string; timing: Date } | null> {
const behavior = this.userBehavior.get(userId);
if (!behavior) return null;
const prompt = `
Generate a personalized push notification for a mobile app user:
User Profile:
- Most active hours: ${behavior.mostActiveHours.join(', ')}
- Average session length: ${Math.round(behavior.averageSessionLength)}min
- Days since last active: ${Math.floor((Date.now() - behavior.lastActiveDate.getTime()) / (1000 * 60 * 60 * 24))}
- Recent conversions: ${behavior.conversionEvents.slice(-3).map(e => e.type).join(', ')}
- Notification open rate: ${this.calculateOpenRate(behavior)}%
Notification Type: ${type}
Context: ${JSON.stringify(context)}
Create a notification that:
1. Is highly personalized and relevant
2. Has a clear call-to-action
3. Uses psychological triggers appropriate for this user
4. Respects their behavior patterns
Return JSON with: title, body, cta, reasoning
`;
try {
const response = await this.openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
});
const result = JSON.parse(response.choices[0].message.content || '{}');
if (result.title && result.body) {
return {
title: result.title,
body: result.body,
cta: result.cta || 'Open App',
timing: this.calculateOptimalTiming(behavior),
};
}
} catch (error) {
console.error('Error generating personalized notification:', error);
}
return null;
}
private calculateOpenRate(behavior: UserBehavior): number {
const total = behavior.notificationHistory.length;
if (total === 0) return 0;
const opened = behavior.notificationHistory.filter(n => n.opened).length;
return Math.round((opened / total) * 100);
}
private calculateOptimalTiming(behavior: UserBehavior): Date {
const now = new Date();
// Find user's most active hour
const mostActiveHour = behavior.mostActiveHours.length > 0
? behavior.mostActiveHours.sort((a, b) =>
behavior.mostActiveHours.filter(h => h === a).length -
behavior.mostActiveHours.filter(h => h === b).length
)[0]
: 10; // Default to 10 AM
// Schedule for next occurrence of their most active hour
const scheduledTime = new Date();
scheduledTime.setHours(mostActiveHour, 0, 0, 0);
// If that time has passed today, schedule for tomorrow
if (scheduledTime <= now) {
scheduledTime.setDate(scheduledTime.getDate() + 1);
}
return scheduledTime;
}
async runRetentionCampaign(userIds: string[]): Promise<void> {
for (const userId of userIds) {
const behavior = this.userBehavior.get(userId);
if (!behavior) continue;
const daysSinceLastActive = Math.floor(
(Date.now() - behavior.lastActiveDate.getTime()) / (1000 * 60 * 60 * 24)
);
let notificationType: 'engagement' | 'conversion' | 'retention' = 'retention';
let context = {};
if (daysSinceLastActive >= 7) {
notificationType = 'retention';
context = { daysSinceLastActive, urgency: 'high' };
} else if (daysSinceLastActive >= 3) {
notificationType = 'engagement';
context = { daysSinceLastActive, urgency: 'medium' };
}
const notification = await this.generatePersonalizedNotification(
userId,
notificationType,
context
);
if (notification) {
await this.schedulePersonalizedNotification(userId, notification);
}
}
}
private async schedulePersonalizedNotification(
userId: string,
notification: { title: string; body: string; cta: string; timing: Date }
) {
// In a real app, you'd use your notification service
console.log(`Scheduling notification for ${userId}:`, notification);
// Track the scheduled notification
const behavior = this.userBehavior.get(userId)!;
behavior.notificationHistory.push({
id: Date.now().toString(),
type: 'personalized',
sentAt: notification.timing,
opened: false,
converted: false,
});
this.userBehavior.set(userId, behavior);
await this.saveUserBehavior();
}
async optimizeNotificationTiming(userId: string): Promise<{
recommendedHours: number[];
avoidHours: number[];
reasoning: string;
}> {
const behavior = this.userBehavior.get(userId);
if (!behavior) {
return {
recommendedHours: [10, 14, 19],
avoidHours: [0, 1, 2, 3, 4, 5, 6, 7, 23],
reasoning: 'Using default timing patterns',
};
}
const openRateByHour: Record<number, { total: number; opened: number }> = {};
behavior.notificationHistory.forEach(notification => {
const hour = notification.sentAt.getHours();
if (!openRateByHour[hour]) {
openRateByHour[hour] = { total: 0, opened: 0 };
}
openRateByHour[hour].total++;
if (notification.opened) {
openRateByHour[hour].opened++;
}
});
const hourlyRates = Object.entries(openRateByHour)
.map(([hour, stats]) => ({
hour: parseInt(hour),
rate: stats.opened / stats.total,
volume: stats.total,
}))
.filter(h => h.volume >= 3) // Need at least 3 notifications to be significant
.sort((a, b) => b.rate - a.rate);
const recommendedHours = hourlyRates.slice(0, 3).map(h => h.hour);
const avoidHours = hourlyRates.slice(-3).map(h => h.hour);
return {
recommendedHours: recommendedHours.length > 0 ? recommendedHours : [10, 14, 19],
avoidHours: avoidHours.length > 0 ? avoidHours : [0, 1, 2, 3, 4, 5, 6, 7],
reasoning: hourlyRates.length > 0
? `Based on ${behavior.notificationHistory.length} historical notifications`
: 'Insufficient data, using behavioral patterns',
};
}
}
export default SmartNotificationEngine;
// components/NotificationSettings.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
Switch,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { notificationService } from '../services/notificationService';
interface NotificationCategory {
id: keyof NotificationPreferences['categories'];
title: string;
description: string;
icon: keyof typeof Ionicons.glyphMap;
examples: string[];
}
const notificationCategories: NotificationCategory[] = [
{
id: 'social',
title: 'Social Interactions',
description: 'Likes, comments, messages, and friend activities',
icon: 'people-outline',
examples: ['New follower', 'Message received', 'Photo liked'],
},
{
id: 'marketing',
title: 'Promotions & Updates',
description: 'Special offers, product updates, and announcements',
icon: 'megaphone-outline',
examples: ['Flash sale', 'New features', 'Weekly digest'],
},
{
id: 'updates',
title: 'App Updates',
description: 'System notifications and important changes',
icon: 'refresh-outline',
examples: ['Security update', 'Terms changed', 'Maintenance'],
},
{
id: 'reminders',
title: 'Personal Reminders',
description: 'Tasks, deadlines, and scheduled activities',
icon: 'alarm-outline',
examples: ['Daily goals', 'Upcoming events', 'Habit reminders'],
},
];
export const NotificationSettings: React.FC = () => {
const [preferences, setPreferences] = useState(notificationService.getPreferences());
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
loadPreferences();
}, []);
const loadPreferences = async () => {
const prefs = notificationService.getPreferences();
if (prefs) {
setPreferences(prefs);
}
};
const updatePreference = async (key: string, value: any) => {
if (!preferences) return;
const updated = { ...preferences, [key]: value };
setPreferences(updated);
try {
setIsLoading(true);
await notificationService.updatePreferences({ [key]: value });
} catch (error) {
Alert.alert('Error', 'Failed to update notification preferences');
// Revert changes on error
loadPreferences();
} finally {
setIsLoading(false);
}
};
const updateCategoryPreference = async (
category: keyof NotificationPreferences['categories'],
enabled: boolean
) => {
if (!preferences) return;
const updatedCategories = {
...preferences.categories,
[category]: enabled,
};
setPreferences({
...preferences,
categories: updatedCategories,
});
try {
setIsLoading(true);
await notificationService.updatePreferences({ categories: updatedCategories });
} catch (error) {
Alert.alert('Error', 'Failed to update category preferences');
loadPreferences();
} finally {
setIsLoading(false);
}
};
const showCategoryDetails = (category: NotificationCategory) => {
Alert.alert(
category.title,
`${category.description}\\n\\nExamples:\\n${category.examples.map(e => `• ${e}`).join('\\n')}`,
[{ text: 'OK' }]
);
};
if (!preferences) {
return (
<View style={styles.loadingContainer}>
<Text>Loading notification settings...</Text>
</View>
);
}
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Ionicons name="notifications-outline" size={48} color="#007AFF" />
<Text style={styles.headerTitle}>Notification Preferences</Text>
<Text style={styles.headerSubtitle}>
Customize when and how you receive notifications
</Text>
</View>
{/* Master Toggle */}
<View style={styles.section}>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Enable Notifications</Text>
<Text style={styles.settingDescription}>
Turn off to disable all push notifications
</Text>
</View>
<Switch
value={preferences.enabled}
onValueChange={(value) => updatePreference('enabled', value)}
trackColor={{ false: '#E5E5EA', true: '#34C759' }}
thumbColor="#FFFFFF"
disabled={isLoading}
/>
</View>
</View>
{/* Notification Categories */}
{preferences.enabled && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Notification Types</Text>
{notificationCategories.map((category) => (
<View key={category.id} style={styles.categoryRow}>
<TouchableOpacity
style={styles.categoryInfo}
onPress={() => showCategoryDetails(category)}
>
<View style={styles.categoryIcon}>
<Ionicons name={category.icon} size={24} color="#007AFF" />
</View>
<View style={styles.categoryText}>
<Text style={styles.categoryTitle}>{category.title}</Text>
<Text style={styles.categoryDescription}>{category.description}</Text>
</View>
<Ionicons name="information-circle-outline" size={20} color="#C7C7CC" />
</TouchableOpacity>
<Switch
value={preferences.categories[category.id]}
onValueChange={(value) => updateCategoryPreference(category.id, value)}
trackColor={{ false: '#E5E5EA', true: '#34C759' }}
thumbColor="#FFFFFF"
disabled={isLoading}
/>
</View>
))}
</View>
)}
{/* Quiet Hours */}
{preferences.enabled && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quiet Hours</Text>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Enable Quiet Hours</Text>
<Text style={styles.settingDescription}>
Reduce notifications during your sleep hours
</Text>
</View>
<Switch
value={preferences.quietHours.enabled}
onValueChange={(value) =>
updatePreference('quietHours', { ...preferences.quietHours, enabled: value })
}
trackColor={{ false: '#E5E5EA', true: '#34C759' }}
thumbColor="#FFFFFF"
disabled={isLoading}
/>
</View>
{preferences.quietHours.enabled && (
<View style={styles.timePickerContainer}>
<TouchableOpacity style={styles.timePicker}>
<Text style={styles.timePickerLabel}>Start Time</Text>
<Text style={styles.timePickerValue}>{preferences.quietHours.start}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.timePicker}>
<Text style={styles.timePickerLabel}>End Time</Text>
<Text style={styles.timePickerValue}>{preferences.quietHours.end}</Text>
</TouchableOpacity>
</View>
)}
</View>
)}
{/* Frequency Setting */}
{preferences.enabled && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Notification Frequency</Text>
{(['high', 'medium', 'low'] as const).map((frequency) => (
<TouchableOpacity
key={frequency}
style={styles.frequencyOption}
onPress={() => updatePreference('frequency', frequency)}
>
<View style={styles.frequencyInfo}>
<Text style={styles.frequencyTitle}>
{frequency.charAt(0).toUpperCase() + frequency.slice(1)} Frequency
</Text>
<Text style={styles.frequencyDescription}>
{frequency === 'high' && 'Receive all relevant notifications immediately'}
{frequency === 'medium' && 'Receive important notifications with some bundling'}
{frequency === 'low' && 'Receive only critical notifications in daily digest'}
</Text>
</View>
<View style={[
styles.radioButton,
preferences.frequency === frequency && styles.radioButtonSelected
]}>
{preferences.frequency === frequency && (
<View style={styles.radioButtonInner} />
)}
</View>
</TouchableOpacity>
))}
</View>
)}
{/* Advanced Options */}
{preferences.enabled && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Advanced</Text>
<TouchableOpacity style={styles.advancedOption}>
<View style={styles.advancedInfo}>
<Text style={styles.advancedTitle}>Smart Timing</Text>
<Text style={styles.advancedDescription}>
Use AI to optimize notification delivery times
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
</TouchableOpacity>
<TouchableOpacity style={styles.advancedOption}>
<View style={styles.advancedInfo}>
<Text style={styles.advancedTitle}>Notification History</Text>
<Text style={styles.advancedDescription}>
View your past notification activity
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
</TouchableOpacity>
</View>
)}
<View style={styles.footer}>
<Text style={styles.footerText}>
You can change these preferences anytime in Settings.
We respect your choices and will never spam you.
</Text>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F2F2F7',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
header: {
alignItems: 'center',
padding: 30,
backgroundColor: 'white',
marginBottom: 20,
},
headerTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#000',
marginTop: 15,
marginBottom: 8,
},
headerSubtitle: {
fontSize: 16,
color: '#8E8E93',
textAlign: 'center',
},
section: {
backgroundColor: 'white',
marginBottom: 20,
paddingVertical: 8,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
color: '#6D6D72',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginHorizontal: 16,
marginTop: 8,
marginBottom: 8,
},
settingRow: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 0.5,
borderBottomColor: '#C6C6C8',
},
settingInfo: {
flex: 1,
marginRight: 16,
},
settingTitle: {
fontSize: 17,
color: '#000',
marginBottom: 2,
},
settingDescription: {
fontSize: 13,
color: '#8E8E93',
},
categoryRow: {
flexDirection: 'row',
alignItems: 'center',
paddingRight: 16,
borderBottomWidth: 0.5,
borderBottomColor: '#C6C6C8',
},
categoryInfo: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
},
categoryIcon: {
marginRight: 12,
},
categoryText: {
flex: 1,
marginRight: 12,
},
categoryTitle: {
fontSize: 17,
color: '#000',
marginBottom: 2,
},
categoryDescription: {
fontSize: 13,
color: '#8E8E93',
},
timePickerContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
},
timePicker: {
flex: 1,
alignItems: 'center',
paddingVertical: 12,
marginHorizontal: 8,
backgroundColor: '#F2F2F7',
borderRadius: 8,
},
timePickerLabel: {
fontSize: 13,
color: '#8E8E93',
marginBottom: 4,
},
timePickerValue: {
fontSize: 17,
color: '#000',
fontWeight: '600',
},
frequencyOption: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 0.5,
borderBottomColor: '#C6C6C8',
},
frequencyInfo: {
flex: 1,
marginRight: 16,
},
frequencyTitle: {
fontSize: 17,
color: '#000',
marginBottom: 2,
},
frequencyDescription: {
fontSize: 13,
color: '#8E8E93',
},
radioButton: {
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 2,
borderColor: '#C7C7CC',
alignItems: 'center',
justifyContent: 'center',
},
radioButtonSelected: {
borderColor: '#007AFF',
},
radioButtonInner: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: '#007AFF',
},
advancedOption: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 0.5,
borderBottomColor: '#C6C6C8',
},
advancedInfo: {
flex: 1,
},
advancedTitle: {
fontSize: 17,
color: '#000',
marginBottom: 2,
},
advancedDescription: {
fontSize: 13,
color: '#8E8E93',
},
footer: {
padding: 20,
marginBottom: 30,
},
footerText: {
fontSize: 13,
color: '#8E8E93',
textAlign: 'center',
lineHeight: 18,
},
});
export default NotificationSettings;
// services/notificationAnalytics.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
interface NotificationCampaign {
id: string;
name: string;
type: 'promotional' | 'engagement' | 'retention';
variants: Array<{
id: string;
title: string;
body: string;
weight: number;
sentCount: number;
openCount: number;
clickCount: number;
conversionCount: number;
}>;
targetAudience: {
segments: string[];
userCount: number;
};
schedule: {
startDate: Date;
endDate?: Date;
frequency: 'immediate' | 'daily' | 'weekly';
};
results: {
totalSent: number;
totalOpened: number;
totalClicked: number;
totalConverted: number;
openRate: number;
clickRate: number;
conversionRate: number;
};
}
class NotificationAnalytics {
private campaigns: Map<string, NotificationCampaign> = new Map();
async createCampaign(campaign: Omit<NotificationCampaign, 'results'>): Promise<string> {
const campaignWithResults: NotificationCampaign = {
...campaign,
results: {
totalSent: 0,
totalOpened: 0,
totalClicked: 0,
totalConverted: 0,
openRate: 0,
clickRate: 0,
conversionRate: 0,
},
};
this.campaigns.set(campaign.id, campaignWithResults);
await this.saveCampaigns();
return campaign.id;
}
async trackNotificationEvent(
campaignId: string,
variantId: string,
event: 'sent' | 'opened' | 'clicked' | 'converted',
userId?: string
): Promise<void> {
const campaign = this.campaigns.get(campaignId);
if (!campaign) return;
const variant = campaign.variants.find(v => v.id === variantId);
if (!variant) return;
// Update variant metrics
switch (event) {
case 'sent':
variant.sentCount++;
campaign.results.totalSent++;
break;
case 'opened':
variant.openCount++;
campaign.results.totalOpened++;
break;
case 'clicked':
variant.clickCount++;
campaign.results.totalClicked++;
break;
case 'converted':
variant.conversionCount++;
campaign.results.totalConverted++;
break;
}
// Recalculate rates
campaign.results.openRate = (campaign.results.totalOpened / campaign.results.totalSent) * 100;
campaign.results.clickRate = (campaign.results.totalClicked / campaign.results.totalOpened) * 100;
campaign.results.conversionRate = (campaign.results.totalConverted / campaign.results.totalClicked) * 100;
this.campaigns.set(campaignId, campaign);
await this.saveCampaigns();
}
async analyzePerformance(campaignId: string): Promise<{
overview: {
status: 'active' | 'completed' | 'paused';
performance: 'excellent' | 'good' | 'poor';
recommendation: string;
};
variants: Array<{
id: string;
title: string;
performance: {
openRate: number;
clickRate: number;
conversionRate: number;
};
confidence: number;
recommendation: string;
}>;
insights: string[];
}> {
const campaign = this.campaigns.get(campaignId);
if (!campaign) {
throw new Error('Campaign not found');
}
const variantAnalysis = campaign.variants.map(variant => {
const openRate = variant.sentCount > 0 ? (variant.openCount / variant.sentCount) * 100 : 0;
const clickRate = variant.openCount > 0 ? (variant.clickCount / variant.openCount) * 100 : 0;
const conversionRate = variant.clickCount > 0 ? (variant.conversionCount / variant.clickCount) * 100 : 0;
// Calculate statistical confidence (simplified)
const confidence = Math.min(variant.sentCount / 100, 1) * 100;
let recommendation = 'Continue monitoring';
if (openRate > 25) recommendation = 'High performance - scale up';
else if (openRate < 10) recommendation = 'Poor performance - optimize or pause';
return {
id: variant.id,
title: variant.title,
performance: {
openRate,
clickRate,
conversionRate,
},
confidence,
recommendation,
};
});
// Overall performance assessment
let performance: 'excellent' | 'good' | 'poor' = 'good';
if (campaign.results.openRate > 30) performance = 'excellent';
else if (campaign.results.openRate < 15) performance = 'poor';
const insights = [
`Campaign sent to ${campaign.results.totalSent} users`,
`Overall open rate: ${campaign.results.openRate.toFixed(1)}%`,
`Best performing variant: ${variantAnalysis.sort((a, b) => b.performance.openRate - a.performance.openRate)[0]?.title}`,
];
return {
overview: {
status: 'active', // Would determine based on schedule
performance,
recommendation: performance === 'excellent'
? 'Excellent performance! Consider expanding audience.'
: performance === 'poor'
? 'Performance below average. Review targeting and messaging.'
: 'Good performance. Monitor and optimize.'
},
variants: variantAnalysis,
insights,
};
}
async getTopPerformingNotifications(limit: number = 10): Promise<Array<{
campaignName: string;
variantTitle: string;
openRate: number;
conversionRate: number;
totalSent: number;
}>> {
const results: Array<{
campaignName: string;
variantTitle: string;
openRate: number;
conversionRate: number;
totalSent: number;
}> = [];
this.campaigns.forEach(campaign => {
campaign.variants.forEach(variant => {
if (variant.sentCount > 0) {
results.push({
campaignName: campaign.name,
variantTitle: variant.title,
openRate: (variant.openCount / variant.sentCount) * 100,
conversionRate: variant.clickCount > 0 ? (variant.conversionCount / variant.clickCount) * 100 : 0,
totalSent: variant.sentCount,
});
}
});
});
return results
.sort((a, b) => b.openRate - a.openRate)
.slice(0, limit);
}
private async saveCampaigns(): Promise<void> {
try {
const data = Object.fromEntries(this.campaigns);
await AsyncStorage.setItem('notification_campaigns', JSON.stringify(data));
} catch (error) {
console.error('Error saving campaigns:', error);
}
}
private async loadCampaigns(): Promise<void> {
try {
const data = await AsyncStorage.getItem('notification_campaigns');
if (data) {
const campaigns = JSON.parse(data);
this.campaigns = new Map(Object.entries(campaigns));
}
} catch (error) {
console.error('Error loading campaigns:', error);
}
}
}
export default NotificationAnalytics;
In this lesson, you learned:
Code with AI: Try building these advanced notification features.
Prompts to try:
Remember: Great notifications add value to users' lives rather than interrupting them - focus on being helpful, not just engaging!