feat(test-app): add dedicated Request Permissions view
Create a new RequestPermissionsView that provides a dedicated interface for checking and requesting notification permissions, matching the functionality found in the iOS test app. The view includes: - Status display with color-coded states (requesting/granted/error) - Permission status grid showing notifications, exact alarm, and background refresh status - Request Permissions button with same functionality as iOS test app - Platform-specific settings access buttons - Automatic status refresh on mount Updated HomeView to navigate to the new view instead of calling permission request function directly, providing better UX with dedicated screen for permission management.
This commit is contained in:
@@ -77,6 +77,15 @@ const router = createRouter({
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/request-permissions',
|
||||
name: 'RequestPermissions',
|
||||
component: (): Promise<typeof import('../views/RequestPermissionsView.vue')> => import('../views/RequestPermissionsView.vue'),
|
||||
meta: {
|
||||
title: 'Request Permissions',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
icon="🔐"
|
||||
title="Request Permissions"
|
||||
description="Check and request notification permissions"
|
||||
@click="checkAndRequestPermissions"
|
||||
:loading="isRequestingPermissions"
|
||||
@click="navigateToRequestPermissions"
|
||||
/>
|
||||
<ActionCard
|
||||
icon="🔔"
|
||||
@@ -203,6 +202,11 @@ const navigateToStatus = (): void => {
|
||||
router.push('/status')
|
||||
}
|
||||
|
||||
const navigateToRequestPermissions = (): void => {
|
||||
console.log('🔄 CLICK: Navigate to Request Permissions')
|
||||
router.push('/request-permissions')
|
||||
}
|
||||
|
||||
const checkSystemStatus = async (): Promise<void> => {
|
||||
console.log('🔄 CLICK: Check System Status')
|
||||
isCheckingStatus.value = true
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
<!--
|
||||
/**
|
||||
* Request Permissions View - Permission Management Interface
|
||||
*
|
||||
* Platform-neutral permissions view with status display and request functionality
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="request-permissions-view">
|
||||
<div class="view-header">
|
||||
<div class="header-title-row">
|
||||
<button class="back-button" @click="goBack" aria-label="Go back to home">
|
||||
←
|
||||
</button>
|
||||
<h1 class="page-title">🔐 Request Permissions</h1>
|
||||
</div>
|
||||
<p class="page-subtitle">Check and request notification permissions</p>
|
||||
</div>
|
||||
|
||||
<!-- Status Display -->
|
||||
<div class="status-section">
|
||||
<div class="status-card" :class="statusCardClass">
|
||||
<div class="status-content">
|
||||
<div class="status-icon">{{ statusIcon }}</div>
|
||||
<div class="status-text">{{ statusText }}</div>
|
||||
<div v-if="statusDetails" class="status-details">{{ statusDetails }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permission Details -->
|
||||
<div class="permissions-section">
|
||||
<h2 class="section-title">Permission Status</h2>
|
||||
<div class="permissions-grid">
|
||||
<div class="permission-item">
|
||||
<span class="permission-label">🔔 Notifications:</span>
|
||||
<span class="permission-value" :class="notificationStatusClass">
|
||||
{{ notificationStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="exactAlarmStatusText" class="permission-item">
|
||||
<span class="permission-label">⏰ Exact Alarm:</span>
|
||||
<span class="permission-value" :class="exactAlarmStatusClass">
|
||||
{{ exactAlarmStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="backgroundRefreshStatusText" class="permission-item">
|
||||
<span class="permission-label">🔄 Background Refresh:</span>
|
||||
<span class="permission-value" :class="backgroundRefreshStatusClass">
|
||||
{{ backgroundRefreshStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<div class="action-section">
|
||||
<button
|
||||
class="action-button primary"
|
||||
@click="requestPermissions"
|
||||
:disabled="isRequesting"
|
||||
>
|
||||
{{ isRequesting ? 'Requesting...' : 'Request Permissions' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action-button secondary"
|
||||
@click="refreshStatus"
|
||||
:disabled="isRefreshing"
|
||||
>
|
||||
{{ isRefreshing ? 'Refreshing...' : 'Refresh Status' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Additional Actions -->
|
||||
<div class="additional-actions">
|
||||
<button
|
||||
class="action-button tertiary"
|
||||
@click="openNotificationSettings"
|
||||
>
|
||||
Open Notification Settings
|
||||
</button>
|
||||
<button
|
||||
v-if="showBackgroundRefreshButton"
|
||||
class="action-button tertiary"
|
||||
@click="openBackgroundRefreshSettings"
|
||||
>
|
||||
Open Background Refresh Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { DailyNotification } from '@timesafari/daily-notification-plugin'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const isRequesting = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const statusText = ref('Ready to check permissions...')
|
||||
const statusDetails = ref('')
|
||||
const notificationGranted = ref(false)
|
||||
const exactAlarmGranted = ref(false)
|
||||
const backgroundRefreshEnabled = ref(false)
|
||||
const showBackgroundRefreshButton = ref(false)
|
||||
|
||||
const statusCardClass = computed(() => {
|
||||
if (isRequesting.value || isRefreshing.value) return 'status-yellow'
|
||||
if (notificationGranted.value) return 'status-green'
|
||||
return 'status-gray'
|
||||
})
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
if (isRequesting.value || isRefreshing.value) return '⏳'
|
||||
if (notificationGranted.value) return '✅'
|
||||
return '⚠️'
|
||||
})
|
||||
|
||||
const notificationStatusClass = computed(() => {
|
||||
return notificationGranted.value ? 'status-success' : 'status-error'
|
||||
})
|
||||
|
||||
const notificationStatusText = computed(() => {
|
||||
return notificationGranted.value ? 'Granted' : 'Not Granted'
|
||||
})
|
||||
|
||||
const exactAlarmStatusClass = computed(() => {
|
||||
return exactAlarmGranted.value ? 'status-success' : 'status-warning'
|
||||
})
|
||||
|
||||
const exactAlarmStatusText = computed(() => {
|
||||
if (!exactAlarmGranted.value && Capacitor.getPlatform() === 'android') {
|
||||
return 'Not Granted'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const backgroundRefreshStatusClass = computed(() => {
|
||||
return backgroundRefreshEnabled.value ? 'status-success' : 'status-warning'
|
||||
})
|
||||
|
||||
const backgroundRefreshStatusText = computed(() => {
|
||||
if (Capacitor.getPlatform() === 'ios') {
|
||||
return backgroundRefreshEnabled.value ? 'Enabled' : 'Not Enabled'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const checkStatus = async (): Promise<void> => {
|
||||
isRefreshing.value = true
|
||||
statusText.value = 'Checking permissions...'
|
||||
statusDetails.value = ''
|
||||
|
||||
try {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
statusText.value = 'Web platform - permissions not available'
|
||||
statusDetails.value = 'This feature requires a native platform (iOS or Android)'
|
||||
notificationGranted.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const plugin = DailyNotification
|
||||
if (!plugin) {
|
||||
statusText.value = 'Plugin not available'
|
||||
statusDetails.value = 'DailyNotification plugin could not be loaded'
|
||||
notificationGranted.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission status
|
||||
const permissionStatus = await plugin.checkPermissionStatus()
|
||||
notificationGranted.value = permissionStatus.notificationsEnabled ?? false
|
||||
|
||||
// Check exact alarm status (Android only)
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
try {
|
||||
const exactAlarmStatus = await plugin.getExactAlarmStatus()
|
||||
exactAlarmGranted.value = exactAlarmStatus.enabled ?? false
|
||||
} catch (error) {
|
||||
console.warn('Could not check exact alarm status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Check background refresh status (iOS only)
|
||||
if (Capacitor.getPlatform() === 'ios') {
|
||||
try {
|
||||
if (typeof (plugin as any).getBackgroundTaskStatus === 'function') {
|
||||
const backgroundStatus = await (plugin as any).getBackgroundTaskStatus()
|
||||
// Check if tasks are registered (either fetchTaskRegistered or notifyTaskRegistered)
|
||||
const tasksRegistered = backgroundStatus.tasksRegistered ??
|
||||
backgroundStatus.fetchTaskRegistered ??
|
||||
backgroundStatus.notifyTaskRegistered ??
|
||||
false
|
||||
backgroundRefreshEnabled.value = tasksRegistered
|
||||
showBackgroundRefreshButton.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not check background refresh status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationGranted.value) {
|
||||
statusText.value = 'Permission request completed! Check your device settings if needed.'
|
||||
statusDetails.value = 'Notifications are enabled and ready to use.'
|
||||
} else {
|
||||
statusText.value = 'Permissions not granted'
|
||||
statusDetails.value = 'Tap "Request Permissions" to enable notifications.'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to check permissions:', error)
|
||||
statusText.value = `Status check failed: ${(error as Error).message}`
|
||||
statusDetails.value = 'Please try again or check the console for details.'
|
||||
notificationGranted.value = false
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const requestPermissions = async (): Promise<void> => {
|
||||
if (isRequesting.value) {
|
||||
console.log('⏳ Permission request already in progress')
|
||||
return
|
||||
}
|
||||
|
||||
isRequesting.value = true
|
||||
statusText.value = 'Requesting permissions...'
|
||||
statusDetails.value = ''
|
||||
|
||||
try {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
statusText.value = 'Web platform - permissions not available'
|
||||
statusDetails.value = 'This feature requires a native platform (iOS or Android)'
|
||||
return
|
||||
}
|
||||
|
||||
const plugin = DailyNotification
|
||||
if (!plugin) {
|
||||
statusText.value = 'Plugin not available'
|
||||
statusDetails.value = 'DailyNotification plugin could not be loaded'
|
||||
return
|
||||
}
|
||||
|
||||
// Request permissions - try iOS-specific method first, fallback to generic
|
||||
if (typeof (plugin as any).requestNotificationPermissions === 'function') {
|
||||
await (plugin as { requestNotificationPermissions: () => Promise<any> }).requestNotificationPermissions()
|
||||
} else if (typeof (plugin as any).requestPermissions === 'function') {
|
||||
await (plugin as { requestPermissions: () => Promise<any> }).requestPermissions()
|
||||
} else {
|
||||
throw new Error('Permission request method not available')
|
||||
}
|
||||
|
||||
statusText.value = 'Permission request completed! Check your device settings if needed.'
|
||||
statusDetails.value = 'Refreshing status...'
|
||||
|
||||
// Wait a moment for system to update, then refresh status
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
await checkStatus()
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Permission request failed:', error)
|
||||
statusText.value = `Permission request failed: ${(error as Error).message}`
|
||||
statusDetails.value = 'Please check your device settings or try again.'
|
||||
} finally {
|
||||
isRequesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshStatus = async (): Promise<void> => {
|
||||
await checkStatus()
|
||||
}
|
||||
|
||||
const openNotificationSettings = async (): Promise<void> => {
|
||||
try {
|
||||
const plugin = DailyNotification
|
||||
if (!plugin) {
|
||||
alert('Plugin not available')
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof (plugin as any).openNotificationSettings === 'function') {
|
||||
await (plugin as any).openNotificationSettings()
|
||||
} else {
|
||||
alert('Open notification settings not available on this platform')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to open notification settings:', error)
|
||||
alert(`Failed to open settings: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const openBackgroundRefreshSettings = async (): Promise<void> => {
|
||||
try {
|
||||
const plugin = DailyNotification
|
||||
if (!plugin) {
|
||||
alert('Plugin not available')
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof (plugin as any).openBackgroundAppRefreshSettings === 'function') {
|
||||
await (plugin as any).openBackgroundAppRefreshSettings()
|
||||
} else {
|
||||
alert('Open background refresh settings not available on this platform')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to open background refresh settings:', error)
|
||||
alert(`Failed to open settings: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = (): void => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// Check status when component mounts
|
||||
onMounted(async () => {
|
||||
await checkStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.request-permissions-view {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Status Section */
|
||||
.status-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-card.status-yellow {
|
||||
background: rgba(255, 255, 0, 0.3);
|
||||
border-color: rgba(255, 255, 0, 0.5);
|
||||
}
|
||||
|
||||
.status-card.status-green {
|
||||
background: rgba(0, 255, 0, 0.3);
|
||||
border-color: rgba(0, 255, 0, 0.5);
|
||||
}
|
||||
|
||||
.status-card.status-gray {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-details {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Permissions Section */
|
||||
.permissions-section {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.permissions-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.permission-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.permission-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.permission-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.permission-value.status-success {
|
||||
background: rgba(34, 197, 94, 0.3);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.permission-value.status-error {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.permission-value.status-warning {
|
||||
background: rgba(251, 191, 36, 0.3);
|
||||
color: #fde047;
|
||||
}
|
||||
|
||||
/* Action Section */
|
||||
.action-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.primary:hover:not(:disabled) {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.secondary:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.action-button.tertiary {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.tertiary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.additional-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.request-permissions-view {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.permissions-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-title-row {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user