fix(ios): iOS 13.0 compatibility and test app UI unification

Fixed iOS 13.0 compatibility issue in test harness by replacing Logger
(iOS 14+) with os_log (iOS 13+). Fixed build script to correctly detect
and sync Capacitor config from App subdirectory. Unified both Android
and iOS test app UIs to use www/index.html as the canonical source.

Changes:
- DailyNotificationBackgroundTaskTestHarness: Replace Logger with os_log
  for iOS 13.0 deployment target compatibility
- build-ios-test-app.sh: Fix Capacitor sync path detection to check
  both current directory and App/ subdirectory for config files
- test-apps: Update both Android and iOS test apps to use www/index.html
  as the canonical UI source for consistency

This ensures the plugin builds on iOS 13.0+ and both test apps provide
the same testing experience across platforms.
This commit is contained in:
Matthew
2025-11-18 21:25:14 -08:00
parent b74d38056f
commit 9f26588331
4 changed files with 1152 additions and 901 deletions

View File

@@ -46,16 +46,16 @@ class DailyNotificationBackgroundTaskTestHarness {
// MARK: - Structured Logging
/// Swift Logger categories for structured logging
static let pluginLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "plugin")
static let fetchLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "fetch")
static let schedulerLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "scheduler")
static let storageLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "storage")
/// OSLog categories for structured logging (iOS 13.0+ compatible)
private static let pluginLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "plugin")
private static let fetchLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "fetch")
private static let schedulerLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "scheduler")
private static let storageLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "storage")
/// Log telemetry snapshot for validation
static func logTelemetrySnapshot(prefix: String = "DNP-") {
// Phase 2: Capture telemetry counters from structured logs
fetchLogger.info("Telemetry snapshot: \(prefix)prefetch_scheduled_total, \(prefix)prefetch_executed_total, \(prefix)prefetch_success_total")
os_log("[DNP-FETCH] Telemetry snapshot: %{public}@prefetch_scheduled_total, %{public}@prefetch_executed_total, %{public}@prefetch_success_total", log: fetchLog, type: .info, prefix, prefix, prefix)
}
// MARK: - Registration
@@ -106,16 +106,16 @@ class DailyNotificationBackgroundTaskTestHarness {
do {
try BGTaskScheduler.shared.submit(request)
fetchLogger.info("[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=\(String(describing: request.earliestBeginDate)))")
os_log("[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=%{public}@)", log: fetchLog, type: .info, String(describing: request.earliestBeginDate))
return true
} catch {
// Handle simulator limitation (Code=1 is expected on simulator)
if let nsError = error as NSError?, nsError.domain == "BGTaskSchedulerErrorDomain", nsError.code == 1 {
fetchLogger.warning("[DNP-FETCH] BGTask scheduling failed on simulator (expected): Code=1 notPermitted")
os_log("[DNP-FETCH] BGTask scheduling failed on simulator (expected): Code=1 notPermitted", log: fetchLog, type: .default)
print("[DNP-FETCH] NOTE: BGTaskScheduler doesn't work on simulator - this is expected. Use Xcode → Debug → Simulate Background Fetch for testing.")
return false
}
fetchLogger.error("[DNP-FETCH] Failed to schedule BGAppRefreshTask: \(error.localizedDescription)")
os_log("[DNP-FETCH] Failed to schedule BGAppRefreshTask: %{public}@", log: fetchLog, type: .error, error.localizedDescription)
print("[DNP-FETCH] Failed to schedule BGAppRefreshTask: \(error)")
return false
}
@@ -130,7 +130,7 @@ class DailyNotificationBackgroundTaskTestHarness {
// In real implementation, check if this request matches the notificationId
// For now, cancel all prefetch tasks (Phase 1: single notification)
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: prefetchTaskIdentifier)
fetchLogger.info("[DNP-FETCH] Cancelled existing pending task for notificationId=\(notificationId)")
os_log("[DNP-FETCH] Cancelled existing pending task for notificationId=%{public}@", log: fetchLog, type: .info, notificationId)
}
}
}
@@ -149,7 +149,7 @@ class DailyNotificationBackgroundTaskTestHarness {
semaphore.wait()
if pendingCount > 1 {
fetchLogger.warning("[DNP-FETCH] WARNING: Multiple pending tasks detected (\(pendingCount)) - expected 1")
os_log("[DNP-FETCH] WARNING: Multiple pending tasks detected (%d) - expected 1", log: fetchLog, type: .default, pendingCount)
return false
}
@@ -203,7 +203,7 @@ class DailyNotificationBackgroundTaskTestHarness {
/// This is called by the system when the background task is launched.
/// Replace PrefetchOperation with your actual prefetch logic.
private static func handlePrefetchTask(task: BGAppRefreshTask) {
fetchLogger.info("[DNP-FETCH] BGTask handler invoked (task.identifier=\(task.identifier))")
os_log("[DNP-FETCH] BGTask handler invoked (task.identifier=%{public}@)", log: fetchLog, type: .info, task.identifier)
// STEP 1: Schedule the next task IMMEDIATELY (Apple best practice)
// This ensures continuity even if app is terminated shortly after
@@ -219,14 +219,14 @@ class DailyNotificationBackgroundTaskTestHarness {
// STEP 4: Set expiration handler (called if iOS terminates task early, ~30 seconds)
task.expirationHandler = {
fetchLogger.warning("[DNP-FETCH] Task expired - cancelling operations")
os_log("[DNP-FETCH] Task expired - cancelling operations", log: fetchLog, type: .default)
operation.cancel()
// Ensure task is marked complete even on expiration
if !taskCompleted {
taskCompleted = true
task.setTaskCompleted(success: false)
fetchLogger.info("[DNP-FETCH] Task marked complete (success=false) due to expiration")
os_log("[DNP-FETCH] Task marked complete (success=false) due to expiration", log: fetchLog, type: .info)
}
}
@@ -239,9 +239,9 @@ class DailyNotificationBackgroundTaskTestHarness {
if !taskCompleted {
taskCompleted = true
task.setTaskCompleted(success: success)
fetchLogger.info("[DNP-FETCH] Task marked complete (success=\(success))")
os_log("[DNP-FETCH] Task marked complete (success=%{public}@)", log: fetchLog, type: .info, success ? "true" : "false")
} else {
fetchLogger.warning("[DNP-FETCH] WARNING: Attempted to complete task twice")
os_log("[DNP-FETCH] WARNING: Attempted to complete task twice", log: fetchLog, type: .default)
}
}
@@ -262,12 +262,12 @@ class DailyNotificationBackgroundTaskTestHarness {
class PrefetchOperation: Operation {
var isFailed = false
private let fetchLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "fetch")
private static let fetchLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "fetch")
override func main() {
if isCancelled { return }
fetchLogger.info("[DNP-FETCH] PrefetchOperation: starting fetch...")
os_log("[DNP-FETCH] PrefetchOperation: starting fetch...", log: Self.fetchLog, type: .info)
// Simulate some work
// In real implementation, this would be:
@@ -287,11 +287,11 @@ class PrefetchOperation: Operation {
let success = true // Replace with actual fetch result
if success {
fetchLogger.info("[DNP-FETCH] PrefetchOperation: fetch success")
os_log("[DNP-FETCH] PrefetchOperation: fetch success", log: Self.fetchLog, type: .info)
// In real implementation: persist cache here before completion
} else {
isFailed = true
fetchLogger.error("[DNP-FETCH] PrefetchOperation: fetch failed")
os_log("[DNP-FETCH] PrefetchOperation: fetch failed", log: Self.fetchLog, type: .error)
}
}
}

View File

@@ -174,12 +174,28 @@ build_ios_test_app() {
fi
# Sync Capacitor if needed
if command -v npx &> /dev/null && [ -f "capacitor.config.ts" ] || [ -f "capacitor.config.json" ]; then
# Check for config in current directory or App subdirectory
CAP_CONFIG_DIR=""
if [ -f "capacitor.config.ts" ] || [ -f "capacitor.config.json" ]; then
CAP_CONFIG_DIR="."
elif [ -f "App/capacitor.config.json" ] || [ -f "App/capacitor.config.ts" ]; then
CAP_CONFIG_DIR="App"
fi
if command -v npx &> /dev/null && [ -n "$CAP_CONFIG_DIR" ]; then
log_step "Syncing Capacitor..."
# Run sync from directory containing config
if [ "$CAP_CONFIG_DIR" != "." ]; then
cd "$CAP_CONFIG_DIR" || exit 1
fi
if ! npx cap sync ios; then
log_error "Capacitor sync failed"
exit 1
fi
# Return to ios/App directory if we changed
if [ "$CAP_CONFIG_DIR" != "." ]; then
cd .. || exit 1
fi
log_info "Capacitor synced"
fi
fi

View File

@@ -17,417 +17,627 @@
color: white;
}
.container {
max-width: 600px;
max-width: 800px;
margin: 0 auto;
text-align: center;
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
}
.section {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
.section h2 {
margin-top: 0;
color: #ffd700;
}
.button {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 15px 30px;
margin: 10px;
border-radius: 25px;
padding: 12px 24px;
margin: 8px;
border-radius: 20px;
cursor: pointer;
font-size: 16px;
font-size: 14px;
transition: all 0.3s ease;
display: inline-block;
}
.button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.status {
margin-top: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
margin-top: 15px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
.success { color: #4CAF50; }
.error { color: #f44336; }
.warning { color: #ff9800; }
.info { color: #2196F3; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 15px 0;
}
.input-group {
margin: 10px 0;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.input-group input, .input-group select {
width: 100%;
padding: 8px;
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
color: white;
}
.input-group input::placeholder {
color: rgba(255, 255, 255, 0.7);
}
</style>
</head>
<body>
<div class="container">
<div id="statusCard" class="status" style="margin-bottom: 20px; font-size: 14px;">
<strong>Plugin Status</strong><br>
<div style="margin-top: 10px;">
⚙️ Plugin Settings: <span id="configStatus">Not configured</span><br>
🔌 Native Fetcher: <span id="fetcherStatus">Not configured</span><br>
🔔 Notifications: <span id="notificationPermStatus">Checking...</span><br>
⏰ Exact Alarms: <span id="exactAlarmPermStatus">Checking...</span><br>
📢 Channel: <span id="channelStatus">Checking...</span><br>
<div id="pluginStatusContent" style="margin-top: 8px;">
Loading plugin status...
</div>
<h1>🔔 DailyNotification Plugin Test</h1>
<!-- Plugin Status Section -->
<div class="section">
<h2>📊 Plugin Status</h2>
<div class="grid">
<button class="button" onclick="checkPluginAvailability()">Check Availability</button>
<button class="button" onclick="getNotificationStatus()">Get Status</button>
<button class="button" onclick="checkPermissions()">Check Permissions</button>
<button class="button" onclick="getBatteryStatus()">Battery Status</button>
</div>
<div id="status" class="status">Ready to test...</div>
</div>
<!-- Permission Management Section -->
<div class="section">
<h2>🔐 Permission Management</h2>
<div class="grid">
<button class="button" onclick="requestPermissions()">Request Permissions</button>
<button class="button" onclick="requestExactAlarmPermission()">Request Exact Alarm</button>
<button class="button" onclick="openExactAlarmSettings()">Open Settings</button>
<button class="button" onclick="requestBatteryOptimizationExemption()">Battery Exemption</button>
</div>
</div>
<button class="button" onclick="configurePlugin()">Configure Plugin</button>
<button class="button" onclick="requestPermissions()">Request Permissions</button>
<button class="button" onclick="testNotification()">Test Notification</button>
<button class="button" onclick="checkComprehensiveStatus()">Full System Status</button>
<!-- Notification Scheduling Section -->
<div class="section">
<h2>⏰ Notification Scheduling</h2>
<div class="input-group">
<label for="notificationUrl">Content URL:</label>
<input type="text" id="notificationUrl" placeholder="https://api.example.com/daily-content" value="https://api.example.com/daily-content">
</div>
<div class="input-group">
<label for="notificationTime">Schedule Time:</label>
<input type="time" id="notificationTime" value="09:00">
</div>
<div class="input-group">
<label for="notificationTitle">Title:</label>
<input type="text" id="notificationTitle" placeholder="Daily Notification" value="Daily Notification">
</div>
<div class="input-group">
<label for="notificationBody">Body:</label>
<input type="text" id="notificationBody" placeholder="Your daily content is ready!" value="Your daily content is ready!">
</div>
<div class="grid">
<button class="button" onclick="scheduleNotification()">Schedule Notification</button>
<button class="button" onclick="cancelAllNotifications()">Cancel All</button>
<button class="button" onclick="getLastNotification()">Get Last</button>
</div>
</div>
<div id="status" class="status">
Ready to test...
<!-- Configuration Section -->
<div class="section">
<h2>⚙️ Plugin Configuration</h2>
<div class="input-group">
<label for="configUrl">Fetch URL:</label>
<input type="text" id="configUrl" placeholder="https://api.example.com/content" value="https://api.example.com/content">
</div>
<div class="input-group">
<label for="configTime">Schedule Time:</label>
<input type="time" id="configTime" value="09:00">
</div>
<div class="input-group">
<label for="configRetryCount">Retry Count:</label>
<input type="number" id="configRetryCount" value="3" min="0" max="10">
</div>
<div class="grid">
<button class="button" onclick="configurePlugin()">Configure Plugin</button>
<button class="button" onclick="updateSettings()">Update Settings</button>
</div>
</div>
<!-- Advanced Features Section -->
<div class="section">
<h2>🚀 Advanced Features</h2>
<div class="grid">
<button class="button" onclick="getExactAlarmStatus()">Exact Alarm Status</button>
<button class="button" onclick="getRebootRecoveryStatus()">Reboot Recovery</button>
<button class="button" onclick="getRollingWindowStats()">Rolling Window</button>
<button class="button" onclick="maintainRollingWindow()">Maintain Window</button>
<button class="button" onclick="getContentCache()">Content Cache</button>
<button class="button" onclick="clearContentCache()">Clear Cache</button>
<button class="button" onclick="getContentHistory()">Content History</button>
<button class="button" onclick="getDualScheduleStatus()">Dual Schedule</button>
</div>
</div>
</div>
<script>
console.log('Script loading...');
console.log('JavaScript is working!');
console.log('🔔 DailyNotification Plugin Test Interface Loading...');
// Use real DailyNotification plugin
console.log('Using real DailyNotification plugin...');
window.DailyNotification = window.Capacitor.Plugins.DailyNotification;
// Global variables
let plugin = null;
let isPluginAvailable = false;
// Define functions immediately and attach to window
function configurePlugin() {
console.log('configurePlugin called');
const status = document.getElementById('status');
const configStatus = document.getElementById('configStatus');
const fetcherStatus = document.getElementById('fetcherStatus');
status.innerHTML = 'Configuring plugin...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
// Update top status to show configuring
configStatus.innerHTML = '⏳ Configuring...';
fetcherStatus.innerHTML = '⏳ Waiting...';
// Initialize plugin on page load
document.addEventListener('DOMContentLoaded', async function() {
console.log('📱 DOM loaded, initializing plugin...');
await initializePlugin();
});
// Initialize the real DailyNotification plugin
async function initializePlugin() {
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
configStatus.innerHTML = '❌ Plugin unavailable';
fetcherStatus.innerHTML = '❌ Plugin unavailable';
return;
// Try to access the real plugin through Capacitor
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DailyNotification) {
plugin = window.Capacitor.Plugins.DailyNotification;
isPluginAvailable = true;
console.log('✅ Real DailyNotification plugin found!');
updateStatus('success', '✅ Real DailyNotification plugin loaded successfully!');
} else {
// Fallback to mock for development
console.log('⚠️ Real plugin not available, using mock for development');
plugin = createMockPlugin();
isPluginAvailable = false;
updateStatus('warning', '⚠️ Using mock plugin (real plugin not available)');
}
// Configure plugin settings
window.DailyNotification.configure({
storage: 'tiered',
ttlSeconds: 86400,
prefetchLeadMinutes: 60,
maxNotificationsPerDay: 3,
retentionDays: 7
})
.then(() => {
console.log('Plugin settings configured, now configuring native fetcher...');
// Update top status
configStatus.innerHTML = '✅ Configured';
// Configure native fetcher with demo credentials
// Note: DemoNativeFetcher uses hardcoded mock data, so this is optional
// but demonstrates the API. In production, this would be real credentials.
return window.DailyNotification.configureNativeFetcher({
apiBaseUrl: 'http://10.0.2.2:3000', // Android emulator → host localhost
activeDid: 'did:ethr:0xDEMO1234567890', // Demo DID
jwtSecret: 'demo-jwt-secret-for-development-testing'
});
})
.then(() => {
// Update top status
fetcherStatus.innerHTML = '✅ Configured';
// Update bottom status for user feedback
status.innerHTML = 'Plugin configured successfully!';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
})
.catch(error => {
// Update top status with error
if (configStatus.innerHTML.includes('Configuring')) {
configStatus.innerHTML = '❌ Failed';
}
if (fetcherStatus.innerHTML.includes('Waiting') || fetcherStatus.innerHTML.includes('Configuring')) {
fetcherStatus.innerHTML = '❌ Failed';
}
status.innerHTML = `Configuration failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
configStatus.innerHTML = '❌ Error';
fetcherStatus.innerHTML = '❌ Error';
status.innerHTML = `Configuration failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
console.error('❌ Plugin initialization failed:', error);
updateStatus('error', `❌ Plugin initialization failed: ${error.message}`);
}
}
function loadPluginStatus() {
console.log('loadPluginStatus called');
const pluginStatusContent = document.getElementById('pluginStatusContent');
const statusCard = document.getElementById('statusCard');
try {
if (!window.DailyNotification) {
pluginStatusContent.innerHTML = '❌ DailyNotification plugin not available';
statusCard.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.getNotificationStatus()
.then(result => {
const nextTime = result.nextNotificationTime ? new Date(result.nextNotificationTime).toLocaleString() : 'None scheduled';
const hasSchedules = result.isEnabled || (result.pending && result.pending > 0);
const statusIcon = hasSchedules ? '✅' : '⏸️';
pluginStatusContent.innerHTML = `${statusIcon} Active Schedules: ${hasSchedules ? 'Yes' : 'No'}<br>
📅 Next Notification: ${nextTime}<br>
⏳ Pending: ${result.pending || 0}`;
statusCard.style.background = hasSchedules ?
'rgba(0, 255, 0, 0.15)' : 'rgba(255, 255, 255, 0.1)'; // Green if active, light gray if none
})
.catch(error => {
pluginStatusContent.innerHTML = `⚠️ Status check failed: ${error.message}`;
statusCard.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
// Create mock plugin for development/testing
function createMockPlugin() {
return {
configure: async (options) => {
console.log('Mock configure called with:', options);
return Promise.resolve();
},
getNotificationStatus: async () => {
return Promise.resolve({
isEnabled: true,
isScheduled: true,
lastNotificationTime: Date.now() - 86400000,
nextNotificationTime: Date.now() + 3600000,
pending: 1,
settings: { url: 'https://api.example.com/content', time: '09:00' },
error: null
});
},
checkPermissions: async () => {
return Promise.resolve({
notifications: 'granted',
backgroundRefresh: 'granted',
alert: true,
badge: true,
sound: true
});
},
requestPermissions: async () => {
return Promise.resolve({
notifications: 'granted',
backgroundRefresh: 'granted',
alert: true,
badge: true,
sound: true
});
},
scheduleDailyNotification: async (options) => {
console.log('Mock scheduleDailyNotification called with:', options);
return Promise.resolve();
},
cancelAllNotifications: async () => {
console.log('Mock cancelAllNotifications called');
return Promise.resolve();
},
getLastNotification: async () => {
return Promise.resolve({
id: 'mock-123',
title: 'Mock Notification',
body: 'This is a mock notification',
timestamp: Date.now() - 3600000,
url: 'https://example.com'
});
},
getBatteryStatus: async () => {
return Promise.resolve({
level: 85,
isCharging: false,
powerState: 1,
isOptimizationExempt: false
});
},
getExactAlarmStatus: async () => {
return Promise.resolve({
supported: true,
enabled: true,
canSchedule: true,
fallbackWindow: '±15 minutes'
});
},
requestExactAlarmPermission: async () => {
console.log('Mock requestExactAlarmPermission called');
return Promise.resolve();
},
openExactAlarmSettings: async () => {
console.log('Mock openExactAlarmSettings called');
return Promise.resolve();
},
requestBatteryOptimizationExemption: async () => {
console.log('Mock requestBatteryOptimizationExemption called');
return Promise.resolve();
},
getRebootRecoveryStatus: async () => {
return Promise.resolve({
inProgress: false,
lastRecoveryTime: Date.now() - 86400000,
timeSinceLastRecovery: 86400000,
recoveryNeeded: false
});
},
getRollingWindowStats: async () => {
return Promise.resolve({
stats: 'Window: 7 days, Notifications: 5, Success rate: 100%',
maintenanceNeeded: false,
timeUntilNextMaintenance: 3600000
});
},
maintainRollingWindow: async () => {
console.log('Mock maintainRollingWindow called');
return Promise.resolve();
},
getContentCache: async () => {
return Promise.resolve({
'cache-key-1': { content: 'Mock cached content', timestamp: Date.now() },
'cache-key-2': { content: 'Another mock item', timestamp: Date.now() - 3600000 }
});
},
clearContentCache: async () => {
console.log('Mock clearContentCache called');
return Promise.resolve();
},
getContentHistory: async () => {
return Promise.resolve([
{ id: '1', timestamp: Date.now() - 86400000, success: true, content: 'Mock content 1' },
{ id: '2', timestamp: Date.now() - 172800000, success: true, content: 'Mock content 2' }
]);
},
getDualScheduleStatus: async () => {
return Promise.resolve({
isActive: true,
contentSchedule: { nextRun: Date.now() + 3600000, isEnabled: true },
userSchedule: { nextRun: Date.now() + 7200000, isEnabled: true },
lastContentFetch: Date.now() - 3600000,
lastUserNotification: Date.now() - 7200000
});
}
};
}
// Utility function to update status display
function updateStatus(type, message) {
const statusEl = document.getElementById('status');
statusEl.className = `status ${type}`;
statusEl.textContent = message;
console.log(`[${type.toUpperCase()}] ${message}`);
}
// Plugin availability check
async function checkPluginAvailability() {
updateStatus('info', '🔍 Checking plugin availability...');
try {
if (plugin) {
updateStatus('success', `✅ Plugin available: ${isPluginAvailable ? 'Real plugin' : 'Mock plugin'}`);
} else {
updateStatus('error', '❌ Plugin not available');
}
} catch (error) {
pluginStatusContent.innerHTML = `⚠️ Status check failed: ${error.message}`;
statusCard.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
updateStatus('error', `❌ Availability check failed: ${error.message}`);
}
}
// Notification test functions
function testNotification() {
console.log('testNotification called');
// Quick sanity check - test plugin availability
if (window.Capacitor && window.Capacitor.isPluginAvailable) {
const isAvailable = window.Capacitor.isPluginAvailable('DailyNotification');
console.log('is plugin available?', isAvailable);
}
const status = document.getElementById('status');
status.innerHTML = 'Testing plugin connection...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
// Get notification status
async function getNotificationStatus() {
updateStatus('info', '📊 Getting notification status...');
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
const status = await plugin.getNotificationStatus();
updateStatus('success', `📊 Status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Status check failed: ${error.message}`);
}
}
// Test the notification method directly
console.log('Testing notification scheduling...');
// Check permissions
async function checkPermissions() {
updateStatus('info', '🔐 Checking permissions...');
try {
const permissions = await plugin.checkPermissions();
updateStatus('success', `🔐 Permissions: ${JSON.stringify(permissions, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Permission check failed: ${error.message}`);
}
}
// Request permissions
async function requestPermissions() {
updateStatus('info', '🔐 Requesting permissions...');
try {
const result = await plugin.requestPermissions();
updateStatus('success', `🔐 Permission result: ${JSON.stringify(result, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Permission request failed: ${error.message}`);
}
}
// Get battery status
async function getBatteryStatus() {
updateStatus('info', '🔋 Getting battery status...');
try {
const battery = await plugin.getBatteryStatus();
updateStatus('success', `🔋 Battery: ${JSON.stringify(battery, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Battery check failed: ${error.message}`);
}
}
// Schedule notification
async function scheduleNotification() {
updateStatus('info', '⏰ Scheduling notification...');
try {
const timeInput = document.getElementById('notificationTime').value;
const [hours, minutes] = timeInput.split(':');
const now = new Date();
const notificationTime = new Date(now.getTime() + 240000); // 4 minutes from now
const prefetchTime = new Date(now.getTime() + 120000); // 2 minutes from now (2 min before notification)
const notificationTimeString = notificationTime.getHours().toString().padStart(2, '0') + ':' +
notificationTime.getMinutes().toString().padStart(2, '0');
const scheduledTime = new Date();
scheduledTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
// If scheduled time is in the past, schedule for tomorrow
if (scheduledTime <= now) {
scheduledTime.setDate(scheduledTime.getDate() + 1);
}
// Calculate prefetch time (2 minutes before notification)
const prefetchTime = new Date(scheduledTime.getTime() - 120000); // 2 minutes
const prefetchTimeReadable = prefetchTime.toLocaleTimeString();
const notificationTimeReadable = scheduledTime.toLocaleTimeString();
const prefetchTimeString = prefetchTime.getHours().toString().padStart(2, '0') + ':' +
prefetchTime.getMinutes().toString().padStart(2, '0');
const notificationTimeString = scheduledTime.getHours().toString().padStart(2, '0') + ':' +
scheduledTime.getMinutes().toString().padStart(2, '0');
window.DailyNotification.scheduleDailyNotification({
time: notificationTimeString,
title: 'Test Notification',
body: 'This is a test notification from the DailyNotification plugin!',
const options = {
url: document.getElementById('notificationUrl').value,
time: timeInput,
title: document.getElementById('notificationTitle').value,
body: document.getElementById('notificationBody').value,
sound: true,
priority: 'high'
})
.then(() => {
const prefetchTimeReadable = prefetchTime.toLocaleTimeString();
const notificationTimeReadable = notificationTime.toLocaleTimeString();
status.innerHTML = '✅ Notification scheduled!<br>' +
'📥 Prefetch: ' + prefetchTimeReadable + ' (' + prefetchTimeString + ')<br>' +
'🔔 Notification: ' + notificationTimeReadable + ' (' + notificationTimeString + ')';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
// Refresh plugin status display
setTimeout(() => loadPluginStatus(), 500);
})
.catch(error => {
// Check if this is an exact alarm permission error
if (error.code === 'EXACT_ALARM_PERMISSION_REQUIRED' ||
error.message.includes('Exact alarm permission') ||
error.message.includes('Alarms & reminders')) {
status.innerHTML = '⚠️ Exact Alarm Permission Required<br><br>' +
'Settings opened automatically.<br>' +
'Please enable "Allow exact alarms" and return to try again.';
status.style.background = 'rgba(255, 165, 0, 0.3)'; // Orange background
} else {
status.innerHTML = `❌ Notification failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
});
};
await plugin.scheduleDailyNotification(options);
updateStatus('success', `✅ Notification scheduled!<br>` +
`📥 Prefetch: ${prefetchTimeReadable} (${prefetchTimeString})<br>` +
`🔔 Notification: ${notificationTimeReadable} (${notificationTimeString})`);
} catch (error) {
status.innerHTML = `Notification test failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
updateStatus('error', `❌ Scheduling failed: ${error.message}`);
}
}
// Permission management functions
function requestPermissions() {
console.log('requestPermissions called');
const status = document.getElementById('status');
status.innerHTML = 'Requesting permissions...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
// Cancel all notifications
async function cancelAllNotifications() {
updateStatus('info', '❌ Cancelling all notifications...');
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.requestNotificationPermissions()
.then(() => {
status.innerHTML = 'Permission request completed! Check your device settings if needed.';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
// Refresh permission and channel status display after request
setTimeout(() => {
loadPermissionStatus();
loadChannelStatus();
}, 1000);
})
.catch(error => {
status.innerHTML = `Permission request failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
await plugin.cancelAllNotifications();
updateStatus('success', '❌ All notifications cancelled');
} catch (error) {
status.innerHTML = `Permission request failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
updateStatus('error', `❌ Cancel failed: ${error.message}`);
}
}
function loadChannelStatus() {
const channelStatus = document.getElementById('channelStatus');
// Get last notification
async function getLastNotification() {
updateStatus('info', '📱 Getting last notification...');
try {
if (!window.DailyNotification) {
channelStatus.innerHTML = '❌ Plugin unavailable';
return;
}
window.DailyNotification.isChannelEnabled()
.then(result => {
const importanceText = getImportanceText(result.importance);
if (result.enabled) {
channelStatus.innerHTML = `✅ Enabled (${importanceText})`;
} else {
channelStatus.innerHTML = `❌ Disabled (${importanceText})`;
}
})
.catch(error => {
channelStatus.innerHTML = '⚠️ Error';
});
const notification = await plugin.getLastNotification();
updateStatus('success', `📱 Last notification: ${JSON.stringify(notification, null, 2)}`);
} catch (error) {
channelStatus.innerHTML = '⚠️ Error';
updateStatus('error', `❌ Get last notification failed: ${error.message}`);
}
}
function checkComprehensiveStatus() {
const status = document.getElementById('status');
status.innerHTML = 'Checking comprehensive status...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
// Configure plugin
async function configurePlugin() {
updateStatus('info', '⚙️ Configuring plugin...');
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.checkStatus()
.then(result => {
const canSchedule = result.canScheduleNow;
const issues = [];
if (!result.postNotificationsGranted) {
issues.push('POST_NOTIFICATIONS permission');
}
if (!result.channelEnabled) {
issues.push('notification channel disabled');
}
if (!result.exactAlarmsGranted) {
issues.push('exact alarm permission');
}
let statusText = `Status: ${canSchedule ? 'Ready to schedule' : 'Issues found'}`;
if (issues.length > 0) {
statusText += `\nIssues: ${issues.join(', ')}`;
}
statusText += `\nChannel: ${getImportanceText(result.channelImportance)}`;
statusText += `\nChannel ID: ${result.channelId}`;
status.innerHTML = statusText;
status.style.background = canSchedule ? 'rgba(0, 255, 0, 0.3)' : 'rgba(255, 0, 0, 0.3)';
})
.catch(error => {
status.innerHTML = `Status check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
const config = {
fetchUrl: document.getElementById('configUrl').value,
scheduleTime: document.getElementById('configTime').value,
retryCount: parseInt(document.getElementById('configRetryCount').value),
enableNotifications: true,
offlineFallback: true
};
await plugin.configure(config);
updateStatus('success', `⚙️ Plugin configured: ${JSON.stringify(config, null, 2)}`);
} catch (error) {
status.innerHTML = `Status check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
updateStatus('error', `❌ Configuration failed: ${error.message}`);
}
}
function getImportanceText(importance) {
switch (importance) {
case 0: return 'None (blocked)';
case 1: return 'Min';
case 2: return 'Low';
case 3: return 'Default';
case 4: return 'High';
case 5: return 'Max';
default: return `Unknown (${importance})`;
}
}
// Attach to window object
window.configurePlugin = configurePlugin;
window.testNotification = testNotification;
window.requestPermissions = requestPermissions;
window.checkComprehensiveStatus = checkComprehensiveStatus;
function loadPermissionStatus() {
const notificationPermStatus = document.getElementById('notificationPermStatus');
const exactAlarmPermStatus = document.getElementById('exactAlarmPermStatus');
// Update settings
async function updateSettings() {
updateStatus('info', '⚙️ Updating settings...');
try {
if (!window.DailyNotification) {
notificationPermStatus.innerHTML = '❌ Plugin unavailable';
exactAlarmPermStatus.innerHTML = '❌ Plugin unavailable';
return;
}
window.DailyNotification.checkPermissionStatus()
.then(result => {
notificationPermStatus.innerHTML = result.notificationsEnabled ? '✅ Granted' : '❌ Not granted';
exactAlarmPermStatus.innerHTML = result.exactAlarmEnabled ? '✅ Granted' : '❌ Not granted';
})
.catch(error => {
notificationPermStatus.innerHTML = '⚠️ Error';
exactAlarmPermStatus.innerHTML = '⚠️ Error';
});
const settings = {
url: document.getElementById('configUrl').value,
time: document.getElementById('configTime').value,
retryCount: parseInt(document.getElementById('configRetryCount').value),
sound: true,
priority: 'high'
};
await plugin.updateSettings(settings);
updateStatus('success', `⚙️ Settings updated: ${JSON.stringify(settings, null, 2)}`);
} catch (error) {
notificationPermStatus.innerHTML = '⚠️ Error';
exactAlarmPermStatus.innerHTML = '⚠️ Error';
updateStatus('error', `❌ Settings update failed: ${error.message}`);
}
}
// Load plugin status automatically on page load
window.addEventListener('load', () => {
console.log('Page loaded, loading plugin status...');
// Small delay to ensure Capacitor is ready
setTimeout(() => {
loadPluginStatus();
loadPermissionStatus();
loadChannelStatus();
}, 500);
});
// Get exact alarm status
async function getExactAlarmStatus() {
updateStatus('info', '⏰ Getting exact alarm status...');
try {
const status = await plugin.getExactAlarmStatus();
updateStatus('success', `⏰ Exact alarm status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Exact alarm check failed: ${error.message}`);
}
}
// Request exact alarm permission
async function requestExactAlarmPermission() {
updateStatus('info', '⏰ Requesting exact alarm permission...');
try {
await plugin.requestExactAlarmPermission();
updateStatus('success', '⏰ Exact alarm permission requested');
} catch (error) {
updateStatus('error', `❌ Exact alarm permission request failed: ${error.message}`);
}
}
// Open exact alarm settings
async function openExactAlarmSettings() {
updateStatus('info', '⚙️ Opening exact alarm settings...');
try {
await plugin.openExactAlarmSettings();
updateStatus('success', '⚙️ Exact alarm settings opened');
} catch (error) {
updateStatus('error', `❌ Open settings failed: ${error.message}`);
}
}
console.log('Functions attached to window:', {
configurePlugin: typeof window.configurePlugin,
testNotification: typeof window.testNotification
});
// Request battery optimization exemption
async function requestBatteryOptimizationExemption() {
updateStatus('info', '🔋 Requesting battery optimization exemption...');
try {
await plugin.requestBatteryOptimizationExemption();
updateStatus('success', '🔋 Battery optimization exemption requested');
} catch (error) {
updateStatus('error', `❌ Battery exemption request failed: ${error.message}`);
}
}
// Get reboot recovery status
async function getRebootRecoveryStatus() {
updateStatus('info', '🔄 Getting reboot recovery status...');
try {
const status = await plugin.getRebootRecoveryStatus();
updateStatus('success', `🔄 Reboot recovery status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Reboot recovery check failed: ${error.message}`);
}
}
// Get rolling window stats
async function getRollingWindowStats() {
updateStatus('info', '📊 Getting rolling window stats...');
try {
const stats = await plugin.getRollingWindowStats();
updateStatus('success', `📊 Rolling window stats: ${JSON.stringify(stats, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Rolling window stats failed: ${error.message}`);
}
}
// Maintain rolling window
async function maintainRollingWindow() {
updateStatus('info', '🔧 Maintaining rolling window...');
try {
await plugin.maintainRollingWindow();
updateStatus('success', '🔧 Rolling window maintenance completed');
} catch (error) {
updateStatus('error', `❌ Rolling window maintenance failed: ${error.message}`);
}
}
// Get content cache
async function getContentCache() {
updateStatus('info', '💾 Getting content cache...');
try {
const cache = await plugin.getContentCache();
updateStatus('success', `💾 Content cache: ${JSON.stringify(cache, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Content cache check failed: ${error.message}`);
}
}
// Clear content cache
async function clearContentCache() {
updateStatus('info', '🗑️ Clearing content cache...');
try {
await plugin.clearContentCache();
updateStatus('success', '🗑️ Content cache cleared');
} catch (error) {
updateStatus('error', `❌ Clear cache failed: ${error.message}`);
}
}
// Get content history
async function getContentHistory() {
updateStatus('info', '📚 Getting content history...');
try {
const history = await plugin.getContentHistory();
updateStatus('success', `📚 Content history: ${JSON.stringify(history, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Content history check failed: ${error.message}`);
}
}
// Get dual schedule status
async function getDualScheduleStatus() {
updateStatus('info', '🔄 Getting dual schedule status...');
try {
const status = await plugin.getDualScheduleStatus();
updateStatus('success', `🔄 Dual schedule status: ${JSON.stringify(status, null, 2)}`);
} catch (error) {
updateStatus('error', `❌ Dual schedule check failed: ${error.message}`);
}
}
console.log('🔔 DailyNotification Plugin Test Interface Loaded Successfully!');
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff