You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

433 lines
14 KiB

/**
* Notification Permission Manager
*
* Handles Android 13+ notification permissions with graceful fallbacks
* Provides user-friendly permission request flows and education
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { Capacitor } from '@capacitor/core';
/**
* Permission status interface
*/
export interface PermissionStatus {
notifications: 'granted' | 'denied' | 'prompt' | 'not_supported';
exactAlarms: 'granted' | 'denied' | 'not_supported';
batteryOptimization: 'granted' | 'denied' | 'not_supported';
overall: 'ready' | 'partial' | 'blocked';
}
/**
* Permission request result
*/
export interface PermissionRequestResult {
success: boolean;
permissions: PermissionStatus;
message: string;
nextSteps?: string[];
}
/**
* Permission education content
*/
export interface PermissionEducation {
title: string;
message: string;
benefits: string[];
steps: string[];
fallbackOptions: string[];
}
/**
* Notification Permission Manager
*/
export class NotificationPermissionManager {
private static instance: NotificationPermissionManager;
private constructor() {}
public static getInstance(): NotificationPermissionManager {
if (!NotificationPermissionManager.instance) {
NotificationPermissionManager.instance = new NotificationPermissionManager();
}
return NotificationPermissionManager.instance;
}
/**
* Check current permission status
*/
async checkPermissions(): Promise<PermissionStatus> {
try {
const platform = Capacitor.getPlatform();
if (platform === 'web') {
return {
notifications: 'not_supported',
exactAlarms: 'not_supported',
batteryOptimization: 'not_supported',
overall: 'blocked'
};
}
// Check notification permissions
const notificationStatus = await this.checkNotificationPermissions();
// Check exact alarm permissions
const exactAlarmStatus = await this.checkExactAlarmPermissions();
// Check battery optimization status
const batteryStatus = await this.checkBatteryOptimizationStatus();
// Determine overall status
const overall = this.determineOverallStatus(notificationStatus, exactAlarmStatus, batteryStatus);
return {
notifications: notificationStatus,
exactAlarms: exactAlarmStatus,
batteryOptimization: batteryStatus,
overall
};
} catch (error) {
console.error('Error checking permissions:', error);
return {
notifications: 'denied',
exactAlarms: 'denied',
batteryOptimization: 'denied',
overall: 'blocked'
};
}
}
/**
* Request all required permissions with education
*/
async requestPermissionsWithEducation(): Promise<PermissionRequestResult> {
try {
const currentStatus = await this.checkPermissions();
if (currentStatus.overall === 'ready') {
return {
success: true,
permissions: currentStatus,
message: 'All permissions already granted'
};
}
const results: string[] = [];
const nextSteps: string[] = [];
// Request notification permissions
if (currentStatus.notifications === 'prompt') {
const notificationResult = await this.requestNotificationPermissions();
results.push(notificationResult.message);
if (!notificationResult.success) {
nextSteps.push('Enable notifications in device settings');
}
}
// Request exact alarm permissions
if (currentStatus.exactAlarms === 'denied') {
const exactAlarmResult = await this.requestExactAlarmPermissions();
results.push(exactAlarmResult.message);
if (!exactAlarmResult.success) {
nextSteps.push('Enable exact alarms in device settings');
}
}
// Request battery optimization exemption
if (currentStatus.batteryOptimization === 'denied') {
const batteryResult = await this.requestBatteryOptimizationExemption();
results.push(batteryResult.message);
if (!batteryResult.success) {
nextSteps.push('Disable battery optimization for this app');
}
}
const finalStatus = await this.checkPermissions();
const success = finalStatus.overall === 'ready' || finalStatus.overall === 'partial';
return {
success,
permissions: finalStatus,
message: results.join('; '),
nextSteps: nextSteps.length > 0 ? nextSteps : undefined
};
} catch (error) {
console.error('Error requesting permissions:', error);
return {
success: false,
permissions: await this.checkPermissions(),
message: 'Failed to request permissions: ' + (error instanceof Error ? error.message : String(error))
};
}
}
/**
* Get permission education content
*/
getPermissionEducation(): PermissionEducation {
return {
title: 'Enable Notifications for Better Experience',
message: 'To receive timely updates and reminders, please enable notifications and related permissions.',
benefits: [
'Receive daily updates at your preferred time',
'Get notified about important changes',
'Never miss important reminders',
'Enjoy reliable notification delivery'
],
steps: [
'Tap "Allow" when prompted for notification permissions',
'Enable exact alarms for precise timing',
'Disable battery optimization for this app',
'Test notifications to ensure everything works'
],
fallbackOptions: [
'Use in-app reminders as backup',
'Check the app regularly for updates',
'Enable email notifications if available'
]
};
}
/**
* Show permission education dialog
*/
async showPermissionEducation(): Promise<boolean> {
try {
const education = this.getPermissionEducation();
// Create and show education dialog
const userChoice = await this.showEducationDialog(education);
if (userChoice === 'continue') {
return await this.requestPermissionsWithEducation().then(result => result.success);
}
return false;
} catch (error) {
console.error('Error showing permission education:', error);
return false;
}
}
/**
* Handle permission denied gracefully
*/
async handlePermissionDenied(permissionType: 'notifications' | 'exactAlarms' | 'batteryOptimization'): Promise<void> {
try {
const education = this.getPermissionEducation();
switch (permissionType) {
case 'notifications':
await this.showNotificationDeniedDialog(education);
break;
case 'exactAlarms':
await this.showExactAlarmDeniedDialog(education);
break;
case 'batteryOptimization':
await this.showBatteryOptimizationDeniedDialog(education);
break;
}
} catch (error) {
console.error('Error handling permission denied:', error);
}
}
/**
* Check if app can function with current permissions
*/
async canFunctionWithCurrentPermissions(): Promise<boolean> {
try {
const status = await this.checkPermissions();
// App can function if notifications are granted, even without exact alarms
return status.notifications === 'granted';
} catch (error) {
console.error('Error checking if app can function:', error);
return false;
}
}
/**
* Get fallback notification strategy
*/
getFallbackStrategy(): string[] {
return [
'Use in-app notifications instead of system notifications',
'Implement periodic background checks',
'Show notification badges in the app',
'Use email notifications as backup',
'Implement push notifications through a service'
];
}
// Private helper methods
private async checkNotificationPermissions(): Promise<'granted' | 'denied' | 'prompt' | 'not_supported'> {
try {
if (Capacitor.getPlatform() === 'web') {
return 'not_supported';
}
// Check if we can access the plugin
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return 'not_supported';
}
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.checkPermissions();
return status?.notifications || 'denied';
} catch (error) {
console.error('Error checking notification permissions:', error);
return 'denied';
}
}
private async checkExactAlarmPermissions(): Promise<'granted' | 'denied' | 'not_supported'> {
try {
if (Capacitor.getPlatform() === 'web') {
return 'not_supported';
}
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return 'denied';
}
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
return status?.canSchedule ? 'granted' : 'denied';
} catch (error) {
console.error('Error checking exact alarm permissions:', error);
return 'denied';
}
}
private async checkBatteryOptimizationStatus(): Promise<'granted' | 'denied' | 'not_supported'> {
try {
if (Capacitor.getPlatform() === 'web') {
return 'not_supported';
}
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return 'denied';
}
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
return status?.isOptimized ? 'denied' : 'granted';
} catch (error) {
console.error('Error checking battery optimization status:', error);
return 'denied';
}
}
private determineOverallStatus(
notifications: string,
exactAlarms: string,
batteryOptimization: string
): 'ready' | 'partial' | 'blocked' {
if (notifications === 'granted' && exactAlarms === 'granted' && batteryOptimization === 'granted') {
return 'ready';
} else if (notifications === 'granted') {
return 'partial';
} else {
return 'blocked';
}
}
private async requestNotificationPermissions(): Promise<{ success: boolean; message: string }> {
try {
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return { success: false, message: 'Plugin not available' };
}
const result = await (window as any).Capacitor?.Plugins?.DailyNotification?.requestPermissions();
return {
success: result?.notifications === 'granted',
message: result?.notifications === 'granted' ? 'Notification permissions granted' : 'Notification permissions denied'
};
} catch (error) {
return { success: false, message: 'Failed to request notification permissions' };
}
}
private async requestExactAlarmPermissions(): Promise<{ success: boolean; message: string }> {
try {
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return { success: false, message: 'Plugin not available' };
}
await (window as any).Capacitor?.Plugins?.DailyNotification?.requestExactAlarmPermission();
// Check if permission was granted
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
return {
success: status?.canSchedule,
message: status?.canSchedule ? 'Exact alarm permissions granted' : 'Exact alarm permissions denied'
};
} catch (error) {
return { success: false, message: 'Failed to request exact alarm permissions' };
}
}
private async requestBatteryOptimizationExemption(): Promise<{ success: boolean; message: string }> {
try {
if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return { success: false, message: 'Plugin not available' };
}
await (window as any).Capacitor?.Plugins?.DailyNotification?.requestBatteryOptimizationExemption();
// Check if exemption was granted
const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
return {
success: !status?.isOptimized,
message: !status?.isOptimized ? 'Battery optimization exemption granted' : 'Battery optimization exemption denied'
};
} catch (error) {
return { success: false, message: 'Failed to request battery optimization exemption' };
}
}
private async showEducationDialog(education: PermissionEducation): Promise<'continue' | 'cancel'> {
// This would show a custom dialog with the education content
// For now, we'll use a simple confirm dialog
const message = `${education.title}\n\n${education.message}\n\nBenefits:\n${education.benefits.map(b => `${b}`).join('\n')}`;
return new Promise((resolve) => {
if (confirm(message)) {
resolve('continue');
} else {
resolve('cancel');
}
});
}
private async showNotificationDeniedDialog(education: PermissionEducation): Promise<void> {
const message = `Notifications are disabled. You can still use the app, but you won't receive timely updates.\n\nTo enable notifications:\n${education.steps.slice(0, 2).map(s => `${s}`).join('\n')}`;
alert(message);
}
private async showExactAlarmDeniedDialog(_education: PermissionEducation): Promise<void> {
const message = `Exact alarms are disabled. Notifications may not arrive at the exact time you specified.\n\nTo enable exact alarms:\n• Go to device settings\n• Find this app\n• Enable "Alarms & reminders"`;
alert(message);
}
private async showBatteryOptimizationDeniedDialog(_education: PermissionEducation): Promise<void> {
const message = `Battery optimization is enabled. This may prevent notifications from arriving on time.\n\nTo disable battery optimization:\n• Go to device settings\n• Find "Battery optimization"\n• Select this app\n• Choose "Don't optimize"`;
alert(message);
}
}
export default NotificationPermissionManager;