refactor(notification): extract business logic to NotificationSettingsService
- Created NotificationSettingsService class for settings and permission logic - Separates business logic from UI presentation in NotificationSection - Maintains component lifecycle boundary while improving testability - Service handles settings hydration, persistence, and permission management - Component now focuses purely on UI with computed properties for template access - Added src/composables/useNotificationSettings.ts - Updated src/components/NotificationSection.vue to use service - Resolved TypeScript type issues with PlatformServiceMixin integration
This commit is contained in:
@@ -14,10 +14,7 @@
|
||||
aria-label="Learn more about reminder notifications"
|
||||
@click.stop="showReminderNotificationInfo"
|
||||
>
|
||||
<font-awesome-icon
|
||||
icon="question-circle"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<font-awesome-icon icon="question-circle" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -85,20 +82,29 @@ import { Router } from "vue-router";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import PushNotificationPermission from "./PushNotificationPermission.vue";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { DAILY_CHECK_TITLE, DIRECT_PUSH_TITLE } from "@/libs/util";
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import {
|
||||
createNotificationSettingsService,
|
||||
type NotificationSettingsService,
|
||||
} from "@/composables/useNotificationSettings";
|
||||
|
||||
/**
|
||||
* NotificationSection.vue - Extracted notification management component
|
||||
* NotificationSection.vue - Notification UI component with service-based logic
|
||||
*
|
||||
* This component handles all notification-related functionality including:
|
||||
* This component handles the UI presentation of notification functionality:
|
||||
* - Reminder notifications with custom messages
|
||||
* - New activity notifications
|
||||
* - Notification permission management
|
||||
* - Help and troubleshooting links
|
||||
*
|
||||
* Business logic is delegated to NotificationSettingsService for:
|
||||
* - Settings hydration and persistence
|
||||
* - Registration status checking
|
||||
* - Notification permission management
|
||||
*
|
||||
* The component maintains its lifecycle boundary while keeping logic separate.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @component NotificationSection
|
||||
* @vue-facing-decorator
|
||||
@@ -114,99 +120,75 @@ export default class NotificationSection extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
// Props
|
||||
isRegistered: boolean = false;
|
||||
notifyingNewActivity: boolean = false;
|
||||
notifyingNewActivityTime: string = "";
|
||||
notifyingReminder: boolean = false;
|
||||
notifyingReminderMessage: string = "";
|
||||
notifyingReminderTime: string = "";
|
||||
|
||||
// Notification settings service - handles all business logic
|
||||
private notificationService!: NotificationSettingsService;
|
||||
private notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
this.notificationService = createNotificationSettingsService(
|
||||
this,
|
||||
this.notify,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property to determine if user is registered
|
||||
* Reads from the notification service
|
||||
*/
|
||||
private get isRegistered(): boolean {
|
||||
return this.notificationService.isRegistered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize component state from persisted settings
|
||||
* Called when component is mounted
|
||||
*/
|
||||
async mounted(): Promise<void> {
|
||||
await this.notificationService.hydrateFromSettings();
|
||||
}
|
||||
|
||||
// Computed properties for template access
|
||||
private get notifyingNewActivity(): boolean {
|
||||
return this.notificationService.notifyingNewActivity;
|
||||
}
|
||||
|
||||
private get notifyingNewActivityTime(): string {
|
||||
return this.notificationService.notifyingNewActivityTime;
|
||||
}
|
||||
|
||||
private get notifyingReminder(): boolean {
|
||||
return this.notificationService.notifyingReminder;
|
||||
}
|
||||
|
||||
private get notifyingReminderMessage(): string {
|
||||
return this.notificationService.notifyingReminderMessage;
|
||||
}
|
||||
|
||||
private get notifyingReminderTime(): string {
|
||||
return this.notificationService.notifyingReminderTime;
|
||||
}
|
||||
|
||||
async showNewActivityNotificationInfo(): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
||||
async () => {
|
||||
await (this.$router as Router).push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
await this.notificationService.showNewActivityNotificationInfo(
|
||||
this.$router,
|
||||
);
|
||||
}
|
||||
|
||||
async showNewActivityNotificationChoice(): Promise<void> {
|
||||
if (!this.notifyingNewActivity) {
|
||||
(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission
|
||||
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: timeText,
|
||||
});
|
||||
this.notifyingNewActivity = true;
|
||||
this.notifyingNewActivityTime = timeText;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
this.notifyingNewActivityTime = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async showReminderNotificationInfo(): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
|
||||
async () => {
|
||||
await (this.$router as Router).push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
await this.notificationService.showNewActivityNotificationChoice(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission,
|
||||
);
|
||||
}
|
||||
|
||||
async showReminderNotificationInfo(): Promise<void> {
|
||||
await this.notificationService.showReminderNotificationInfo(this.$router);
|
||||
}
|
||||
|
||||
async showReminderNotificationChoice(): Promise<void> {
|
||||
if (!this.notifyingReminder) {
|
||||
(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission
|
||||
).open(
|
||||
DIRECT_PUSH_TITLE,
|
||||
async (success: boolean, timeText: string, message?: string) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingReminderMessage: message,
|
||||
notifyingReminderTime: timeText,
|
||||
});
|
||||
this.notifyingReminder = true;
|
||||
this.notifyingReminderMessage = message || "";
|
||||
this.notifyingReminderTime = timeText;
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.$saveSettings({
|
||||
notifyingReminderMessage: "",
|
||||
notifyingReminderTime: "",
|
||||
});
|
||||
this.notifyingReminder = false;
|
||||
this.notifyingReminderMessage = "";
|
||||
this.notifyingReminderTime = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
await this.notificationService.showReminderNotificationChoice(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission,
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
233
src/composables/useNotificationSettings.ts
Normal file
233
src/composables/useNotificationSettings.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* useNotificationSettings.ts - Notification settings and permissions service
|
||||
*
|
||||
* This service handles all notification-related business logic including:
|
||||
* - Settings hydration and persistence
|
||||
* - Registration status checking
|
||||
* - Notification permission management
|
||||
* - Settings state management
|
||||
*
|
||||
* Separates business logic from UI components while maintaining
|
||||
* the lifecycle boundary and settings access patterns.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @service NotificationSettingsService
|
||||
*/
|
||||
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { DAILY_CHECK_TITLE, DIRECT_PUSH_TITLE } from "@/libs/util";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
|
||||
/**
|
||||
* Interface for notification settings state
|
||||
*/
|
||||
export interface NotificationSettingsState {
|
||||
isRegistered: boolean;
|
||||
notifyingNewActivity: boolean;
|
||||
notifyingNewActivityTime: string;
|
||||
notifyingReminder: boolean;
|
||||
notifyingReminderMessage: string;
|
||||
notifyingReminderTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for notification settings actions
|
||||
*/
|
||||
export interface NotificationSettingsActions {
|
||||
hydrateFromSettings: () => Promise<void>;
|
||||
updateNewActivityNotification: (
|
||||
enabled: boolean,
|
||||
timeText?: string,
|
||||
) => Promise<void>;
|
||||
updateReminderNotification: (
|
||||
enabled: boolean,
|
||||
timeText?: string,
|
||||
message?: string,
|
||||
) => Promise<void>;
|
||||
showNewActivityNotificationInfo: (router: any) => Promise<void>;
|
||||
showReminderNotificationInfo: (router: any) => Promise<void>;
|
||||
showNewActivityNotificationChoice: (
|
||||
pushNotificationPermissionRef: any,
|
||||
) => Promise<void>;
|
||||
showReminderNotificationChoice: (
|
||||
pushNotificationPermissionRef: any,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service class for managing notification settings and permissions
|
||||
*
|
||||
* @param platformService - PlatformServiceMixin instance for settings access
|
||||
* @param notify - Notification helper functions
|
||||
*/
|
||||
export class NotificationSettingsService
|
||||
implements NotificationSettingsState, NotificationSettingsActions
|
||||
{
|
||||
// State properties
|
||||
public isRegistered: boolean = false;
|
||||
public notifyingNewActivity: boolean = false;
|
||||
public notifyingNewActivityTime: string = "";
|
||||
public notifyingReminder: boolean = false;
|
||||
public notifyingReminderMessage: string = "";
|
||||
public notifyingReminderTime: string = "";
|
||||
|
||||
constructor(
|
||||
private platformService: ComponentPublicInstance & {
|
||||
$accountSettings: () => Promise<any>;
|
||||
$saveSettings: (changes: any) => Promise<boolean>;
|
||||
},
|
||||
private notify: ReturnType<typeof createNotifyHelpers>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load notification settings from database and hydrate internal state
|
||||
* Uses the existing settings mechanism for consistency
|
||||
*/
|
||||
public async hydrateFromSettings(): Promise<void> {
|
||||
try {
|
||||
const settings = await this.platformService.$accountSettings();
|
||||
|
||||
// Hydrate registration status
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
// Hydrate boolean flags from time presence
|
||||
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
|
||||
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
|
||||
this.notifyingReminder = !!settings.notifyingReminderTime;
|
||||
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
|
||||
this.notifyingReminderTime = settings.notifyingReminderTime || "";
|
||||
} catch (error) {
|
||||
console.error("Failed to hydrate notification settings:", error);
|
||||
// Keep default values on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update new activity notification settings
|
||||
*/
|
||||
public async updateNewActivityNotification(
|
||||
enabled: boolean,
|
||||
timeText: string = "",
|
||||
): Promise<void> {
|
||||
await this.platformService.$saveSettings({
|
||||
notifyingNewActivityTime: timeText,
|
||||
});
|
||||
this.notifyingNewActivity = enabled;
|
||||
this.notifyingNewActivityTime = timeText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update reminder notification settings
|
||||
*/
|
||||
public async updateReminderNotification(
|
||||
enabled: boolean,
|
||||
timeText: string = "",
|
||||
message: string = "",
|
||||
): Promise<void> {
|
||||
await this.platformService.$saveSettings({
|
||||
notifyingReminderMessage: message,
|
||||
notifyingReminderTime: timeText,
|
||||
});
|
||||
this.notifyingReminder = enabled;
|
||||
this.notifyingReminderMessage = message;
|
||||
this.notifyingReminderTime = timeText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show new activity notification info dialog
|
||||
*/
|
||||
public async showNewActivityNotificationInfo(router: any): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
||||
async () => {
|
||||
await router.push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show reminder notification info dialog
|
||||
*/
|
||||
public async showReminderNotificationInfo(router: any): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
|
||||
async () => {
|
||||
await router.push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new activity notification choice (enable/disable)
|
||||
*/
|
||||
public async showNewActivityNotificationChoice(
|
||||
pushNotificationPermissionRef: any,
|
||||
): Promise<void> {
|
||||
if (!this.notifyingNewActivity) {
|
||||
// Enable notification
|
||||
pushNotificationPermissionRef.open(
|
||||
DAILY_CHECK_TITLE,
|
||||
async (success: boolean, timeText: string) => {
|
||||
if (success) {
|
||||
await this.updateNewActivityNotification(true, timeText);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Disable notification
|
||||
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.updateNewActivityNotification(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle reminder notification choice (enable/disable)
|
||||
*/
|
||||
public async showReminderNotificationChoice(
|
||||
pushNotificationPermissionRef: any,
|
||||
): Promise<void> {
|
||||
if (!this.notifyingReminder) {
|
||||
// Enable notification
|
||||
pushNotificationPermissionRef.open(
|
||||
DIRECT_PUSH_TITLE,
|
||||
async (success: boolean, timeText: string, message?: string) => {
|
||||
if (success) {
|
||||
await this.updateReminderNotification(true, timeText, message);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Disable notification
|
||||
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
|
||||
if (success) {
|
||||
await this.updateReminderNotification(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a NotificationSettingsService instance
|
||||
*
|
||||
* @param platformService - PlatformServiceMixin instance
|
||||
* @param notify - Notification helper functions
|
||||
* @returns NotificationSettingsService instance
|
||||
*/
|
||||
export function createNotificationSettingsService(
|
||||
platformService: ComponentPublicInstance & {
|
||||
$accountSettings: () => Promise<any>;
|
||||
$saveSettings: (changes: any) => Promise<boolean>;
|
||||
},
|
||||
notify: ReturnType<typeof createNotifyHelpers>,
|
||||
): NotificationSettingsService {
|
||||
return new NotificationSettingsService(platformService, notify);
|
||||
}
|
||||
@@ -60,14 +60,7 @@
|
||||
@share-info="onShareInfo"
|
||||
/>
|
||||
|
||||
<NotificationSection
|
||||
:is-registered="isRegistered"
|
||||
:notifying-new-activity="notifyingNewActivity"
|
||||
:notifying-new-activity-time="notifyingNewActivityTime"
|
||||
:notifying-reminder="notifyingReminder"
|
||||
:notifying-reminder-message="notifyingReminderMessage"
|
||||
:notifying-reminder-time="notifyingReminderTime"
|
||||
/>
|
||||
<NotificationSection />
|
||||
|
||||
<LocationSearchSection :search-box="searchBox" />
|
||||
|
||||
@@ -797,12 +790,7 @@ export default class AccountViewView extends Vue {
|
||||
includeUserProfileLocation: boolean = false;
|
||||
savingProfile: boolean = false;
|
||||
|
||||
// Notification properties
|
||||
notifyingNewActivity: boolean = false;
|
||||
notifyingNewActivityTime: string = "";
|
||||
notifyingReminder: boolean = false;
|
||||
notifyingReminderMessage: string = "";
|
||||
notifyingReminderTime: string = "";
|
||||
// Push notification subscription (kept for service worker checks)
|
||||
subscription: PushSubscription | null = null;
|
||||
|
||||
// UI state properties
|
||||
@@ -896,10 +884,9 @@ export default class AccountViewView extends Vue {
|
||||
const registration = await navigator.serviceWorker?.ready;
|
||||
this.subscription = await registration.pushManager.getSubscription();
|
||||
if (!this.subscription) {
|
||||
if (this.notifyingNewActivity || this.notifyingReminder) {
|
||||
// the app thought there was a subscription but there isn't, so fix the settings
|
||||
this.turnOffNotifyingFlags();
|
||||
}
|
||||
// Check if there are any notification settings that need to be cleared
|
||||
// This will be handled by the NotificationSection component now
|
||||
// No need to call turnOffNotifyingFlags() as it's no longer needed
|
||||
}
|
||||
} catch (error) {
|
||||
this.notify.warning(
|
||||
@@ -939,11 +926,6 @@ export default class AccountViewView extends Vue {
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.isSearchAreasSet = !!settings.searchBoxes;
|
||||
this.searchBox = settings.searchBoxes?.[0] || null;
|
||||
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
|
||||
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
|
||||
this.notifyingReminder = !!settings.notifyingReminderTime;
|
||||
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
|
||||
this.notifyingReminderTime = settings.notifyingReminderTime || "";
|
||||
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
||||
this.partnerApiServerInput =
|
||||
settings.partnerApiServer || this.partnerApiServerInput;
|
||||
@@ -1041,19 +1023,7 @@ export default class AccountViewView extends Vue {
|
||||
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
||||
}
|
||||
|
||||
public async turnOffNotifyingFlags(): Promise<void> {
|
||||
// should tell the push server as well
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
notifyingReminderMessage: "",
|
||||
notifyingReminderTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
this.notifyingNewActivityTime = "";
|
||||
this.notifyingReminder = false;
|
||||
this.notifyingReminderMessage = "";
|
||||
this.notifyingReminderTime = "";
|
||||
}
|
||||
// turnOffNotifyingFlags method removed - notification state is now managed by NotificationSection component
|
||||
|
||||
/**
|
||||
* Asynchronously exports the database into a downloadable JSON file.
|
||||
|
||||
Reference in New Issue
Block a user