feat(notifications): integrate DailyNotificationPlugin with UI for native platforms

Integrate DailyNotificationPlugin with notification UI to enable native
notifications on iOS/Android while maintaining web push for web/PWA.

- Add platform detection to PushNotificationPermission component
- Implement native notification flow via NotificationService
- Hide push server setting on native platforms (not needed)
- Add time conversion (AM/PM to 24-hour) for native plugin
- Add comprehensive documentation

Breaking Changes: None (backward compatible)
This commit is contained in:
Jose Olarte III
2026-01-23 19:06:16 +08:00
parent 84c3f79c57
commit 5a4ab84bfe
5 changed files with 1290 additions and 44 deletions

View File

@@ -95,6 +95,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Capacitor } from "@capacitor/core";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
import {
@@ -116,6 +117,7 @@ import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
import { NotificationService } from "@/services/notifications";
// Example interface for error
interface ErrorResponse {
@@ -167,6 +169,13 @@ export default class PushNotificationPermission extends Vue {
serviceWorkerReady = false;
vapidKey = "";
/**
* Check if running on native platform (iOS/Android)
*/
private get isNativePlatform(): boolean {
return Capacitor.isNativePlatform();
}
async open(
pushType: string,
callback?: (success: boolean, time: string, message?: string) => void,
@@ -174,6 +183,30 @@ export default class PushNotificationPermission extends Vue {
this.callback = callback || this.callback;
this.isVisible = true;
this.pushType = pushType;
// Native platforms: Skip web push initialization
if (this.isNativePlatform) {
logger.debug(
"[PushNotificationPermission] Native platform detected, skipping web push initialization",
);
// For native, we don't need VAPID or service worker
this.serviceWorkerReady = true;
this.vapidKey = "native"; // Placeholder for computed properties
// Set up message input based on push type
if (this.pushType === this.DIRECT_PUSH_TITLE) {
this.messageInput = this.notificationMessagePlaceholder;
// focus on the message input
setTimeout(function () {
document.getElementById("push-message")?.focus();
}, 100);
} else {
this.messageInput = "";
}
return;
}
// Web platform: Initialize web push (existing logic)
try {
const settings = await this.$accountSettings();
let pushUrl = DEFAULT_PUSH_SERVER;
@@ -585,16 +618,24 @@ export default class PushNotificationPermission extends Vue {
/**
* Computed property: isSystemReady
* Returns true if serviceWorkerReady and vapidKey are set
* For native platforms, always returns true (no VAPID needed)
*/
get isSystemReady(): boolean {
if (this.isNativePlatform) {
return true; // Native doesn't need VAPID/service worker
}
return this.serviceWorkerReady && !!this.vapidKey;
}
/**
* Computed property: canShowNotificationForm
* Returns true if serviceWorkerReady and vapidKey are set
* For native platforms, always returns true (no VAPID needed)
*/
get canShowNotificationForm(): boolean {
if (this.isNativePlatform) {
return true; // Native doesn't need VAPID/service worker
}
return this.serviceWorkerReady && !!this.vapidKey;
}
@@ -642,7 +683,12 @@ export default class PushNotificationPermission extends Vue {
*/
handleTurnOnNotifications() {
this.close();
this.turnOnNotifications();
// Route to native or web notification flow based on platform
if (this.isNativePlatform) {
this.turnOnNativeNotifications();
} else {
this.turnOnNotifications();
}
}
/**
@@ -652,6 +698,168 @@ export default class PushNotificationPermission extends Vue {
get waitingMessage(): string {
return "Waiting for system initialization, which may take up to 5 seconds...";
}
/**
* Handle native notification setup using DailyNotificationPlugin
*/
private async turnOnNativeNotifications(): Promise<void> {
try {
logger.debug(
"[PushNotificationPermission] Starting native notification setup",
);
const service = NotificationService.getInstance();
// Request permissions
logger.debug(
"[PushNotificationPermission] Requesting native permissions",
);
const granted = await service.requestPermissions();
if (!granted) {
logger.warn(
"[PushNotificationPermission] Native notification permissions denied",
);
this.$notify(
{
group: "alert",
type: "danger",
title: NOTIFY_PUSH_PERMISSION_ERROR.title,
text: NOTIFY_PUSH_PERMISSION_ERROR.message,
},
PUSH_NOTIFICATION_TIMEOUT_PERSISTENT,
);
this.callback(false, "", this.messageInput);
return;
}
// Convert time to 24-hour format (HH:mm)
const time24h = this.convertTo24HourFormat();
logger.debug(
"[PushNotificationPermission] Converted time to 24-hour format:",
time24h,
);
// Determine title and body based on pushType
const title =
this.pushType === this.DAILY_CHECK_TITLE
? "Daily Check-In"
: "Daily Reminder";
const body =
this.pushType === this.DIRECT_PUSH_TITLE
? this.messageInput || this.notificationMessagePlaceholder
: "Time to check your TimeSafari activity";
// Schedule notification
logger.info(
"[PushNotificationPermission] Scheduling native notification:",
{
time: time24h,
title,
pushType: this.pushType,
},
);
const success = await service.scheduleDailyNotification({
time: time24h,
title,
body,
priority: "normal",
});
if (!success) {
logger.error(
"[PushNotificationPermission] Failed to schedule native notification",
);
this.$notify(
{
group: "alert",
type: "danger",
title: NOTIFY_PUSH_SETUP_ERROR.title,
text: NOTIFY_PUSH_SETUP_ERROR.message,
},
PUSH_NOTIFICATION_TIMEOUT_SHORT,
);
this.callback(false, "", this.messageInput);
return;
}
// Save to settings
const timeText = this.notificationTimeText;
const settingsToSave: Record<string, string> = {};
if (this.pushType === this.DAILY_CHECK_TITLE) {
settingsToSave.notifyingNewActivityTime = timeText;
} else {
settingsToSave.notifyingReminderTime = timeText;
if (this.messageInput) {
settingsToSave.notifyingReminderMessage = this.messageInput;
}
}
await this.$saveSettings(settingsToSave);
logger.debug(
"[PushNotificationPermission] Settings saved:",
settingsToSave,
);
// Show success message
this.$notify(
{
group: "alert",
type: "success",
title: NOTIFY_PUSH_SUCCESS.title,
text: NOTIFY_PUSH_SUCCESS.message,
},
PUSH_NOTIFICATION_TIMEOUT_LONG,
);
// Call callback with success
this.callback(true, timeText, this.messageInput);
} catch (error) {
logger.error(
"[PushNotificationPermission] Error in native notification setup:",
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: NOTIFY_PUSH_SETUP_ERROR.title,
text: NOTIFY_PUSH_SETUP_ERROR.message,
},
PUSH_NOTIFICATION_TIMEOUT_SHORT,
);
this.callback(false, "", this.messageInput);
}
}
/**
* Convert AM/PM time input to 24-hour format (HH:mm)
* @returns Time string in HH:mm format
*/
private convertTo24HourFormat(): string {
const hour = parseInt(this.hourInput);
const minute = parseInt(this.minuteInput);
let hour24 = hour;
// Convert to 24-hour format
if (!this.hourAm && hour !== 12) {
// PM: add 12 (except for 12 PM which stays 12)
hour24 = hour + 12;
} else if (this.hourAm && hour === 12) {
// 12 AM: convert to 0
hour24 = 0;
}
// AM (except 12): keep as is
// Format with leading zeros
const hourStr = hour24.toString().padStart(2, "0");
const minuteStr = minute.toString().padStart(2, "0");
return `${hourStr}:${minuteStr}`;
}
}
</script>

View File

@@ -503,50 +503,53 @@
</label>
</div>
<h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server
</h2>
<div class="px-3 py-4">
<input
v-model="webPushServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
/>
<button
v-if="webPushServerInput != webPushServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSavePushServer()"
>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
>
Use Prod
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
>
Use Test 1
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
>
Use Test 2
</button>
<!-- Notification Push Server setting - only show on web platforms -->
<div v-if="!isNativePlatform">
<h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server
</h2>
<div class="px-3 py-4">
<input
v-model="webPushServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
/>
<button
v-if="webPushServerInput != webPushServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSavePushServer()"
>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
>
Use Prod
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
>
Use Test 1
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
>
Use Test 2
</button>
</div>
<span v-if="!webPushServerInput" class="px-4 text-sm">
When that setting is blank, this app will use the default web push
server URL:
{{ DEFAULT_PUSH_SERVER }}
</span>
</div>
<span v-if="!webPushServerInput" class="px-4 text-sm">
When that setting is blank, this app will use the default web push
server URL:
{{ DEFAULT_PUSH_SERVER }}
</span>
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
<div class="px-3 py-4">
@@ -887,6 +890,13 @@ export default class AccountViewView extends Vue {
private notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Check if running on native platform (iOS/Android)
*/
private get isNativePlatform(): boolean {
return Capacitor.isNativePlatform();
}
created() {
this.notify = createNotifyHelpers(this.$notify);