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
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;
|
|
|