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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user