Browse Source

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
pull/150/head
Matthew Raymer 7 days ago
parent
commit
d62178bca5
  1. 146
      src/components/NotificationSection.vue
  2. 233
      src/composables/useNotificationSettings.ts
  3. 42
      src/views/AccountViewView.vue

146
src/components/NotificationSection.vue

@ -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,
);
}
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",
});
},
);
/**
* Computed property to determine if user is registered
* Reads from the notification service
*/
private get isRegistered(): boolean {
return this.notificationService.isRegistered;
}
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;
/**
* Initialize component state from persisted settings
* Called when component is mounted
*/
async mounted(): Promise<void> {
await this.notificationService.hydrateFromSettings();
}
});
} else {
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingNewActivityTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
// 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;
}
async showReminderNotificationInfo(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
);
private get notifyingReminderMessage(): string {
return this.notificationService.notifyingReminderMessage;
}
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;
private get notifyingReminderTime(): string {
return this.notificationService.notifyingReminderTime;
}
},
async showNewActivityNotificationInfo(): Promise<void> {
await this.notificationService.showNewActivityNotificationInfo(
this.$router,
);
} else {
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
});
async showNewActivityNotificationChoice(): Promise<void> {
await this.notificationService.showNewActivityNotificationChoice(
this.$refs.pushNotificationPermission as PushNotificationPermission,
);
}
async showReminderNotificationInfo(): Promise<void> {
await this.notificationService.showReminderNotificationInfo(this.$router);
}
async showReminderNotificationChoice(): Promise<void> {
await this.notificationService.showReminderNotificationChoice(
this.$refs.pushNotificationPermission as PushNotificationPermission,
);
}
}
</script>

233
src/composables/useNotificationSettings.ts

@ -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);
}

42
src/views/AccountViewView.vue

@ -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.

Loading…
Cancel
Save