feat: add push notification settings to user profile and update related functionality
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m50s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m50s
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
:loading="loading"
|
||||
:error="errorMsg"
|
||||
:title="'User Profile'"
|
||||
:fieldErrors="{ push_enabled: pushError }"
|
||||
@submit="handleSubmit"
|
||||
@cancel="router.back"
|
||||
@add-image="onAddImage"
|
||||
@@ -15,22 +16,6 @@
|
||||
<template #custom-field-email="{ modelValue }">
|
||||
<div class="email-actions">
|
||||
<input id="email" type="email" :value="modelValue" disabled class="readonly-input" />
|
||||
<ToggleField
|
||||
label="Daily Digest"
|
||||
:modelValue="emailDigestEnabled"
|
||||
:disabled="savingDigest"
|
||||
description="Receive a 9pm summary of pending chore and reward requests with one-click approve/deny links."
|
||||
:error="digestError"
|
||||
@update:modelValue="toggleDigest"
|
||||
/>
|
||||
<ToggleField
|
||||
label="Push Notifications"
|
||||
:modelValue="pushEnabled"
|
||||
:disabled="savingPush || getPushPermissionState() === 'denied'"
|
||||
description="Receive instant push notifications when a chore or reward needs your approval."
|
||||
:error="pushError"
|
||||
@update:modelValue="togglePush"
|
||||
/>
|
||||
<button type="button" class="btn-link btn-link-space" @click="goToChangeParentPin">
|
||||
Change Parent PIN
|
||||
</button>
|
||||
@@ -114,16 +99,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import EntityEditForm from '../shared/EntityEditForm.vue'
|
||||
import ModalDialog from '../shared/ModalDialog.vue'
|
||||
import ToggleField from '../shared/ToggleField.vue'
|
||||
import {
|
||||
isSubscribedToPush,
|
||||
subscribeToPushWithResult,
|
||||
unsubscribeFromPush,
|
||||
getPushPermissionState,
|
||||
setPushOptOut,
|
||||
} from '@/services/pushSubscription'
|
||||
import { parseErrorResponse, isEmailValid } from '@/common/api'
|
||||
import { ALREADY_MARKED } from '@/common/errorCodes'
|
||||
@@ -148,39 +133,45 @@ const showDeleteSuccess = ref(false)
|
||||
const showDeleteError = ref(false)
|
||||
const deleteErrorMessage = ref('')
|
||||
|
||||
const emailDigestEnabled = ref(true)
|
||||
const savingDigest = ref(false)
|
||||
const digestError = ref('')
|
||||
|
||||
const pushEnabled = ref(false)
|
||||
const savingPush = ref(false)
|
||||
const pushError = ref('')
|
||||
const pushPermissionDenied = ref(false)
|
||||
|
||||
const initialData = ref<{
|
||||
image_id: string | null
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
email_digest_enabled: boolean
|
||||
push_enabled: boolean
|
||||
}>({
|
||||
image_id: null,
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
email_digest_enabled: true,
|
||||
push_enabled: false,
|
||||
})
|
||||
|
||||
const fields: Array<{
|
||||
name: string
|
||||
label: string
|
||||
type: 'image' | 'text' | 'custom'
|
||||
imageType?: number
|
||||
required?: boolean
|
||||
maxlength?: number
|
||||
}> = [
|
||||
{ name: 'image_id', label: 'Image', type: 'image', imageType: 1 },
|
||||
{ name: 'first_name', label: 'First Name', type: 'text', required: true, maxlength: 64 },
|
||||
{ name: 'last_name', label: 'Last Name', type: 'text', required: true, maxlength: 64 },
|
||||
{ name: 'email', label: 'Email Address', type: 'custom' },
|
||||
]
|
||||
const fields = computed(() => [
|
||||
{ name: 'image_id', label: 'Image', type: 'image' as const, imageType: 1 },
|
||||
{ name: 'first_name', label: 'First Name', type: 'text' as const, required: true, maxlength: 64 },
|
||||
{ name: 'last_name', label: 'Last Name', type: 'text' as const, required: true, maxlength: 64 },
|
||||
{ name: 'email', label: 'Email Address', type: 'custom' as const },
|
||||
{
|
||||
name: 'email_digest_enabled',
|
||||
label: 'Daily Digest',
|
||||
type: 'toggle' as const,
|
||||
description:
|
||||
'Receive a 9pm summary of pending chore and reward requests with one-click approve/deny links.',
|
||||
},
|
||||
{
|
||||
name: 'push_enabled',
|
||||
label: 'Push Notifications',
|
||||
type: 'toggle' as const,
|
||||
description: 'Receive instant push notifications when a chore or reward needs your approval.',
|
||||
disabled: pushPermissionDenied.value,
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
@@ -188,14 +179,16 @@ onMounted(async () => {
|
||||
const res = await fetch('/api/user/profile')
|
||||
if (!res.ok) throw new Error('Failed to load profile')
|
||||
const data = await res.json()
|
||||
pushPermissionDenied.value = getPushPermissionState() === 'denied'
|
||||
const pushSubscribed = await isSubscribedToPush()
|
||||
initialData.value = {
|
||||
image_id: data.image_id || null,
|
||||
first_name: data.first_name || '',
|
||||
last_name: data.last_name || '',
|
||||
email: data.email || '',
|
||||
email_digest_enabled: data.email_digest_enabled !== false,
|
||||
push_enabled: data.push_notifications_enabled !== false && pushSubscribed,
|
||||
}
|
||||
emailDigestEnabled.value = data.email_digest_enabled !== false
|
||||
pushEnabled.value = await isSubscribedToPush()
|
||||
} catch {
|
||||
errorMsg.value = 'Could not load user profile.'
|
||||
} finally {
|
||||
@@ -217,6 +210,8 @@ function handleSubmit(form: {
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
email_digest_enabled?: boolean
|
||||
push_enabled?: boolean
|
||||
}) {
|
||||
errorMsg.value = ''
|
||||
loading.value = true
|
||||
@@ -256,31 +251,73 @@ async function updateProfile(form: {
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
email_digest_enabled?: boolean
|
||||
push_enabled?: boolean
|
||||
}) {
|
||||
fetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
const prevDigest = initialData.value.email_digest_enabled
|
||||
const prevPush = initialData.value.push_enabled
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
first_name: form.first_name,
|
||||
last_name: form.last_name,
|
||||
image_id: form.image_id,
|
||||
}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error('Failed to update profile')
|
||||
// Update initialData to reflect the saved state
|
||||
initialData.value = { ...form }
|
||||
modalTitle.value = 'Profile Updated'
|
||||
modalSubtitle.value = ''
|
||||
modalMessage.value = 'Your profile was updated successfully.'
|
||||
showModal.value = true
|
||||
})
|
||||
.catch(() => {
|
||||
errorMsg.value = 'Failed to update profile.'
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
}
|
||||
if (form.email_digest_enabled !== undefined && form.email_digest_enabled !== prevDigest) {
|
||||
body.email_digest_enabled = form.email_digest_enabled
|
||||
}
|
||||
if (form.push_enabled !== undefined && form.push_enabled !== prevPush) {
|
||||
body.push_notifications_enabled = form.push_enabled
|
||||
}
|
||||
const res = await fetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed to update profile')
|
||||
let actualPushEnabled = prevPush
|
||||
if (form.push_enabled !== undefined && form.push_enabled !== prevPush) {
|
||||
const pushOk = await applyPushChange(form.push_enabled)
|
||||
actualPushEnabled = pushOk ? form.push_enabled : prevPush
|
||||
}
|
||||
initialData.value = {
|
||||
...initialData.value,
|
||||
...form,
|
||||
push_enabled: actualPushEnabled,
|
||||
}
|
||||
modalTitle.value = 'Profile Updated'
|
||||
modalSubtitle.value = ''
|
||||
modalMessage.value = 'Your profile was updated successfully.'
|
||||
showModal.value = true
|
||||
} catch {
|
||||
errorMsg.value = 'Failed to update profile.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPushChange(newValue: boolean): Promise<boolean> {
|
||||
pushError.value = ''
|
||||
try {
|
||||
if (newValue) {
|
||||
const result = await subscribeToPushWithResult()
|
||||
if (result.ok) {
|
||||
setPushOptOut(false)
|
||||
return true
|
||||
}
|
||||
pushError.value =
|
||||
result.reason === 'permission_denied'
|
||||
? 'Notifications are blocked. Enable them in your browser settings.'
|
||||
: 'Failed to enable push notifications.'
|
||||
return false
|
||||
} else {
|
||||
await unsubscribeFromPush()
|
||||
setPushOptOut(true)
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
pushError.value = 'Failed to update push notification settings.'
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasswordModalClose() {
|
||||
@@ -317,47 +354,6 @@ async function resetPassword() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleDigest(newValue?: boolean) {
|
||||
const value = newValue ?? !emailDigestEnabled.value
|
||||
digestError.value = ''
|
||||
savingDigest.value = true
|
||||
try {
|
||||
const res = await fetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email_digest_enabled: value }),
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed to update preference')
|
||||
emailDigestEnabled.value = value
|
||||
} catch {
|
||||
digestError.value = 'Failed to save notification preference.'
|
||||
} finally {
|
||||
savingDigest.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePush(newValue: boolean) {
|
||||
pushError.value = ''
|
||||
savingPush.value = true
|
||||
try {
|
||||
if (newValue) {
|
||||
const result = await subscribeToPushWithResult()
|
||||
if (result.ok) {
|
||||
pushEnabled.value = true
|
||||
} else if (result.reason === 'permission_denied') {
|
||||
pushError.value = 'Notifications are blocked. Enable them in your browser settings.'
|
||||
} else {
|
||||
pushError.value = 'Failed to enable push notifications.'
|
||||
}
|
||||
} else {
|
||||
await unsubscribeFromPush()
|
||||
pushEnabled.value = false
|
||||
}
|
||||
} finally {
|
||||
savingPush.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToChangeParentPin() {
|
||||
router.push({ name: 'ParentPinSetup' })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user