feat(test-apps/daily-notification-test): implement notification status display in NotificationsView

Replace placeholder content with functional notification status viewer that
displays scheduled notifications and rollover information. Enables verification
of both manually scheduled notifications and automatic rollover scheduling
(24-hour recurrence).

Features:
- Display next scheduled notification time with formatted date/time
- Show time until next notification (days, hours, minutes)
- Display pending notification count
- Show last notification delivery time
- Display rollover status (enabled/disabled, last rollover time) when available
- Additional status info (enabled, scheduled, errors)
- Manual refresh button for status updates
- Loading and error states with platform detection

Uses typed plugin wrapper for type safety with fallback to raw plugin access
for rollover fields not in TypeScript interface (iOS-specific fields).
This commit is contained in:
Jose Olarte III
2026-01-05 20:57:15 +08:00
parent 911aabf671
commit f97b3bec5b

View File

@@ -7,23 +7,274 @@
</button>
<h1 class="page-title">🔔 Notifications</h1>
</div>
<p class="page-subtitle">Manage scheduled notifications</p>
<p class="page-subtitle">View scheduled notifications and rollover status</p>
</div>
<div class="placeholder-content">
<p>Notifications management coming soon...</p>
<div class="notifications-content">
<div class="actions-bar">
<button
class="action-button refresh"
@click="refreshNotifications"
:disabled="isLoading"
>
🔄 {{ isLoading ? 'Refreshing...' : 'Refresh' }}
</button>
</div>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div v-if="!isLoading && notificationStatus" class="status-cards">
<!-- Main Status Card -->
<div class="status-card main">
<div class="card-header">
<h2>📅 Scheduled Notification</h2>
<span class="status-badge" :class="statusClass">
{{ statusText }}
</span>
</div>
<div class="card-content">
<div v-if="nextNotificationTime" class="info-row">
<span class="label">Next Notification:</span>
<span class="value highlight">{{ formattedNextTime }}</span>
</div>
<div v-else class="info-row">
<span class="label">Next Notification:</span>
<span class="value">None scheduled</span>
</div>
<div v-if="timeUntilNext" class="info-row">
<span class="label">Time Until:</span>
<span class="value">{{ timeUntilNext }}</span>
</div>
<div class="info-row">
<span class="label">Pending Count:</span>
<span class="value">{{ notificationStatus.pending || 0 }}</span>
</div>
<div v-if="notificationStatus.lastNotificationTime" class="info-row">
<span class="label">Last Notification:</span>
<span class="value">{{ formattedLastTime }}</span>
</div>
</div>
</div>
<!-- Rollover Status Card -->
<div v-if="rolloverInfo" class="status-card rollover">
<div class="card-header">
<h2>🔄 Rollover Status</h2>
<span class="status-badge" :class="rolloverInfo.enabled ? 'success' : 'warning'">
{{ rolloverInfo.enabled ? 'Active' : 'Inactive' }}
</span>
</div>
<div class="card-content">
<div v-if="rolloverInfo.lastRolloverTime" class="info-row">
<span class="label">Last Rollover:</span>
<span class="value">{{ formattedRolloverTime }}</span>
</div>
<div v-else class="info-row">
<span class="label">Last Rollover:</span>
<span class="value">Never</span>
</div>
<div class="info-row">
<span class="label">Rollover Enabled:</span>
<span class="value">{{ rolloverInfo.enabled ? 'Yes' : 'No' }}</span>
</div>
</div>
</div>
<!-- Additional Info Card -->
<div class="status-card info">
<div class="card-header">
<h2> Additional Information</h2>
</div>
<div class="card-content">
<div class="info-row">
<span class="label">Notifications Enabled:</span>
<span class="value">{{ notificationStatus.isEnabled ? 'Yes' : 'No' }}</span>
</div>
<div class="info-row">
<span class="label">Is Scheduled:</span>
<span class="value">{{ notificationStatus.isScheduled ? 'Yes' : 'No' }}</span>
</div>
<div v-if="notificationStatus.error" class="info-row error">
<span class="label">Error:</span>
<span class="value">{{ notificationStatus.error }}</span>
</div>
</div>
</div>
</div>
<div v-else-if="!isLoading && !notificationStatus" class="empty-state">
<p>No notification status available. Try refreshing.</p>
</div>
<div v-if="isLoading" class="loading-state">
<p>Loading notification status...</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, toNative } from 'vue-facing-decorator'
import { Capacitor } from '@capacitor/core'
import router from '../router'
import { createTypedPlugin } from '../lib/typed-plugin'
import type { NotificationStatus } from '../lib/bridge'
interface RolloverInfo {
enabled: boolean
lastRolloverTime?: number
}
@Component
class NotificationsView extends Vue {
isLoading = false
errorMessage = ''
notificationStatus: NotificationStatus | null = null
rolloverInfo: RolloverInfo | null = null
async mounted() {
await this.refreshNotifications()
}
goBack() {
router.push('/')
}
async refreshNotifications() {
this.isLoading = true
this.errorMessage = ''
try {
const platform = Capacitor.getPlatform()
const isNative = platform !== 'web'
if (!isNative) {
this.errorMessage = 'Notification status is only available on native platforms (iOS/Android)'
this.isLoading = false
return
}
const typedPlugin = await createTypedPlugin()
if (!typedPlugin) {
throw new Error('Plugin not available')
}
// Get notification status
const status = await typedPlugin.getNotificationStatus()
this.notificationStatus = status
// Try to get rollover info (may not be in type definition but could be in response)
// The iOS plugin returns rolloverEnabled and lastRolloverTime
try {
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
const rawStatus = await DailyNotification.getNotificationStatus() as any
if (rawStatus.rolloverEnabled !== undefined || rawStatus.lastRolloverTime !== undefined) {
this.rolloverInfo = {
enabled: rawStatus.rolloverEnabled ?? false,
lastRolloverTime: rawStatus.lastRolloverTime
}
}
} catch (rolloverError) {
// Rollover info not available, that's okay
console.log('Rollover info not available:', rolloverError)
}
} catch (error: any) {
console.error('Failed to fetch notification status:', error)
this.errorMessage = error.message || 'Failed to load notification status'
} finally {
this.isLoading = false
}
}
get statusClass(): string {
if (!this.notificationStatus) return 'info'
if (this.notificationStatus.isScheduled && this.nextNotificationTime) return 'success'
if (this.notificationStatus.error) return 'error'
return 'warning'
}
get statusText(): string {
if (!this.notificationStatus) return 'Unknown'
if (this.notificationStatus.isScheduled && this.nextNotificationTime) return 'Scheduled'
if (this.notificationStatus.error) return 'Error'
return 'Not Scheduled'
}
get nextNotificationTime(): number | null {
if (!this.notificationStatus?.nextNotificationTime) return null
const time = this.notificationStatus.nextNotificationTime
return typeof time === 'number' ? time : null
}
get formattedNextTime(): string {
if (!this.nextNotificationTime) return 'N/A'
const date = new Date(this.nextNotificationTime)
return date.toLocaleString(undefined, {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
get timeUntilNext(): string | null {
if (!this.nextNotificationTime) return null
const now = Date.now()
const diff = this.nextNotificationTime - now
if (diff < 0) return 'Past due'
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
const parts: string[] = []
if (days > 0) parts.push(`${days} day${days !== 1 ? 's' : ''}`)
if (hours > 0) parts.push(`${hours} hour${hours !== 1 ? 's' : ''}`)
if (minutes > 0 || parts.length === 0) parts.push(`${minutes} minute${minutes !== 1 ? 's' : ''}`)
return parts.join(', ')
}
get formattedLastTime(): string {
if (!this.notificationStatus?.lastNotificationTime) return 'N/A'
const date = new Date(this.notificationStatus.lastNotificationTime)
return date.toLocaleString(undefined, {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
get formattedRolloverTime(): string {
if (!this.rolloverInfo?.lastRolloverTime) return 'N/A'
const date = new Date(this.rolloverInfo.lastRolloverTime)
return date.toLocaleString(undefined, {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
}
export default toNative(NotificationsView)
</script>
@@ -82,7 +333,162 @@ export default toNative(NotificationsView)
color: rgba(255, 255, 255, 0.8);
}
.placeholder-content {
.notifications-content {
padding: 0 16px;
}
.actions-bar {
margin-bottom: 24px;
display: flex;
justify-content: flex-end;
}
.action-button {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.action-button.refresh {
background: #1976d2;
color: white;
}
.action-button.refresh:hover:not(:disabled) {
background: #1565c0;
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error-message {
background: rgba(244, 67, 54, 0.2);
border: 1px solid rgba(244, 67, 54, 0.4);
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
color: #e57373;
font-size: 14px;
}
.status-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
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);
}
.status-card.main {
grid-column: 1 / -1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.card-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: white;
}
.status-badge {
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-badge.success {
background: rgba(76, 175, 80, 0.3);
color: #81c784;
border: 1px solid rgba(76, 175, 80, 0.5);
}
.status-badge.warning {
background: rgba(255, 152, 0, 0.3);
color: #ffb74d;
border: 1px solid rgba(255, 152, 0, 0.5);
}
.status-badge.error {
background: rgba(244, 67, 54, 0.3);
color: #e57373;
border: 1px solid rgba(244, 67, 54, 0.5);
}
.status-badge.info {
background: rgba(33, 150, 243, 0.3);
color: #64b5f6;
border: 1px solid rgba(33, 150, 243, 0.5);
}
.card-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.info-row:last-child {
border-bottom: none;
}
.info-row.error {
color: #e57373;
}
.info-row .label {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
flex: 0 0 40%;
}
.info-row .value {
font-size: 14px;
color: white;
text-align: right;
flex: 1;
word-break: break-word;
}
.info-row .value.highlight {
color: #81c784;
font-weight: 600;
font-size: 15px;
}
.empty-state,
.loading-state {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
@@ -97,11 +503,32 @@ export default toNative(NotificationsView)
.header-title-row {
gap: 8px;
}
.back-button {
min-width: 36px;
height: 36px;
font-size: 16px;
}
.status-cards {
grid-template-columns: 1fr;
}
.status-card.main {
grid-column: 1;
}
.info-row {
flex-direction: column;
gap: 8px;
}
.info-row .label {
flex: 1;
}
.info-row .value {
text-align: left;
}
}
</style>