diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 83ff8bee..45869c63 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -44,15 +44,18 @@
+
+
+ android:exported="true">
+
= {};
@@ -784,13 +840,22 @@ export default class PushNotificationPermission extends Vue {
settingsToSave,
);
- // Show success message
+ // Check if exact alarm permission is needed (Android 12+)
+ const needsExactAlarmWarning =
+ Capacitor.getPlatform() === "android" &&
+ !finalPermissionCheck.details.exactAlarm;
+
+ // Show success message with optional warning
+ const successMessage = needsExactAlarmWarning
+ ? `${NOTIFY_PUSH_SUCCESS.message} Note: If notifications don't appear, enable "Exact alarms" in Android Settings → Apps → TimeSafari → App settings.`
+ : NOTIFY_PUSH_SUCCESS.message;
+
this.$notify(
{
group: "alert",
type: "success",
title: NOTIFY_PUSH_SUCCESS.title,
- text: NOTIFY_PUSH_SUCCESS.message,
+ text: successMessage,
},
PUSH_NOTIFICATION_TIMEOUT_LONG,
);
@@ -802,6 +867,14 @@ export default class PushNotificationPermission extends Vue {
"[PushNotificationPermission] Error in native notification setup:",
error,
);
+ // Log additional error details for debugging
+ if (error instanceof Error) {
+ logger.error("[PushNotificationPermission] Error details:", {
+ message: error.message,
+ stack: error.stack,
+ name: error.name,
+ });
+ }
this.$notify(
{
group: "alert",
diff --git a/src/services/notifications/NativeNotificationService.ts b/src/services/notifications/NativeNotificationService.ts
index 5e271d7a..9b14707e 100644
--- a/src/services/notifications/NativeNotificationService.ts
+++ b/src/services/notifications/NativeNotificationService.ts
@@ -50,6 +50,10 @@ export class NativeNotificationService implements NotificationServiceInterface {
return true;
}
+ /**
+ * Request notification permissions from the OS
+ * Shows native permission dialog on first call
+ */
/**
* Request notification permissions from the OS
* Shows native permission dialog on first call
@@ -58,31 +62,132 @@ export class NativeNotificationService implements NotificationServiceInterface {
try {
logger.debug("[NativeNotificationService] Requesting permissions");
+ // Check if plugin is available
+ if (!DailyNotification) {
+ logger.error(
+ "[NativeNotificationService] DailyNotification plugin is not available. " +
+ "Make sure the plugin is registered in MainActivity.",
+ );
+ return false;
+ }
+
+ // Check if the method exists
+ if (
+ typeof DailyNotification.requestNotificationPermissions !== "function"
+ ) {
+ logger.error(
+ "[NativeNotificationService] requestNotificationPermissions method not found on plugin. " +
+ "Available methods: " +
+ Object.keys(DailyNotification).join(", "),
+ );
+ return false;
+ }
+
+ logger.debug(
+ "[NativeNotificationService] Calling requestNotificationPermissions...",
+ );
+
// Use requestNotificationPermissions() which is the method exposed by iOS
// For Android, this should also work via the plugin bridge
const result = await DailyNotification.requestNotificationPermissions();
- logger.debug("[NativeNotificationService] Permission result:", result);
+ logger.debug(
+ "[NativeNotificationService] Permission result:",
+ JSON.stringify(result, null, 2),
+ );
+ logger.debug(
+ "[NativeNotificationService] Result type:",
+ typeof result,
+ "Keys:",
+ result ? Object.keys(result) : "null",
+ );
- // The result may be PermissionStatus (with granted boolean) or PermissionStatusResult
- // Handle both formats for compatibility
- if ("granted" in result && typeof result.granted === "boolean") {
+ // The result is PermissionStatus which has:
+ // - granted?: boolean
+ // - notifications: PermissionState ("granted" | "denied" | "prompt")
+ // Handle all possible formats for compatibility
+ if (
+ result &&
+ "granted" in result &&
+ typeof result.granted === "boolean"
+ ) {
+ logger.debug(
+ "[NativeNotificationService] Using 'granted' field:",
+ result.granted,
+ );
return result.granted;
}
+
+ // Check the notifications PermissionState field
+ if (result && "notifications" in result) {
+ const notificationsState = result.notifications;
+ logger.debug(
+ "[NativeNotificationService] Notifications state:",
+ notificationsState,
+ );
+ // PermissionState can be "granted", "denied", or "prompt"
+ if (notificationsState === "granted") {
+ return true;
+ }
+ if (notificationsState === "denied") {
+ return false;
+ }
+ // If "prompt", the user hasn't decided yet, so return false
+ if (notificationsState === "prompt") {
+ logger.warn(
+ "[NativeNotificationService] Permission still in prompt state after request",
+ );
+ return false;
+ }
+ }
+
+ // Check for PermissionStatusResult format (from checkPermissionStatus)
if (
+ result &&
"allPermissionsGranted" in result &&
typeof result.allPermissionsGranted === "boolean"
) {
+ logger.debug(
+ "[NativeNotificationService] Using 'allPermissionsGranted' field:",
+ result.allPermissionsGranted,
+ );
return result.allPermissionsGranted;
}
+
+ // If result is a boolean directly (some plugin versions might return this)
+ if (typeof result === "boolean") {
+ logger.debug("[NativeNotificationService] Result is boolean:", result);
+ return result;
+ }
+
// Fallback: check status after requesting
+ logger.debug(
+ "[NativeNotificationService] Falling back to checkPermissionStatus...",
+ );
const status = await DailyNotification.checkPermissionStatus();
+ logger.debug(
+ "[NativeNotificationService] Status check result:",
+ JSON.stringify(status, null, 2),
+ );
return status.allPermissionsGranted;
} catch (error) {
logger.error(
"[NativeNotificationService] Permission request failed:",
error,
);
+ // Log additional error details for debugging
+ if (error instanceof Error) {
+ logger.error("[NativeNotificationService] Error details:", {
+ message: error.message,
+ stack: error.stack,
+ name: error.name,
+ });
+ } else {
+ logger.error(
+ "[NativeNotificationService] Non-Error exception:",
+ JSON.stringify(error, null, 2),
+ );
+ }
return false;
}
}
@@ -94,8 +199,13 @@ export class NativeNotificationService implements NotificationServiceInterface {
try {
const status = await DailyNotification.checkPermissionStatus();
+ // Calculate granted status from individual permissions
+ const allGranted =
+ status.allPermissionsGranted ||
+ (status.notificationsEnabled && status.exactAlarmEnabled);
+
return {
- granted: status.allPermissionsGranted,
+ granted: allGranted,
details: {
notifications: status.notificationsEnabled,
exactAlarm: status.exactAlarmEnabled,
@@ -134,6 +244,41 @@ export class NativeNotificationService implements NotificationServiceInterface {
},
);
+ // Note: The notification channel should be created automatically by the plugin
+ // when the notification is delivered. The "Channel does not exist" warning
+ // during permission checks is expected and not a problem - the channel will
+ // be created when the receiver tries to show the notification.
+
+ // Check permissions before scheduling to ensure everything is set up correctly
+ logger.debug(
+ "[NativeNotificationService] Checking permissions before scheduling",
+ );
+ const permissionStatus = await this.checkPermissions();
+ logger.info(
+ "[NativeNotificationService] Permission status:",
+ JSON.stringify(permissionStatus, null, 2),
+ );
+
+ // Check if permissions are actually granted (all details must be true)
+ const allPermissionsGranted =
+ permissionStatus.details?.notifications &&
+ permissionStatus.details?.exactAlarm &&
+ permissionStatus.details?.backgroundRefresh;
+
+ if (!allPermissionsGranted) {
+ logger.warn(
+ "[NativeNotificationService] Permissions not fully granted. Details:",
+ permissionStatus.details,
+ );
+ // Continue anyway - the plugin might handle permission requests internally
+ // but log a warning for debugging
+ } else {
+ logger.debug(
+ "[NativeNotificationService] All permissions granted:",
+ permissionStatus.details,
+ );
+ }
+
// Cancel any existing notification with the same ID before scheduling a new one
// This ensures the old notification is removed from iOS notification center
try {
@@ -155,6 +300,48 @@ export class NativeNotificationService implements NotificationServiceInterface {
);
}
+ // Log current time and scheduled time for debugging
+ const now = new Date();
+ const [hours, minutes] = options.time.split(":").map(Number);
+ const scheduledTime = new Date();
+ scheduledTime.setHours(hours, minutes, 0, 0);
+
+ // If scheduled time is in the past, it should be scheduled for tomorrow
+ if (scheduledTime < now) {
+ scheduledTime.setDate(scheduledTime.getDate() + 1);
+ logger.info(
+ "[NativeNotificationService] Scheduled time is in the past, will schedule for tomorrow:",
+ {
+ requestedTime: options.time,
+ currentTime: now.toISOString(),
+ scheduledFor: scheduledTime.toISOString(),
+ },
+ );
+ } else {
+ logger.info("[NativeNotificationService] Scheduling notification:", {
+ requestedTime: options.time,
+ currentTime: now.toISOString(),
+ scheduledFor: scheduledTime.toISOString(),
+ minutesUntilNotification: Math.round(
+ (scheduledTime.getTime() - now.getTime()) / 1000 / 60,
+ ),
+ });
+ }
+
+ logger.debug(
+ "[NativeNotificationService] Calling scheduleDailyReminder with options:",
+ {
+ id: this.reminderId,
+ title: options.title,
+ body: options.body,
+ time: options.time,
+ repeatDaily: true,
+ sound: true,
+ vibration: true,
+ priority: options.priority || "normal",
+ },
+ );
+
await DailyNotification.scheduleDailyReminder({
id: this.reminderId,
title: options.title,
@@ -166,56 +353,91 @@ export class NativeNotificationService implements NotificationServiceInterface {
priority: options.priority || "normal",
});
- // Verify the notification was actually scheduled
- logger.debug(
- "[NativeNotificationService] Verifying notification was scheduled",
+ logger.info(
+ "[NativeNotificationService] scheduleDailyReminder call completed successfully",
+ {
+ reminderId: this.reminderId,
+ requestedTime: options.time,
+ },
);
- const remindersResult = await DailyNotification.getScheduledReminders();
- // Handle both array and object with reminders property
- const reminders = Array.isArray(remindersResult)
- ? remindersResult
- : (remindersResult as { reminders: typeof remindersResult })
- .reminders || [];
- const scheduledReminder = reminders.find((r) => r.id === this.reminderId);
- if (scheduledReminder && scheduledReminder.isScheduled) {
- // Verify the time matches what we scheduled
- if (scheduledReminder.time !== options.time) {
- logger.error(
- "[NativeNotificationService] Notification time mismatch!",
+ // Verify the notification was actually scheduled (if method is available)
+ // Note: getScheduledReminders() is not implemented on Android, so we
+ // only verify on iOS. On Android, we assume success if no error was thrown.
+ try {
+ logger.debug(
+ "[NativeNotificationService] Attempting to verify notification was scheduled",
+ );
+ const remindersResult = await DailyNotification.getScheduledReminders();
+ // Handle both array and object with reminders property
+ const reminders = Array.isArray(remindersResult)
+ ? remindersResult
+ : (remindersResult as { reminders: typeof remindersResult })
+ .reminders || [];
+ const scheduledReminder = reminders.find(
+ (r) => r.id === this.reminderId,
+ );
+
+ if (scheduledReminder && scheduledReminder.isScheduled) {
+ // Verify the time matches what we scheduled
+ if (scheduledReminder.time !== options.time) {
+ logger.error(
+ "[NativeNotificationService] Notification time mismatch!",
+ {
+ scheduled: scheduledReminder.time,
+ requested: options.time,
+ reminderId: this.reminderId,
+ },
+ );
+ return false;
+ }
+
+ logger.info(
+ "[NativeNotificationService] Daily notification verified as scheduled:",
{
- scheduled: scheduledReminder.time,
- requested: options.time,
- reminderId: this.reminderId,
+ id: scheduledReminder.id,
+ time: scheduledReminder.time,
+ requestedTime: options.time,
+ nextTriggerTime: scheduledReminder.nextTriggerTime,
},
);
+ return true;
+ } else {
+ logger.warn(
+ "[NativeNotificationService] Notification was not found in scheduled reminders after scheduling",
+ {
+ reminderId: this.reminderId,
+ requestedTime: options.time,
+ allReminders: reminders.map((r) => ({
+ id: r.id,
+ time: r.time,
+ isScheduled: r.isScheduled,
+ })),
+ },
+ );
+ // On iOS, if verification fails, return false
+ // On Android, this method isn't available, so we'll fall through to return true
return false;
}
-
- logger.info(
- "[NativeNotificationService] Daily notification scheduled successfully:",
- {
- id: scheduledReminder.id,
- time: scheduledReminder.time,
- requestedTime: options.time,
- nextTriggerTime: scheduledReminder.nextTriggerTime,
- },
+ } catch (verifyError) {
+ // If getScheduledReminders() is not implemented (Android), assume success
+ // since scheduleDailyReminder() completed without error
+ if (
+ verifyError instanceof Error &&
+ verifyError.message.includes("not implemented")
+ ) {
+ logger.debug(
+ "[NativeNotificationService] getScheduledReminders() not available on this platform (expected on Android). " +
+ "Assuming success since scheduleDailyReminder() completed without error.",
+ );
+ return true;
+ }
+ // For other errors, log but still assume success since the schedule call succeeded
+ logger.warn(
+ "[NativeNotificationService] Verification failed, but schedule call succeeded:",
+ verifyError,
);
return true;
- } else {
- logger.warn(
- "[NativeNotificationService] Notification was not found in scheduled reminders after scheduling",
- {
- reminderId: this.reminderId,
- requestedTime: options.time,
- allReminders: reminders.map((r) => ({
- id: r.id,
- time: r.time,
- isScheduled: r.isScheduled,
- })),
- },
- );
- return false;
}
} catch (error) {
logger.error("[NativeNotificationService] Schedule failed:", error);