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">
|
<script lang="ts">
|
||||||
import { Vue, Component, toNative } from 'vue-facing-decorator'
|
import { Vue, Component, toNative } from 'vue-facing-decorator'
|
||||||
import StatusCard from '../components/cards/StatusCard.vue'
|
import StatusCard from '../components/cards/StatusCard.vue'
|
||||||
|
import { createTypedPlugin, type PermissionStatus, type NotificationStatus, type ExactAlarmStatus } from '../lib/typed-plugin'
|
||||||
|
|
||||||
interface StatusItem {
|
interface StatusItem {
|
||||||
key: string
|
key: string
|
||||||
@@ -213,19 +214,18 @@ class StatusView extends Vue {
|
|||||||
try {
|
try {
|
||||||
console.log('🔄 Refreshing status matrix...')
|
console.log('🔄 Refreshing status matrix...')
|
||||||
|
|
||||||
// Import the plugin dynamically
|
// Create typed plugin instance
|
||||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
const typedPlugin = await createTypedPlugin()
|
||||||
const plugin = DailyNotification
|
|
||||||
|
|
||||||
if (!plugin) {
|
if (!typedPlugin) {
|
||||||
throw new Error('DailyNotification plugin not available')
|
throw new Error('DailyNotification plugin not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect comprehensive status
|
// Collect comprehensive status with type safety
|
||||||
const [notificationStatus, permissions, exactAlarmStatus] = await Promise.all([
|
const [notificationStatus, permissions, exactAlarmStatus] = await Promise.all([
|
||||||
plugin.getNotificationStatus().catch(() => ({ isEnabled: false, isScheduled: false })),
|
typedPlugin.getNotificationStatus(),
|
||||||
plugin.checkPermissions().catch(() => ({ notifications: 'denied' })),
|
typedPlugin.checkPermissions(),
|
||||||
plugin.getExactAlarmStatus().catch(() => ({ enabled: false, supported: false }))
|
typedPlugin.getExactAlarmStatus()
|
||||||
])
|
])
|
||||||
|
|
||||||
// Update diagnostics
|
// Update diagnostics
|
||||||
@@ -262,21 +262,24 @@ class StatusView extends Vue {
|
|||||||
try {
|
try {
|
||||||
console.log(`🔧 Executing action: ${action.method}`)
|
console.log(`🔧 Executing action: ${action.method}`)
|
||||||
|
|
||||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
const typedPlugin = await createTypedPlugin()
|
||||||
const plugin = DailyNotification
|
|
||||||
|
if (!typedPlugin) {
|
||||||
|
throw new Error('Plugin not available')
|
||||||
|
}
|
||||||
|
|
||||||
switch (action.method) {
|
switch (action.method) {
|
||||||
case 'requestPermissions':
|
case 'requestPermissions':
|
||||||
await plugin.requestPermissions()
|
await typedPlugin.requestPermissions()
|
||||||
break
|
break
|
||||||
case 'openExactAlarmSettings':
|
case 'openExactAlarmSettings':
|
||||||
await plugin.openExactAlarmSettings()
|
await typedPlugin.openExactAlarmSettings()
|
||||||
break
|
break
|
||||||
case 'openChannelSettings':
|
case 'openChannelSettings':
|
||||||
await plugin.openChannelSettings()
|
await typedPlugin.openChannelSettings()
|
||||||
break
|
break
|
||||||
case 'requestBatteryOptimizationExemption':
|
case 'requestBatteryOptimizationExemption':
|
||||||
await plugin.requestBatteryOptimizationExemption()
|
await typedPlugin.requestBatteryOptimizationExemption()
|
||||||
break
|
break
|
||||||
case 'checkPrerequisites':
|
case 'checkPrerequisites':
|
||||||
await this.refreshStatus()
|
await this.refreshStatus()
|
||||||
|
|||||||
Reference in New Issue
Block a user