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:
@@ -7,23 +7,274 @@
|
|||||||
</button>
|
</button>
|
||||||
<h1 class="page-title">🔔 Notifications</h1>
|
<h1 class="page-title">🔔 Notifications</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="page-subtitle">Manage scheduled notifications</p>
|
<p class="page-subtitle">View scheduled notifications and rollover status</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, toNative } from 'vue-facing-decorator'
|
import { Vue, Component, toNative } from 'vue-facing-decorator'
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
import { createTypedPlugin } from '../lib/typed-plugin'
|
||||||
|
import type { NotificationStatus } from '../lib/bridge'
|
||||||
|
|
||||||
|
interface RolloverInfo {
|
||||||
|
enabled: boolean
|
||||||
|
lastRolloverTime?: number
|
||||||
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class NotificationsView extends Vue {
|
class NotificationsView extends Vue {
|
||||||
|
isLoading = false
|
||||||
|
errorMessage = ''
|
||||||
|
notificationStatus: NotificationStatus | null = null
|
||||||
|
rolloverInfo: RolloverInfo | null = null
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.refreshNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
router.push('/')
|
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)
|
export default toNative(NotificationsView)
|
||||||
</script>
|
</script>
|
||||||
@@ -82,7 +333,162 @@ export default toNative(NotificationsView)
|
|||||||
color: rgba(255, 255, 255, 0.8);
|
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);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -97,11 +503,32 @@ export default toNative(NotificationsView)
|
|||||||
.header-title-row {
|
.header-title-row {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
font-size: 16px;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user