Files
daily-notification-plugin/test-apps/daily-notification-test/src/views/HomeView.vue
Jose Olarte III 9565191101 Fix iOS build errors and test app setup
- Fix async/await usage in background fetch handler
- Fix Core Data metadata access errors
- Replace SQLITE_TRANSIENT with nil for Swift compatibility
- Fix PermissionStatus interface and type casts in test app
- Add iOS setup documentation to BUILDING.md
- Update iOS sync workflow to handle Podfile regeneration

Resolves all iOS compilation errors and improves test app setup process.
2025-12-30 12:35:10 +08:00

718 lines
25 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
/**
* Home View - Main Dashboard
*
* Platform-neutral home view with quick actions and plugin diagnostics
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="home-view">
<!-- Welcome Section -->
<div class="welcome-section">
<h1 class="welcome-title">
🔔 Daily Notification Test
</h1>
<p class="welcome-subtitle">
Vue 3 + Capacitor + DailyNotification Plugin
</p>
<div class="platform-info">
<span class="platform-badge" :class="platformClass">
{{ platformName }}
</span>
<span class="status-badge" :class="statusClass">
{{ statusText }}
</span>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<h2 class="section-title">Quick Actions</h2>
<div class="action-grid">
<ActionCard
icon="📅"
title="Schedule Notification"
description="Schedule a new daily notification"
@click="navigateToSchedule"
:loading="isScheduling"
/>
<ActionCard
icon="📊"
title="Check Status"
description="View notification system status"
@click="checkSystemStatus"
:loading="isCheckingStatus"
/>
<ActionCard
icon="🔐"
title="Request Permissions"
description="Check and request notification permissions"
@click="checkAndRequestPermissions"
:loading="isRequestingPermissions"
/>
<ActionCard
icon="🔔"
title="View Notifications"
description="Manage scheduled notifications"
@click="navigateToNotifications"
/>
<ActionCard
icon="📋"
title="View History"
description="Check notification history"
@click="navigateToHistory"
/>
<ActionCard
icon="📜"
title="View Logs"
description="Check system logs and debug info"
@click="navigateToLogs"
/>
<ActionCard
icon="⚙️"
title="Settings"
description="Configure app preferences"
@click="navigateToSettings"
/>
</div>
</div>
<!-- System Status -->
<div class="system-status">
<h2 class="section-title">System Status</h2>
<div class="status-grid">
<StatusCard
v-for="item in systemStatus"
:key="item.label"
:title="item.label"
:status="getStatusType(item.status)"
:value="item.value"
:description="getStatusDescription(item.label)"
@refresh="refreshSystemStatus"
/>
</div>
<!-- Diagnostic Actions -->
<div class="section">
<h2 class="section-title">🔧 Diagnostics</h2>
<ActionCard
title="Plugin Diagnostics"
description="Check plugin loading and availability"
button-text="Run Diagnostics"
@click="runPluginDiagnostics"
/>
<ActionCard
title="View Console Logs"
description="Open browser console for detailed logs"
button-text="Open Console"
@click="openConsole"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app'
import ActionCard from '@/components/cards/ActionCard.vue'
import StatusCard from '@/components/cards/StatusCard.vue'
import { Capacitor } from '@capacitor/core'
import { DailyNotification } from '@timesafari/daily-notification-plugin'
import { TEST_USER_ZERO_CONFIG, generateEndorserJWT } from '@/config/test-user-zero'
import { logger } from '@/lib/logger'
const router = useRouter()
const appStore = useAppStore()
const isScheduling = ref(false)
const isCheckingStatus = ref(false)
const isRequestingPermissions = ref(false)
const nativeFetcherConfigured = ref(false)
const platformName = computed(() => {
const platform = appStore.platform
return platform.charAt(0).toUpperCase() + platform.slice(1)
})
const platformClass = computed(() => `platform-${appStore.platform}`)
const statusClass = computed(() => {
const status = appStore.notificationStatus
if (!status) return 'unknown'
if (status.canScheduleNow) return 'ready'
return 'not-ready'
})
const statusText = computed(() => {
const status = appStore.notificationStatus
if (!status) return 'Unknown'
if (status.canScheduleNow) return 'Ready'
return 'Not Ready'
})
const systemStatus = computed(() => {
const status = appStore.notificationStatus
if (!status) {
return [
{ label: 'Platform', value: platformName.value, status: 'info' },
{ label: 'Plugin', value: 'Not Available', status: 'error' }
]
}
return [
{ label: 'Platform', value: platformName.value, status: 'info' },
{ label: 'Plugin', value: 'Available', status: 'success' },
{ label: 'Permissions', value: status.postNotificationsGranted ? 'Granted' : 'Not Granted', status: status.postNotificationsGranted ? 'success' : 'warning' },
{ label: 'Can Schedule', value: status.canScheduleNow ? 'Yes' : 'No', status: status.canScheduleNow ? 'success' : 'warning' },
{ label: 'Next Scheduled', value: status.nextScheduledAt ? new Date(status.nextScheduledAt).toLocaleTimeString() : 'None', status: 'info' }
]
})
const navigateToSchedule = (): void => {
console.log('🔄 CLICK: Navigate to Schedule')
router.push('/schedule')
}
const navigateToNotifications = (): void => {
console.log('🔄 CLICK: Navigate to Notifications')
router.push('/notifications')
}
const navigateToHistory = (): void => {
console.log('🔄 CLICK: Navigate to History')
router.push('/history')
}
const navigateToLogs = (): void => {
console.log('🔄 CLICK: Navigate to Logs')
router.push('/logs')
}
const navigateToSettings = (): void => {
console.log('🔄 CLICK: Navigate to Settings')
router.push('/settings')
}
const checkSystemStatus = async (): Promise<void> => {
console.log('🔄 CLICK: Check System Status')
isCheckingStatus.value = true
try {
console.log('🔧 Checking system status...')
// Use the same detection logic as runPluginDiagnostics
const { Capacitor } = await import('@capacitor/core')
const platform = Capacitor.getPlatform()
const isNative = platform !== 'web'
if (isNative) {
console.log('🔧 Native platform detected, checking plugin availability...')
// Check if DailyNotification plugin is available
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
const plugin = DailyNotification
const isPluginAvailable = plugin && typeof plugin.getNotificationStatus === 'function'
console.log('🔧 DailyNotification plugin check:', isPluginAvailable ? 'Available' : 'Not Available')
console.log('🔧 Plugin object:', plugin)
if (plugin) {
console.log('✅ Plugin available, checking status...')
try {
const status = await plugin.getNotificationStatus()
// Use checkPermissionStatus() which is the correct method name for iOS
const permissions = await plugin.checkPermissionStatus()
const exactAlarmStatus = await plugin.getExactAlarmStatus()
console.log('📊 Plugin status object:', status)
console.log('📊 Status values:')
console.log(' - isEnabled:', status.isEnabled)
console.log(' - isScheduled:', status.isScheduled)
console.log(' - lastNotificationTime:', status.lastNotificationTime)
console.log(' - nextNotificationTime:', status.nextNotificationTime)
console.log(' - pending:', status.pending)
console.log(' - error:', status.error)
console.log('📊 Plugin permissions:', permissions)
console.log('📊 Permissions details:')
console.log(' - notificationsEnabled:', permissions.notificationsEnabled)
console.log(' - exactAlarmEnabled:', permissions.exactAlarmEnabled)
console.log(' - wakeLockEnabled:', permissions.wakeLockEnabled)
console.log(' - allPermissionsGranted:', permissions.allPermissionsGranted)
console.log('📊 Exact alarm status:', exactAlarmStatus)
// Map plugin response to app store format
// checkPermissionStatus() returns PermissionStatusResult with boolean flags
const mappedStatus = {
canScheduleNow: status.isEnabled ?? false,
postNotificationsGranted: permissions.notificationsEnabled ?? false,
channelEnabled: true, // Default for now
channelImportance: 3, // Default for now
channelId: 'daily-notifications',
exactAlarmsGranted: exactAlarmStatus.enabled,
exactAlarmsSupported: exactAlarmStatus.supported,
androidVersion: 33, // Default for now
nextScheduledAt: typeof status.nextNotificationTime === 'number'
? status.nextNotificationTime
: await status.nextNotificationTime
}
// Update the app store status - even if permissions aren't granted
appStore.setNotificationStatus(mappedStatus)
console.log('✅ System status updated successfully')
// Log permission status for debugging
if (!mappedStatus.postNotificationsGranted) {
console.warn('⚠️ Notification permissions not granted - user needs to enable in settings')
// Only request permissions if not already requesting
if (!isRequestingPermissions.value) {
console.log('🔧 Testing permission request...')
isRequestingPermissions.value = true
// Enhanced debugging for permission request
console.log('🔍 DEBUG: Plugin object:', plugin)
console.log('🔍 DEBUG: Plugin type:', typeof plugin)
console.log('🔍 DEBUG: requestPermissions method:', typeof plugin.requestPermissions)
console.log('🔍 DEBUG: Available methods:', Object.getOwnPropertyNames(plugin))
try {
console.log('🔍 DEBUG: About to call plugin.requestPermissions()')
const result = await plugin.requestPermissions()
console.log('✅ Permission request completed, result:', result)
// After permission request, refresh the status
console.log('🔄 Refreshing status after permission request...')
await checkSystemStatus()
} catch (permError) {
console.error('❌ Permission request failed:', permError)
console.error('❌ Error details:', {
name: permError.name,
message: permError.message,
stack: permError.stack
})
} finally {
isRequestingPermissions.value = false
}
} else {
console.log('⏳ Permission request already in progress, skipping...')
}
}
} catch (error) {
console.error('❌ Plugin status check failed:', error)
// Keep existing status or set error state
}
} else {
console.warn('⚠️ DailyNotification plugin not available')
// Reset to error state
appStore.setNotificationStatus(null)
}
} else {
console.log('🌐 Web platform - plugin not available')
appStore.setNotificationStatus(null)
}
console.log('🔧 System status check completed')
} catch (error) {
console.error('❌ System status check failed:', error)
} finally {
isCheckingStatus.value = false
}
}
const getStatusType = (status: string): 'success' | 'warning' | 'error' | 'info' => {
switch (status) {
case 'success':
case 'warning':
case 'error':
case 'info':
return status
default:
return 'info'
}
}
const getStatusDescription = (label: string): string => {
switch (label) {
case 'Platform':
return 'Current platform information'
case 'Plugin':
return 'DailyNotification plugin availability'
case 'Permissions':
return 'Notification permission status'
case 'Can Schedule':
return 'Ready to schedule notifications'
case 'Next Scheduled':
return 'Next scheduled notification time'
default:
return 'System status information'
}
}
const refreshSystemStatus = async (): Promise<void> => {
console.log('🔄 CLICK: Refresh System Status')
await checkSystemStatus()
}
/**
* Check permissions and request if needed (Android pattern)
* 1. Check permission status first
* 2. If not granted, show system dialog
* 3. Refresh status after request
*/
const checkAndRequestPermissions = async (): Promise<void> => {
console.log('🔐 CLICK: Check and Request Permissions')
if (isRequestingPermissions.value) {
console.log('⏳ Permission request already in progress')
return
}
isRequestingPermissions.value = true
try {
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
const plugin = DailyNotification
if (!plugin) {
console.error('❌ DailyNotification plugin not available')
return
}
// Step 1: Check permission status first (Android pattern)
console.log('🔍 Step 1: Checking current permission status...')
const permissionStatus = await plugin.checkPermissionStatus()
console.log('📊 Permission status:', {
notificationsEnabled: permissionStatus.notificationsEnabled,
exactAlarmEnabled: permissionStatus.exactAlarmEnabled,
allPermissionsGranted: permissionStatus.allPermissionsGranted
})
// Step 2: If not granted, show system dialog
if (!permissionStatus.notificationsEnabled) {
console.log('⚠️ Permissions not granted - showing system dialog...')
console.log('📱 iOS will show native permission dialog now...')
// Request permissions - this will show the iOS system dialog
// Try requestNotificationPermissions first (iOS), fallback to requestPermissions
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')
}
console.log('✅ Permission request completed')
// Step 3: Refresh status after request
console.log('🔄 Refreshing status after permission request...')
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second for system to update
await checkSystemStatus()
} else {
console.log('✅ Permissions already granted - no dialog needed')
// Still refresh status to show current state
await checkSystemStatus()
}
} catch (error) {
console.error('❌ Permission check/request failed:', error)
console.error('❌ Error details:', {
name: (error as Error).name,
message: (error as Error).message,
stack: (error as Error).stack
})
} finally {
isRequestingPermissions.value = false
}
}
const runPluginDiagnostics = async (): Promise<void> => {
console.log('🔄 CLICK: Plugin Diagnostics - METHOD CALLED!')
console.log('🔄 FUNCTION START: runPluginDiagnostics called at', new Date().toISOString())
try {
console.log('🔧 Running plugin diagnostics...')
console.log('🔧 BUTTON CLICKED - METHOD CALLED!')
// Check if we're on a native platform
const { Capacitor } = await import('@capacitor/core')
const platform = Capacitor.getPlatform()
const isNative = Capacitor.isNativePlatform()
console.log('📱 Platform:', platform)
console.log('🔧 Native Platform:', isNative)
if (isNative) {
// Use proper plugin import
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
const plugin = DailyNotification
const isPluginAvailable = plugin && typeof plugin.getNotificationStatus === 'function'
console.log('🔍 Plugin detection debug:')
console.log(' - DailyNotification plugin:', isPluginAvailable ? 'Available' : 'Not Available')
console.log(' - Plugin object:', plugin)
if (plugin) {
console.log('✅ DailyNotification plugin available')
// Get all available plugins
const allPlugins = Object.keys((window as Window & { Capacitor?: { Plugins?: Record<string, unknown> } }).Capacitor?.Plugins || {})
console.log('📋 All available plugins:', allPlugins)
// Test the checkStatus method
try {
const status = await plugin.getNotificationStatus()
console.log('📊 Plugin status check result:', status)
// Create detailed plugin report
const pluginReport = {
platform: platform,
nativePlatform: isNative,
dailyNotificationAvailable: true,
allAvailablePlugins: allPlugins,
dailyNotificationStatus: status,
capacitorVersion: (window as Window & { Capacitor?: { getPlatform?: () => string } }).Capacitor?.getPlatform ? 'Available' : 'Unknown',
webViewInfo: {
userAgent: navigator.userAgent,
platform: navigator.platform
}
}
alert(`✅ Plugin Diagnostics Complete!\n\n` +
`Platform: ${platform}\n` +
`Native Platform: ${isNative}\n` +
`DailyNotification Plugin: Available\n` +
`All Plugins (${allPlugins.length}): ${allPlugins.join(', ')}\n\n` +
`DailyNotification Status:\n${JSON.stringify(status, null, 2)}\n\n` +
`Full Report:\n${JSON.stringify(pluginReport, null, 2)}`)
} catch (error) {
console.error('❌ Plugin status check failed:', error)
alert(`⚠️ Plugin Diagnostics Complete!\n\nPlatform: ${platform}\nPlugin Available: Yes\nStatus Check Failed: ${error}\n\nAll Available Plugins: ${allPlugins.join(', ')}`)
}
} else {
console.warn('⚠️ DailyNotification plugin not available')
const allPlugins = Object.keys((window as Window & { Capacitor?: { Plugins?: Record<string, unknown> } }).Capacitor?.Plugins || {})
alert(`❌ Plugin Diagnostics Complete!\n\nPlatform: ${platform}\nDailyNotification Plugin: Not Available\n\nAll Available Plugins (${allPlugins.length}):\n${allPlugins.join(', ')}\n\nCapacitor Plugins Object:\n${JSON.stringify((window as Window & { Capacitor?: { Plugins?: Record<string, unknown> } }).Capacitor?.Plugins || {}, null, 2)}`)
}
} else {
console.log('🌐 Running in web mode - plugin not available')
alert(` Plugin Diagnostics Complete!\n\nPlatform: ${platform}\nNative Platform: No\nPlugin Available: No (Web mode)`)
}
} catch (error) {
console.error('❌ Plugin diagnostics failed:', error)
alert(`❌ Plugin Diagnostics Failed!\n\nError: ${error}`)
}
}
const openConsole = (): void => {
console.log('🔄 CLICK: Open Console - METHOD CALLED!')
console.log('📖 Console opened - check browser developer tools for detailed logs')
alert('📖 Console Logs\n\nOpen your browser\'s Developer Tools (F12) and check the Console tab for detailed diagnostic information.')
}
// Configure native fetcher for background workers
const configureNativeFetcher = async (): Promise<void> => {
// Only configure once
if (nativeFetcherConfigured.value) {
console.log('⏭️ HomeView: Native fetcher already configured, skipping')
return
}
// Only configure on native platforms
if (!Capacitor.isNativePlatform()) {
console.log('⏭️ HomeView: Web platform - skipping native fetcher configuration')
return
}
try {
console.log('🚀 HomeView: Starting native fetcher configuration...')
console.log('👤 HomeView: Using User Zero identity:', TEST_USER_ZERO_CONFIG.identity.name)
console.log('🔑 HomeView: User Zero DID:', TEST_USER_ZERO_CONFIG.identity.did)
logger.info('Configuring native fetcher from HomeView using User Zero identity...')
// Get API server URL
const apiBaseUrl = TEST_USER_ZERO_CONFIG.getApiServerUrl()
console.log('🔧 HomeView: API Base URL:', apiBaseUrl)
console.log('🔧 HomeView: Server Mode:', TEST_USER_ZERO_CONFIG.api.serverMode)
// Skip configuration if in mock mode
if (TEST_USER_ZERO_CONFIG.api.serverMode === 'mock') {
console.log('⏭️ HomeView: Mock mode - skipping configuration')
logger.warn('Mock mode enabled - native fetcher will not be configured')
nativeFetcherConfigured.value = true // Mark as "configured" to prevent retries
return
}
console.log('🔧 HomeView: Generating ES256K JWT token for User Zero...')
// Generate JWT token for authentication using User Zero's DID and seed phrase
// This uses TEST_USER_ZERO_CONFIG.identity.did and TEST_USER_ZERO_CONFIG.identity.seedPhrase
const jwtToken = await generateEndorserJWT()
console.log('✅ HomeView: JWT token generated for User Zero, length:', jwtToken.length)
console.log('🔧 HomeView: Configuring native fetcher with User Zero credentials:', {
apiBaseUrl,
activeDid: TEST_USER_ZERO_CONFIG.identity.did,
userZeroName: TEST_USER_ZERO_CONFIG.identity.name,
jwtTokenLength: jwtToken.length
})
// Configure native fetcher with User Zero's credentials
// This passes User Zero's DID and JWT token (signed with User Zero's private key)
await DailyNotification.configureNativeFetcher({
apiBaseUrl: apiBaseUrl,
activeDid: TEST_USER_ZERO_CONFIG.identity.did, // User Zero's DID
jwtToken: jwtToken // JWT signed with User Zero's private key derived from seed phrase
})
console.log('✅ HomeView: Native fetcher configured successfully!')
logger.info('Native fetcher configured successfully', {
apiBaseUrl: apiBaseUrl.substring(0, 50) + '...',
activeDid: TEST_USER_ZERO_CONFIG.identity.did.substring(0, 30) + '...'
})
// Update starred plan IDs from config (non-blocking - if this fails, fetcher is still configured)
try {
console.log('🔧 HomeView: Updating starred plan IDs...')
const planIds = [...TEST_USER_ZERO_CONFIG.starredProjects.planIds]
console.log('🔧 HomeView: Plan IDs to update:', planIds.length, 'plans')
console.log('🔧 HomeView: Plan IDs array:', JSON.stringify(planIds))
const updateResult = await DailyNotification.updateStarredPlans({
planIds: planIds
})
console.log('✅ HomeView: Starred plans updated:', {
count: updateResult.planIdsCount,
updatedAt: new Date(updateResult.updatedAt).toISOString()
})
} catch (starredPlansError) {
// Non-critical error - native fetcher is already configured
console.warn('⚠️ HomeView: Failed to update starred plans (non-critical):', starredPlansError)
logger.warn('Starred plans update failed (native fetcher is still configured)', {
error: starredPlansError instanceof Error ? starredPlansError.message : String(starredPlansError)
})
}
// Mark as configured to prevent duplicate configuration
nativeFetcherConfigured.value = true
} catch (error) {
console.error('❌ HomeView: Failed to configure native fetcher:', error)
console.error('❌ HomeView: Error details:', error instanceof Error ? error.stack : String(error))
logger.error('Failed to configure native fetcher from HomeView:', error)
// Don't mark as configured on error, so it can retry on next mount
}
}
// Initialize system status and native fetcher when component mounts
onMounted(async () => {
console.log('🏠 HomeView mounted - checking initial system status and configuring native fetcher...')
// Configure native fetcher first (needed for background workers)
await configureNativeFetcher()
// Then check system status
await checkSystemStatus()
})
</script>
<style scoped>
.home-view {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.welcome-section {
text-align: center;
margin-bottom: 40px;
}
.welcome-title {
font-size: 32px;
font-weight: 700;
color: white;
margin: 0 0 8px 0;
}
.welcome-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 20px 0;
}
.platform-info {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.platform-badge {
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge {
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
}
.quick-actions {
margin-bottom: 40px;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: white;
margin: 0 0 20px 0;
text-align: center;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.system-status {
margin-bottom: 40px;
}
.section {
margin-bottom: 30px;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.home-view {
padding: 16px;
}
.welcome-title {
font-size: 24px;
}
.action-grid {
grid-template-columns: 1fr;
}
.platform-info {
flex-direction: column;
align-items: center;
}
}
</style>