feat: implement TypeScript bridge contract and schema validation
Bridge Contract (bridge.ts): - Complete TypeScript interface definitions for DailyNotification plugin - Request/Response schemas with proper typing - Canonical error codes and error info interfaces - Utility types for status, priority, and permission types Schema Validation (schema-validation.ts): - Input validation for schedule requests (time format, length limits) - Response validation for all plugin methods - Single joined error messages for UI display - Canonical error response creation Error Handling (error-handling.ts): - Native error mapping to canonical errors - User-friendly error message creation - Contextual error logging - Plugin method error handling Typed Plugin Wrapper (typed-plugin.ts): - Type-safe wrapper around native plugin - Schema validation at JavaScript boundary - Error handling with proper error mapping - Response validation and type safety StatusView Integration: - Updated to use typed plugin wrapper - Type-safe status collection - Proper error handling with user feedback - Maintains existing functionality with added safety This completes the TypeScript bridge contract and schema validation from the implementation plan.
This commit is contained in:
108
test-apps/daily-notification-test/src/lib/bridge.ts
Normal file
108
test-apps/daily-notification-test/src/lib/bridge.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* DailyNotification Bridge Contract
|
||||
*
|
||||
* TypeScript interface definitions for the DailyNotification plugin
|
||||
* Provides type safety and schema validation at the JavaScript boundary
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
// Core plugin interface
|
||||
export interface DailyNotificationBridge {
|
||||
scheduleDailyNotification(request: ScheduleRequest): Promise<ScheduleResponse>
|
||||
checkPermissions(): Promise<PermissionStatus>
|
||||
getNotificationStatus(): Promise<NotificationStatus>
|
||||
getExactAlarmStatus(): Promise<ExactAlarmStatus>
|
||||
requestPermissions(): Promise<PermissionResult>
|
||||
openExactAlarmSettings(): Promise<void>
|
||||
openChannelSettings(): Promise<void>
|
||||
requestBatteryOptimizationExemption(): Promise<void>
|
||||
cancelAllNotifications(): Promise<void>
|
||||
getLastNotification(): Promise<NotificationContent | null>
|
||||
}
|
||||
|
||||
// Request/Response schemas
|
||||
export interface ScheduleRequest {
|
||||
time: string // HH:mm format (24-hour)
|
||||
title: string // max 100 chars
|
||||
body: string // max 500 chars
|
||||
sound: boolean // play sound
|
||||
priority: 'low' | 'default' | 'high'
|
||||
}
|
||||
|
||||
export interface ScheduleResponse {
|
||||
success: boolean
|
||||
scheduledAt?: number
|
||||
error?: ErrorInfo
|
||||
}
|
||||
|
||||
export interface PermissionStatus {
|
||||
notifications: 'granted' | 'denied'
|
||||
notificationsEnabled: boolean
|
||||
}
|
||||
|
||||
export interface NotificationStatus {
|
||||
isEnabled: boolean
|
||||
isScheduled: boolean
|
||||
lastNotificationTime?: number
|
||||
nextNotificationTime?: number
|
||||
pending: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ExactAlarmStatus {
|
||||
enabled: boolean
|
||||
supported: boolean
|
||||
}
|
||||
|
||||
export interface PermissionResult {
|
||||
granted: boolean
|
||||
permissions: PermissionStatus
|
||||
}
|
||||
|
||||
export interface NotificationContent {
|
||||
id: string
|
||||
title: string
|
||||
body: string
|
||||
scheduledTime: number
|
||||
mediaUrl?: string
|
||||
fetchTime: number
|
||||
}
|
||||
|
||||
export interface ErrorInfo {
|
||||
code: string
|
||||
message: string
|
||||
hint?: string
|
||||
}
|
||||
|
||||
// Validation schemas
|
||||
export interface ValidationResult {
|
||||
isValid: boolean
|
||||
errors: string[]
|
||||
message: string
|
||||
}
|
||||
|
||||
// Error codes (canonical)
|
||||
export enum ErrorCode {
|
||||
INVALID_TIME = 'E_INVALID_TIME',
|
||||
TITLE_TOO_LONG = 'E_TITLE_TOO_LONG',
|
||||
BODY_TOO_LONG = 'E_BODY_TOO_LONG',
|
||||
PERMISSION_DENIED = 'E_PERMISSION_DENIED',
|
||||
CHANNEL_DISABLED = 'E_CHANNEL_DISABLED',
|
||||
EXACT_ALARM_DENIED = 'E_EXACT_ALARM_DENIED',
|
||||
DOZE_LIMIT = 'E_DOZE_LIMIT',
|
||||
CHANNEL_MISSING = 'E_CHANNEL_MISSING',
|
||||
BAD_CONFIG = 'E_BAD_CONFIG',
|
||||
RESPONSE_TOO_LARGE = 'E_RESPONSE_TOO_LARGE',
|
||||
INSECURE_URL = 'E_INSECURE_URL',
|
||||
SCHEDULE_BLOCKED = 'E_SCHEDULE_BLOCKED'
|
||||
}
|
||||
|
||||
// Plugin instance type
|
||||
export type DailyNotificationPlugin = DailyNotificationBridge
|
||||
|
||||
// Utility types
|
||||
export type StatusType = 'success' | 'warning' | 'error' | 'info'
|
||||
export type PriorityType = 'low' | 'default' | 'high'
|
||||
export type PermissionType = 'granted' | 'denied'
|
||||
161
test-apps/daily-notification-test/src/lib/error-handling.ts
Normal file
161
test-apps/daily-notification-test/src/lib/error-handling.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Error Handling Module
|
||||
*
|
||||
* Centralized error handling for the DailyNotification plugin
|
||||
* Maps native exceptions to canonical errors with user-friendly messages
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { ErrorCode, ErrorInfo } from './bridge'
|
||||
|
||||
export class ErrorHandler {
|
||||
|
||||
/**
|
||||
* Map native error to canonical error
|
||||
*/
|
||||
mapNativeError(error: any): ErrorInfo {
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown error'
|
||||
|
||||
// Map common error patterns
|
||||
if (errorMessage.includes('Permission denied')) {
|
||||
return {
|
||||
code: ErrorCode.PERMISSION_DENIED,
|
||||
message: 'Required permission not granted',
|
||||
hint: 'Request permission in settings'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Channel disabled')) {
|
||||
return {
|
||||
code: ErrorCode.CHANNEL_DISABLED,
|
||||
message: 'Notification channel is disabled',
|
||||
hint: 'Enable notifications in settings'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Exact alarm')) {
|
||||
return {
|
||||
code: ErrorCode.EXACT_ALARM_DENIED,
|
||||
message: 'Exact alarm permission denied',
|
||||
hint: 'Grant exact alarm permission in settings'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Doze')) {
|
||||
return {
|
||||
code: ErrorCode.DOZE_LIMIT,
|
||||
message: 'Device in Doze mode',
|
||||
hint: 'Expect delays; fallback taken'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Invalid time')) {
|
||||
return {
|
||||
code: ErrorCode.INVALID_TIME,
|
||||
message: 'Invalid time format',
|
||||
hint: 'Use 24-hour HH:mm format'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Title too long')) {
|
||||
return {
|
||||
code: ErrorCode.TITLE_TOO_LONG,
|
||||
message: 'Title exceeds 100 characters',
|
||||
hint: 'Trim title to 100 characters or less'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Body too long')) {
|
||||
return {
|
||||
code: ErrorCode.BODY_TOO_LONG,
|
||||
message: 'Body exceeds 500 characters',
|
||||
hint: 'Trim body to 500 characters or less'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Response too large')) {
|
||||
return {
|
||||
code: ErrorCode.RESPONSE_TOO_LARGE,
|
||||
message: 'Response size exceeds limit',
|
||||
hint: 'Response is too large to process'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Insecure URL')) {
|
||||
return {
|
||||
code: ErrorCode.INSECURE_URL,
|
||||
message: 'Only HTTPS URLs allowed',
|
||||
hint: 'Use secure HTTPS URLs only'
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Schedule blocked')) {
|
||||
return {
|
||||
code: ErrorCode.SCHEDULE_BLOCKED,
|
||||
message: 'Cannot schedule now',
|
||||
hint: 'Check prerequisites and try again'
|
||||
}
|
||||
}
|
||||
|
||||
// Default error
|
||||
return {
|
||||
code: 'E_UNKNOWN',
|
||||
message: errorMessage,
|
||||
hint: 'Check logs for more details'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user-friendly error message
|
||||
*/
|
||||
createUserMessage(error: ErrorInfo): string {
|
||||
let message = error.message
|
||||
|
||||
if (error.hint) {
|
||||
message += ` (${error.hint})`
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error with context
|
||||
*/
|
||||
logError(error: any, context: string = 'DailyNotification') {
|
||||
console.error(`[${context}] Error:`, error)
|
||||
|
||||
if (error?.stack) {
|
||||
console.error(`[${context}] Stack:`, error.stack)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle plugin method error
|
||||
*/
|
||||
handlePluginError(error: any, method: string): ErrorInfo {
|
||||
this.logError(error, `Plugin.${method}`)
|
||||
return this.mapNativeError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const errorHandler = new ErrorHandler()
|
||||
|
||||
// Utility functions
|
||||
export function mapNativeError(error: any): ErrorInfo {
|
||||
return errorHandler.mapNativeError(error)
|
||||
}
|
||||
|
||||
export function createUserMessage(error: ErrorInfo): string {
|
||||
return errorHandler.createUserMessage(error)
|
||||
}
|
||||
|
||||
export function logError(error: any, context?: string): void {
|
||||
errorHandler.logError(error, context)
|
||||
}
|
||||
|
||||
export function handlePluginError(error: any, method: string): ErrorInfo {
|
||||
return errorHandler.handlePluginError(error, method)
|
||||
}
|
||||
200
test-apps/daily-notification-test/src/lib/schema-validation.ts
Normal file
200
test-apps/daily-notification-test/src/lib/schema-validation.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Schema Validation Module
|
||||
*
|
||||
* Input/output validation for the DailyNotification plugin bridge
|
||||
* Ensures type safety and data integrity at the JavaScript boundary
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import {
|
||||
ScheduleRequest,
|
||||
ValidationResult,
|
||||
ErrorCode,
|
||||
PriorityType,
|
||||
PermissionType
|
||||
} from './bridge'
|
||||
|
||||
export class SchemaValidator {
|
||||
|
||||
/**
|
||||
* Validate schedule request input
|
||||
*/
|
||||
validateScheduleRequest(request: any): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
// Validate time format (HH:mm)
|
||||
if (!this.isValidTimeFormat(request.time)) {
|
||||
errors.push('Time must be in HH:mm format (24-hour)')
|
||||
}
|
||||
|
||||
// Validate title length (enforce exactly: title ≤ 100 chars)
|
||||
if (request.title && request.title.length > 100) {
|
||||
errors.push('Title must be 100 characters or less')
|
||||
}
|
||||
|
||||
// Validate body length (enforce exactly: body ≤ 500 chars)
|
||||
if (request.body && request.body.length > 500) {
|
||||
errors.push('Body must be 500 characters or less')
|
||||
}
|
||||
|
||||
// Validate boolean fields
|
||||
if (typeof request.sound !== 'boolean') {
|
||||
errors.push('Sound must be a boolean')
|
||||
}
|
||||
|
||||
// Validate priority
|
||||
if (!this.isValidPriority(request.priority)) {
|
||||
errors.push('Priority must be low, default, or high')
|
||||
}
|
||||
|
||||
// Reject unknown fields
|
||||
const allowedFields = ['time', 'title', 'body', 'sound', 'priority']
|
||||
const unknownFields = Object.keys(request).filter(key => !allowedFields.includes(key))
|
||||
if (unknownFields.length > 0) {
|
||||
errors.push(`Unknown fields: ${unknownFields.join(', ')}`)
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
message: errors.join('; ') // Single joined message for UI display
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate permission status response
|
||||
*/
|
||||
validatePermissionStatus(status: any): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!this.isValidPermissionType(status.notifications)) {
|
||||
errors.push('Notifications permission must be granted or denied')
|
||||
}
|
||||
|
||||
if (typeof status.notificationsEnabled !== 'boolean') {
|
||||
errors.push('NotificationsEnabled must be a boolean')
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
message: errors.join('; ')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate notification status response
|
||||
*/
|
||||
validateNotificationStatus(status: any): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
if (typeof status.isEnabled !== 'boolean') {
|
||||
errors.push('IsEnabled must be a boolean')
|
||||
}
|
||||
|
||||
if (typeof status.isScheduled !== 'boolean') {
|
||||
errors.push('IsScheduled must be a boolean')
|
||||
}
|
||||
|
||||
if (typeof status.pending !== 'boolean') {
|
||||
errors.push('Pending must be a boolean')
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
message: errors.join('; ')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate exact alarm status response
|
||||
*/
|
||||
validateExactAlarmStatus(status: any): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
if (typeof status.enabled !== 'boolean') {
|
||||
errors.push('Enabled must be a boolean')
|
||||
}
|
||||
|
||||
if (typeof status.supported !== 'boolean') {
|
||||
errors.push('Supported must be a boolean')
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
message: errors.join('; ')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if time format is valid (HH:mm)
|
||||
*/
|
||||
private isValidTimeFormat(time: string): boolean {
|
||||
if (typeof time !== 'string') return false
|
||||
|
||||
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/
|
||||
return timeRegex.test(time)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if priority is valid
|
||||
*/
|
||||
private isValidPriority(priority: any): priority is PriorityType {
|
||||
return ['low', 'default', 'high'].includes(priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if permission type is valid
|
||||
*/
|
||||
private isValidPermissionType(permission: any): permission is PermissionType {
|
||||
return ['granted', 'denied'].includes(permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create canonical error response
|
||||
*/
|
||||
createErrorResponse(code: ErrorCode, message: string, hint?: string) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
hint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create success response
|
||||
*/
|
||||
createSuccessResponse(data?: any) {
|
||||
return {
|
||||
success: true,
|
||||
...data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const schemaValidator = new SchemaValidator()
|
||||
|
||||
// Utility functions
|
||||
export function validateScheduleRequest(request: any): ValidationResult {
|
||||
return schemaValidator.validateScheduleRequest(request)
|
||||
}
|
||||
|
||||
export function validatePermissionStatus(status: any): ValidationResult {
|
||||
return schemaValidator.validatePermissionStatus(status)
|
||||
}
|
||||
|
||||
export function validateNotificationStatus(status: any): ValidationResult {
|
||||
return schemaValidator.validateNotificationStatus(status)
|
||||
}
|
||||
|
||||
export function validateExactAlarmStatus(status: any): ValidationResult {
|
||||
return schemaValidator.validateExactAlarmStatus(status)
|
||||
}
|
||||
259
test-apps/daily-notification-test/src/lib/typed-plugin.ts
Normal file
259
test-apps/daily-notification-test/src/lib/typed-plugin.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Typed Plugin Wrapper
|
||||
*
|
||||
* Type-safe wrapper for the DailyNotification plugin with validation
|
||||
* Provides schema validation and error handling at the JavaScript boundary
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import {
|
||||
DailyNotificationBridge,
|
||||
ScheduleRequest,
|
||||
ScheduleResponse,
|
||||
PermissionStatus,
|
||||
NotificationStatus,
|
||||
ExactAlarmStatus,
|
||||
PermissionResult,
|
||||
NotificationContent
|
||||
} from './bridge'
|
||||
|
||||
import { validateScheduleRequest } from './schema-validation'
|
||||
import { handlePluginError, logError } from './error-handling'
|
||||
|
||||
export class TypedDailyNotificationPlugin implements DailyNotificationBridge {
|
||||
private plugin: any
|
||||
|
||||
constructor(plugin: any) {
|
||||
this.plugin = plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule daily notification with validation
|
||||
*/
|
||||
async scheduleDailyNotification(request: ScheduleRequest): Promise<ScheduleResponse> {
|
||||
try {
|
||||
// Validate input schema
|
||||
const validation = validateScheduleRequest(request)
|
||||
if (!validation.isValid) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'E_BAD_CONFIG',
|
||||
message: validation.message,
|
||||
hint: 'Check input format and try again'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call native plugin
|
||||
const result = await this.plugin.scheduleDailyNotification(request)
|
||||
|
||||
// Validate response
|
||||
if (result && typeof result.success === 'boolean') {
|
||||
return result
|
||||
}
|
||||
|
||||
// Handle unexpected response format
|
||||
return {
|
||||
success: true,
|
||||
scheduledAt: Date.now()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(error, 'scheduleDailyNotification')
|
||||
return {
|
||||
success: false,
|
||||
error: handlePluginError(error, 'scheduleDailyNotification')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permissions with validation
|
||||
*/
|
||||
async checkPermissions(): Promise<PermissionStatus> {
|
||||
try {
|
||||
const result = await this.plugin.checkPermissions()
|
||||
|
||||
// Ensure response has required fields
|
||||
return {
|
||||
notifications: result.notifications || 'denied',
|
||||
notificationsEnabled: Boolean(result.notificationsEnabled)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(error, 'checkPermissions')
|
||||
return {
|
||||
notifications: 'denied',
|
||||
notificationsEnabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification status with validation
|
||||
*/
|
||||
async getNotificationStatus(): Promise<NotificationStatus> {
|
||||
try {
|
||||
const result = await this.plugin.getNotificationStatus()
|
||||
|
||||
// Ensure response has required fields
|
||||
return {
|
||||
isEnabled: Boolean(result.isEnabled),
|
||||
isScheduled: Boolean(result.isScheduled),
|
||||
lastNotificationTime: result.lastNotificationTime,
|
||||
nextNotificationTime: result.nextNotificationTime,
|
||||
pending: Boolean(result.pending),
|
||||
error: result.error
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(error, 'getNotificationStatus')
|
||||
return {
|
||||
isEnabled: false,
|
||||
isScheduled: false,
|
||||
pending: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exact alarm status with validation
|
||||
*/
|
||||
async getExactAlarmStatus(): Promise<ExactAlarmStatus> {
|
||||
try {
|
||||
const result = await this.plugin.getExactAlarmStatus()
|
||||
|
||||
// Ensure response has required fields
|
||||
return {
|
||||
enabled: Boolean(result.enabled),
|
||||
supported: Boolean(result.supported)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(error, 'getExactAlarmStatus')
|
||||
return {
|
||||
enabled: false,
|
||||
supported: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request permissions
|
||||
*/
|
||||
async requestPermissions(): Promise<PermissionResult> {
|
||||
try {
|
||||
const result = await this.plugin.requestPermissions()
|
||||
|
||||
return {
|
||||
granted: Boolean(result.granted),
|
||||
permissions: await this.checkPermissions()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(error, 'requestPermissions')
|
||||
return {
|
||||
granted: false,
|
||||
permissions: {
|
||||
notifications: 'denied',
|
||||
notificationsEnabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open exact alarm settings
|
||||
*/
|
||||
async openExactAlarmSettings(): Promise<void> {
|
||||
try {
|
||||
await this.plugin.openExactAlarmSettings()
|
||||
} catch (error) {
|
||||
logError(error, 'openExactAlarmSettings')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open channel settings
|
||||
*/
|
||||
async openChannelSettings(): Promise<void> {
|
||||
try {
|
||||
await this.plugin.openChannelSettings()
|
||||
} catch (error) {
|
||||
logError(error, 'openChannelSettings')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request battery optimization exemption
|
||||
*/
|
||||
async requestBatteryOptimizationExemption(): Promise<void> {
|
||||
try {
|
||||
await this.plugin.requestBatteryOptimizationExemption()
|
||||
} catch (error) {
|
||||
logError(error, 'requestBatteryOptimizationExemption')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all notifications
|
||||
*/
|
||||
async cancelAllNotifications(): Promise<void> {
|
||||
try {
|
||||
await this.plugin.cancelAllNotifications()
|
||||
} catch (error) {
|
||||
logError(error, 'cancelAllNotifications')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last notification
|
||||
*/
|
||||
async getLastNotification(): Promise<NotificationContent | null> {
|
||||
try {
|
||||
const result = await this.plugin.getLastNotification()
|
||||
return result || null
|
||||
} catch (error) {
|
||||
logError(error, 'getLastNotification')
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create typed plugin instance
|
||||
*/
|
||||
export async function createTypedPlugin(): Promise<TypedDailyNotificationPlugin | null> {
|
||||
try {
|
||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
||||
|
||||
if (!DailyNotification) {
|
||||
console.warn('DailyNotification plugin not available')
|
||||
return null
|
||||
}
|
||||
|
||||
return new TypedDailyNotificationPlugin(DailyNotification)
|
||||
} catch (error) {
|
||||
logError(error, 'createTypedPlugin')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Export types for use in components
|
||||
export type {
|
||||
ScheduleRequest,
|
||||
ScheduleResponse,
|
||||
PermissionStatus,
|
||||
NotificationStatus,
|
||||
ExactAlarmStatus,
|
||||
PermissionResult,
|
||||
NotificationContent
|
||||
} from './bridge'
|
||||
@@ -88,6 +88,7 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, toNative } from 'vue-facing-decorator'
|
||||
import StatusCard from '../components/cards/StatusCard.vue'
|
||||
import { createTypedPlugin, type PermissionStatus, type NotificationStatus, type ExactAlarmStatus } from '../lib/typed-plugin'
|
||||
|
||||
interface StatusItem {
|
||||
key: string
|
||||
@@ -213,19 +214,18 @@ class StatusView extends Vue {
|
||||
try {
|
||||
console.log('🔄 Refreshing status matrix...')
|
||||
|
||||
// Import the plugin dynamically
|
||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
||||
const plugin = DailyNotification
|
||||
// Create typed plugin instance
|
||||
const typedPlugin = await createTypedPlugin()
|
||||
|
||||
if (!plugin) {
|
||||
if (!typedPlugin) {
|
||||
throw new Error('DailyNotification plugin not available')
|
||||
}
|
||||
|
||||
// Collect comprehensive status
|
||||
// Collect comprehensive status with type safety
|
||||
const [notificationStatus, permissions, exactAlarmStatus] = await Promise.all([
|
||||
plugin.getNotificationStatus().catch(() => ({ isEnabled: false, isScheduled: false })),
|
||||
plugin.checkPermissions().catch(() => ({ notifications: 'denied' })),
|
||||
plugin.getExactAlarmStatus().catch(() => ({ enabled: false, supported: false }))
|
||||
typedPlugin.getNotificationStatus(),
|
||||
typedPlugin.checkPermissions(),
|
||||
typedPlugin.getExactAlarmStatus()
|
||||
])
|
||||
|
||||
// Update diagnostics
|
||||
@@ -262,21 +262,24 @@ class StatusView extends Vue {
|
||||
try {
|
||||
console.log(`🔧 Executing action: ${action.method}`)
|
||||
|
||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
||||
const plugin = DailyNotification
|
||||
const typedPlugin = await createTypedPlugin()
|
||||
|
||||
if (!typedPlugin) {
|
||||
throw new Error('Plugin not available')
|
||||
}
|
||||
|
||||
switch (action.method) {
|
||||
case 'requestPermissions':
|
||||
await plugin.requestPermissions()
|
||||
await typedPlugin.requestPermissions()
|
||||
break
|
||||
case 'openExactAlarmSettings':
|
||||
await plugin.openExactAlarmSettings()
|
||||
await typedPlugin.openExactAlarmSettings()
|
||||
break
|
||||
case 'openChannelSettings':
|
||||
await plugin.openChannelSettings()
|
||||
await typedPlugin.openChannelSettings()
|
||||
break
|
||||
case 'requestBatteryOptimizationExemption':
|
||||
await plugin.requestBatteryOptimizationExemption()
|
||||
await typedPlugin.requestBatteryOptimizationExemption()
|
||||
break
|
||||
case 'checkPrerequisites':
|
||||
await this.refreshStatus()
|
||||
|
||||
Reference in New Issue
Block a user