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.
772 lines
25 KiB
772 lines
25 KiB
<template>
|
|
<section
|
|
v-if="notificationsSupported"
|
|
id="sectionDailyNotifications"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
|
aria-labelledby="dailyNotificationsHeading"
|
|
>
|
|
<h2 id="dailyNotificationsHeading" class="mb-2 font-bold">
|
|
Daily Notifications
|
|
<button
|
|
class="text-slate-400 fa-fw cursor-pointer"
|
|
aria-label="Learn more about native notifications"
|
|
@click.stop="showNativeNotificationInfo"
|
|
>
|
|
<font-awesome icon="circle-question" aria-hidden="true" />
|
|
</button>
|
|
</h2>
|
|
|
|
<div class="flex items-center justify-between">
|
|
<div>Daily Notification</div>
|
|
<!-- Toggle switch -->
|
|
<div
|
|
class="relative ml-2 cursor-pointer"
|
|
role="switch"
|
|
:aria-checked="nativeNotificationEnabled"
|
|
:aria-label="
|
|
nativeNotificationEnabled
|
|
? 'Disable daily notifications'
|
|
: 'Enable daily notifications'
|
|
"
|
|
tabindex="0"
|
|
@click="toggleNativeNotification"
|
|
>
|
|
<!-- input -->
|
|
<input
|
|
:checked="nativeNotificationEnabled"
|
|
type="checkbox"
|
|
class="sr-only"
|
|
tabindex="-1"
|
|
readonly
|
|
/>
|
|
<!-- line -->
|
|
<div
|
|
class="block bg-slate-500 w-14 h-8 rounded-full transition"
|
|
:class="{
|
|
'bg-blue-600': nativeNotificationEnabled,
|
|
}"
|
|
></div>
|
|
<!-- dot -->
|
|
<div
|
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
:class="{
|
|
'left-7 bg-white': nativeNotificationEnabled,
|
|
}"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Show "Open Settings" button when permissions are denied -->
|
|
<div
|
|
v-if="
|
|
notificationsSupported &&
|
|
notificationStatus &&
|
|
notificationStatus.permissions.notifications === 'denied'
|
|
"
|
|
class="mt-2"
|
|
>
|
|
<button
|
|
class="w-full px-3 py-2 bg-blue-600 text-white rounded text-sm font-medium"
|
|
@click="openNotificationSettings"
|
|
>
|
|
Open Settings
|
|
</button>
|
|
<p class="text-xs text-slate-500 mt-1 text-center">
|
|
Enable notifications in Settings > App info > Notifications
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Time input section - show when enabled OR when no time is set -->
|
|
<div
|
|
v-if="nativeNotificationEnabled || !nativeNotificationTimeStorage"
|
|
class="mt-2"
|
|
>
|
|
<div
|
|
v-if="nativeNotificationEnabled"
|
|
class="flex items-center justify-between mb-2"
|
|
>
|
|
<span
|
|
>Scheduled for:
|
|
<span v-if="nativeNotificationTime">{{
|
|
nativeNotificationTime
|
|
}}</span>
|
|
<span v-else class="text-slate-500">Not set</span></span
|
|
>
|
|
<button
|
|
class="text-blue-500 text-sm"
|
|
@click="editNativeNotificationTime"
|
|
>
|
|
{{ showTimeEdit ? "Cancel" : "Edit Time" }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Time input (shown when editing or when no time is set) -->
|
|
<div v-if="showTimeEdit || !nativeNotificationTimeStorage" class="mt-2">
|
|
<label class="block text-sm text-slate-600 mb-1">
|
|
Notification Time
|
|
</label>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
v-model="nativeNotificationTimeStorage"
|
|
type="time"
|
|
class="rounded border border-slate-400 px-2 py-2"
|
|
@change="onTimeChange"
|
|
/>
|
|
<button
|
|
v-if="showTimeEdit || nativeNotificationTimeStorage"
|
|
class="px-3 py-2 bg-blue-600 text-white rounded"
|
|
@click="saveTimeChange"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
<p
|
|
v-if="!nativeNotificationTimeStorage"
|
|
class="text-xs text-slate-500 mt-1"
|
|
>
|
|
Set a time before enabling notifications
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<div v-if="loading" class="mt-2 text-sm text-slate-500">Loading...</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
/**
|
|
* DailyNotificationSection Component
|
|
*
|
|
* A self-contained component for managing daily notification scheduling
|
|
* in AccountViewView. This component handles platform detection, permission
|
|
* requests, scheduling, and state management for daily notifications.
|
|
*
|
|
* Features:
|
|
* - Platform capability detection (hides on unsupported platforms)
|
|
* - Permission request flow
|
|
* - Schedule/cancel notifications
|
|
* - Time editing with HTML5 time input
|
|
* - Settings persistence
|
|
* - Plugin state synchronization
|
|
*
|
|
* @author Generated for TimeSafari Daily Notification Integration
|
|
* @component
|
|
*/
|
|
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
import { logger } from "@/utils/logger";
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
import type {
|
|
NotificationStatus,
|
|
PermissionStatus,
|
|
} from "@/services/PlatformService";
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|
import type { NotificationIface } from "@/constants/app";
|
|
|
|
/**
|
|
* Convert 24-hour time format ("09:00") to 12-hour display format ("9:00 AM")
|
|
*/
|
|
function formatTimeForDisplay(time24: string): string {
|
|
if (!time24) return "";
|
|
const [hours, minutes] = time24.split(":");
|
|
const hourNum = parseInt(hours);
|
|
const isPM = hourNum >= 12;
|
|
const displayHour =
|
|
hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
|
|
return `${displayHour}:${minutes} ${isPM ? "PM" : "AM"}`;
|
|
}
|
|
|
|
@Component({
|
|
name: "DailyNotificationSection",
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
export default class DailyNotificationSection extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
// Component state
|
|
notificationsSupported: boolean = false;
|
|
nativeNotificationEnabled: boolean = false;
|
|
nativeNotificationTime: string = ""; // Display format: "9:00 AM"
|
|
nativeNotificationTimeStorage: string = ""; // Plugin format: "09:00"
|
|
nativeNotificationTitle: string = "Daily Update";
|
|
nativeNotificationMessage: string = "Your daily notification is ready!";
|
|
showTimeEdit: boolean = false;
|
|
loading: boolean = false;
|
|
notificationStatus: NotificationStatus | null = null;
|
|
|
|
// Notify helpers
|
|
private notify!: ReturnType<typeof createNotifyHelpers>;
|
|
|
|
async created(): Promise<void> {
|
|
this.notify = createNotifyHelpers(this.$notify);
|
|
}
|
|
|
|
/**
|
|
* Initialize component state on mount
|
|
* Checks platform support and syncs with plugin state
|
|
*
|
|
* **Token Refresh on Mount:**
|
|
* - Refreshes native fetcher configuration to ensure plugin has valid token
|
|
* - This handles cases where app was closed for extended periods
|
|
* - Token refresh happens automatically without user interaction
|
|
*
|
|
* **App Resume Listener:**
|
|
* - Listens for Capacitor 'resume' event to refresh token when app comes to foreground
|
|
* - Ensures plugin always has fresh token for background prefetch operations
|
|
* - Cleaned up in `beforeDestroy()` lifecycle hook
|
|
*/
|
|
async mounted(): Promise<void> {
|
|
await this.initializeState();
|
|
// Refresh native fetcher configuration on mount
|
|
// This ensures plugin has valid token even if app was closed for extended periods
|
|
await this.refreshNativeFetcherConfig();
|
|
// Listen for app resume events to refresh token when app comes to foreground
|
|
// This is part of the proactive token refresh strategy
|
|
document.addEventListener("resume", this.handleAppResume);
|
|
}
|
|
|
|
/**
|
|
* Cleanup on component destroy
|
|
*/
|
|
beforeDestroy(): void {
|
|
document.removeEventListener("resume", this.handleAppResume);
|
|
}
|
|
|
|
/**
|
|
* Handle app resume event - refresh native fetcher configuration
|
|
*
|
|
* This method is called when the app comes to foreground (via Capacitor 'resume' event).
|
|
* It proactively refreshes the JWT token to ensure the plugin has valid authentication
|
|
* for background prefetch operations.
|
|
*
|
|
* **Why refresh on resume?**
|
|
* - Tokens expire after 72 hours
|
|
* - App may have been closed for extended periods
|
|
* - Refreshing ensures plugin has valid token for next prefetch cycle
|
|
* - No user interaction required - happens automatically
|
|
*
|
|
* @see {@link refreshNativeFetcherConfig} For implementation details
|
|
*/
|
|
async handleAppResume(): Promise<void> {
|
|
logger.debug(
|
|
"[DailyNotificationSection] App resumed, refreshing native fetcher config",
|
|
);
|
|
await this.refreshNativeFetcherConfig();
|
|
}
|
|
|
|
/**
|
|
* Refresh native fetcher configuration with fresh JWT token
|
|
*
|
|
* This method ensures the daily notification plugin has a valid authentication token
|
|
* for background prefetch operations. It's called proactively to prevent token expiration
|
|
* issues during offline periods.
|
|
*
|
|
* **Refresh Triggers:**
|
|
* - Component mount (when notification settings page loads)
|
|
* - App resume (when app comes to foreground)
|
|
* - Notification enabled (when user enables daily notifications)
|
|
*
|
|
* **Token Refresh Strategy (Hybrid Approach):**
|
|
* - Tokens are valid for 72 hours (see `accessTokenForBackground`)
|
|
* - Tokens are refreshed proactively when app is already open
|
|
* - If token expires while offline, plugin uses cached content
|
|
* - Next time app opens, token is automatically refreshed
|
|
*
|
|
* **Why This Approach?**
|
|
* - No app wake-up required (tokens refresh when app is already open)
|
|
* - Works offline (72-hour validity supports extended offline periods)
|
|
* - Automatic (no user interaction required)
|
|
* - Includes starred plans (fetcher receives user's starred plans for prefetch)
|
|
* - Graceful degradation (if refresh fails, cached content still works)
|
|
*
|
|
* **Error Handling:**
|
|
* - Errors are logged but not shown to user (background operation)
|
|
* - Returns early if notifications not supported or disabled
|
|
* - Returns early if API server not configured
|
|
* - Failures don't interrupt user experience
|
|
*
|
|
* @returns Promise that resolves when refresh completes (or fails silently)
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Called automatically on mount/resume
|
|
* await this.refreshNativeFetcherConfig();
|
|
* ```
|
|
*
|
|
* @see {@link CapacitorPlatformService.configureNativeFetcher} For token generation
|
|
* @see {@link accessTokenForBackground} For 72-hour token generation
|
|
*/
|
|
async refreshNativeFetcherConfig(): Promise<void> {
|
|
try {
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
|
|
// Early return: Only refresh if notifications are supported and enabled
|
|
// This prevents unnecessary work when notifications aren't being used
|
|
if (!this.notificationsSupported || !this.nativeNotificationEnabled) {
|
|
return;
|
|
}
|
|
|
|
// Get settings for API server and starred plans
|
|
// API server tells plugin where to fetch content from
|
|
// Starred plans tell plugin which plans to prefetch
|
|
const settings = await this.$accountSettings();
|
|
const apiServer = settings.apiServer || "";
|
|
|
|
if (!apiServer) {
|
|
logger.warn(
|
|
"[DailyNotificationSection] No API server configured, skipping native fetcher refresh",
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get starred plans from settings
|
|
// These are passed to the plugin so it knows which plans to prefetch
|
|
const starredPlanHandleIds = settings.starredPlanHandleIds || [];
|
|
|
|
// Configure native fetcher with fresh token
|
|
// The jwt parameter is ignored - configureNativeFetcher generates it automatically
|
|
// This ensures we always have a fresh token with current expiration time
|
|
await platformService.configureNativeFetcher({
|
|
apiServer,
|
|
jwt: "", // Will be generated automatically by configureNativeFetcher
|
|
starredPlanHandleIds,
|
|
});
|
|
|
|
logger.info(
|
|
"[DailyNotificationSection] Native fetcher configuration refreshed",
|
|
);
|
|
} catch (error) {
|
|
// Don't show error to user - this is a background operation
|
|
// Failures are logged for debugging but don't interrupt user experience
|
|
// If refresh fails, plugin will use existing token (if still valid) or cached content
|
|
logger.error(
|
|
"[DailyNotificationSection] Failed to refresh native fetcher config:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize component state
|
|
* Checks platform support and syncs with plugin state
|
|
*/
|
|
async initializeState(): Promise<void> {
|
|
try {
|
|
this.loading = true;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
|
|
// Check if notifications are supported on this platform
|
|
// This also verifies plugin availability (returns null if plugin unavailable)
|
|
const status = await platformService.getDailyNotificationStatus();
|
|
if (status === null) {
|
|
// Notifications not supported or plugin unavailable - don't initialize
|
|
this.notificationsSupported = false;
|
|
logger.warn(
|
|
"[DailyNotificationSection] Notifications not supported or plugin unavailable",
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.notificationsSupported = true;
|
|
this.notificationStatus = status;
|
|
|
|
// Plugin state is the source of truth
|
|
if (status.isScheduled && status.scheduledTime) {
|
|
// Plugin has a scheduled notification - sync UI to match
|
|
this.nativeNotificationEnabled = true;
|
|
this.nativeNotificationTimeStorage = status.scheduledTime;
|
|
this.nativeNotificationTime = formatTimeForDisplay(
|
|
status.scheduledTime,
|
|
);
|
|
} else {
|
|
// No plugin schedule - UI defaults to disabled
|
|
this.nativeNotificationEnabled = false;
|
|
this.nativeNotificationTimeStorage = "";
|
|
this.nativeNotificationTime = "";
|
|
}
|
|
} catch (error) {
|
|
logger.error("[DailyNotificationSection] Failed to initialize:", error);
|
|
this.notificationsSupported = false;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle notification on/off
|
|
*/
|
|
async toggleNativeNotification(): Promise<void> {
|
|
// Prevent multiple simultaneous toggles
|
|
if (this.loading) {
|
|
logger.warn(
|
|
"[DailyNotificationSection] Toggle ignored - operation in progress",
|
|
);
|
|
return;
|
|
}
|
|
|
|
logger.info(
|
|
`[DailyNotificationSection] Toggling notification: ${this.nativeNotificationEnabled} -> ${!this.nativeNotificationEnabled}`,
|
|
);
|
|
|
|
if (this.nativeNotificationEnabled) {
|
|
await this.disableNativeNotification();
|
|
} else {
|
|
await this.enableNativeNotification();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable daily notification
|
|
*/
|
|
async enableNativeNotification(): Promise<void> {
|
|
try {
|
|
this.loading = true;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
|
|
// Check if we have a time set
|
|
if (!this.nativeNotificationTimeStorage) {
|
|
this.notify.error(
|
|
"Please set a notification time first",
|
|
TIMEOUTS.SHORT,
|
|
);
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
|
|
// Check permissions first - this also verifies plugin availability
|
|
let permissions: PermissionStatus | null;
|
|
try {
|
|
permissions = await platformService.checkNotificationPermissions();
|
|
logger.info(
|
|
`[DailyNotificationSection] Permission check result:`,
|
|
permissions,
|
|
);
|
|
} catch (error) {
|
|
// Plugin may not be available or there's an error
|
|
logger.error(
|
|
"[DailyNotificationSection] Failed to check permissions (plugin may be unavailable):",
|
|
error,
|
|
);
|
|
this.notify.error(
|
|
"Unable to check notification permissions. The notification plugin may not be installed.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
return;
|
|
}
|
|
|
|
if (permissions === null) {
|
|
// Platform doesn't support notifications or plugin unavailable
|
|
logger.warn(
|
|
"[DailyNotificationSection] Notifications not supported or plugin unavailable",
|
|
);
|
|
this.notify.error(
|
|
"Notifications are not supported on this platform or the plugin is not installed.",
|
|
TIMEOUTS.SHORT,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
return;
|
|
}
|
|
|
|
logger.info(
|
|
`[DailyNotificationSection] Permission state: ${permissions.notifications}`,
|
|
);
|
|
|
|
// If permissions are explicitly denied, don't try to request again
|
|
// (this prevents the plugin crash when handling denied permissions)
|
|
// Android won't show the dialog again if permissions are permanently denied
|
|
if (permissions.notifications === "denied") {
|
|
logger.warn(
|
|
"[DailyNotificationSection] Permissions already denied, directing user to settings",
|
|
);
|
|
this.notify.error(
|
|
"Notification permissions were denied. Tap 'Open Settings' to enable them.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
return;
|
|
}
|
|
|
|
// Only request if permissions are in "prompt" state (not denied, not granted)
|
|
// This ensures we only call requestPermissions when Android will actually show a dialog
|
|
if (permissions.notifications === "prompt") {
|
|
logger.info(
|
|
"[DailyNotificationSection] Permission state is 'prompt', requesting permissions...",
|
|
);
|
|
try {
|
|
const result = await platformService.requestNotificationPermissions();
|
|
logger.info(
|
|
`[DailyNotificationSection] Permission request result:`,
|
|
result,
|
|
);
|
|
if (result === null) {
|
|
// Plugin unavailable or request failed
|
|
logger.error(
|
|
"[DailyNotificationSection] Permission request returned null",
|
|
);
|
|
this.notify.error(
|
|
"Unable to request notification permissions. The plugin may not be available.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
return;
|
|
}
|
|
if (!result.notifications) {
|
|
// Permission request was denied
|
|
logger.warn(
|
|
"[DailyNotificationSection] Permission request denied by user",
|
|
);
|
|
this.notify.error(
|
|
"Notification permissions are required. Tap 'Open Settings' to enable them.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
return;
|
|
}
|
|
// Permissions granted - continue
|
|
logger.info(
|
|
"[DailyNotificationSection] Permissions granted successfully",
|
|
);
|
|
} catch (error) {
|
|
// Handle permission request errors (including plugin crashes)
|
|
logger.error(
|
|
"[DailyNotificationSection] Permission request failed:",
|
|
error,
|
|
);
|
|
this.notify.error(
|
|
"Unable to request notification permissions. Tap 'Open Settings' to enable them.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
return;
|
|
}
|
|
} else if (permissions.notifications !== "granted") {
|
|
// Unexpected state - shouldn't happen, but handle gracefully
|
|
logger.warn(
|
|
`[DailyNotificationSection] Unexpected permission state: ${permissions.notifications}`,
|
|
);
|
|
this.notify.error(
|
|
"Unable to determine notification permission status. Tap 'Open Settings' to check.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
return;
|
|
} else {
|
|
logger.info("[DailyNotificationSection] Permissions already granted");
|
|
}
|
|
|
|
// Permissions are granted - continue with scheduling
|
|
|
|
// Schedule notification via PlatformService
|
|
await platformService.scheduleDailyNotification({
|
|
time: this.nativeNotificationTimeStorage, // "09:00" in local time
|
|
title: this.nativeNotificationTitle,
|
|
body: this.nativeNotificationMessage,
|
|
sound: true,
|
|
priority: "high",
|
|
});
|
|
|
|
// Update UI state
|
|
this.nativeNotificationEnabled = true;
|
|
|
|
// Refresh native fetcher configuration with fresh token
|
|
// This ensures plugin has valid authentication when notifications are first enabled
|
|
// Token will be valid for 72 hours, supporting offline prefetch operations
|
|
await this.refreshNativeFetcherConfig();
|
|
|
|
this.notify.success(
|
|
"Daily notification scheduled successfully",
|
|
TIMEOUTS.SHORT,
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
"[DailyNotificationSection] Failed to enable notification:",
|
|
error,
|
|
);
|
|
this.notify.error(
|
|
"Failed to schedule notification. Please try again.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
this.nativeNotificationEnabled = false;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable daily notification
|
|
*/
|
|
async disableNativeNotification(): Promise<void> {
|
|
try {
|
|
this.loading = true;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
|
|
// Cancel notification via PlatformService
|
|
await platformService.cancelDailyNotification();
|
|
|
|
// Update UI state
|
|
this.nativeNotificationEnabled = false;
|
|
this.nativeNotificationTime = "";
|
|
this.nativeNotificationTimeStorage = "";
|
|
this.showTimeEdit = false;
|
|
|
|
this.notify.success("Daily notification disabled", TIMEOUTS.SHORT);
|
|
} catch (error) {
|
|
logger.error(
|
|
"[DailyNotificationSection] Failed to disable notification:",
|
|
error,
|
|
);
|
|
this.notify.error(
|
|
"Failed to disable notification. Please try again.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show/hide time edit input
|
|
*/
|
|
editNativeNotificationTime(): void {
|
|
this.showTimeEdit = !this.showTimeEdit;
|
|
}
|
|
|
|
/**
|
|
* Handle time input change
|
|
*/
|
|
onTimeChange(): void {
|
|
// Time is already in nativeNotificationTimeStorage via v-model
|
|
// Just update display format
|
|
if (this.nativeNotificationTimeStorage) {
|
|
this.nativeNotificationTime = formatTimeForDisplay(
|
|
this.nativeNotificationTimeStorage,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save time change and update notification schedule
|
|
*/
|
|
async saveTimeChange(): Promise<void> {
|
|
if (!this.nativeNotificationTimeStorage) {
|
|
this.notify.error("Please select a time", TIMEOUTS.SHORT);
|
|
return;
|
|
}
|
|
|
|
// Update display format
|
|
this.nativeNotificationTime = formatTimeForDisplay(
|
|
this.nativeNotificationTimeStorage,
|
|
);
|
|
|
|
// If notification is enabled, update the schedule
|
|
if (this.nativeNotificationEnabled) {
|
|
await this.updateNotificationTime(this.nativeNotificationTimeStorage);
|
|
} else {
|
|
// Just update local state (time preference stored in component)
|
|
this.showTimeEdit = false;
|
|
this.notify.success("Notification time saved", TIMEOUTS.SHORT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update notification time
|
|
* If notification is enabled, immediately updates the schedule
|
|
*/
|
|
async updateNotificationTime(newTime: string): Promise<void> {
|
|
// newTime is in "HH:mm" format from HTML5 time input
|
|
if (!this.nativeNotificationEnabled) {
|
|
// If notification is disabled, just update local state
|
|
this.nativeNotificationTimeStorage = newTime;
|
|
this.nativeNotificationTime = formatTimeForDisplay(newTime);
|
|
this.showTimeEdit = false;
|
|
return;
|
|
}
|
|
|
|
// Notification is enabled - update the schedule
|
|
try {
|
|
this.loading = true;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
|
|
// 1. Cancel existing notification
|
|
await platformService.cancelDailyNotification();
|
|
|
|
// 2. Schedule with new time
|
|
await platformService.scheduleDailyNotification({
|
|
time: newTime, // "09:00" in local time
|
|
title: this.nativeNotificationTitle,
|
|
body: this.nativeNotificationMessage,
|
|
sound: true,
|
|
priority: "high",
|
|
});
|
|
|
|
// 3. Update local state
|
|
this.nativeNotificationTimeStorage = newTime;
|
|
this.nativeNotificationTime = formatTimeForDisplay(newTime);
|
|
|
|
this.notify.success(
|
|
"Notification time updated successfully",
|
|
TIMEOUTS.SHORT,
|
|
);
|
|
this.showTimeEdit = false;
|
|
} catch (error) {
|
|
logger.error(
|
|
"[DailyNotificationSection] Failed to update notification time:",
|
|
error,
|
|
);
|
|
this.notify.error(
|
|
"Failed to update notification time. Please try again.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show info dialog about native notifications
|
|
*/
|
|
showNativeNotificationInfo(): void {
|
|
// TODO: Implement info dialog or navigate to help page
|
|
this.notify.info(
|
|
"Daily notifications use your device's native notification system. They work even when the app is closed.",
|
|
TIMEOUTS.STANDARD,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Open app notification settings
|
|
*/
|
|
async openNotificationSettings(): Promise<void> {
|
|
try {
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const result = await platformService.openAppNotificationSettings();
|
|
if (result === null) {
|
|
this.notify.error(
|
|
"Unable to open settings. Please go to Settings > Apps > TimeSafari > Notifications manually.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
} else {
|
|
this.notify.success("Opening notification settings...", TIMEOUTS.SHORT);
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
"[DailyNotificationSection] Failed to open notification settings:",
|
|
error,
|
|
);
|
|
this.notify.error(
|
|
"Unable to open settings. Please go to Settings > Apps > TimeSafari > Notifications manually.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dot {
|
|
transition: left 0.2s ease;
|
|
}
|
|
</style>
|
|
|