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:
Jose Olarte III
2025-12-31 14:20:49 +08:00
parent 2f0d733b10
commit 2d353c877c
3 changed files with 591 additions and 2 deletions

View File

@@ -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',

View File

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

View File

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