Files
crowd-funder-for-time-pwa/doc/notification-permissions-and-rollovers.md
Jose Olarte III 5a4ab84bfe feat(notifications): integrate DailyNotificationPlugin with UI for native platforms
Integrate DailyNotificationPlugin with notification UI to enable native
notifications on iOS/Android while maintaining web push for web/PWA.

- Add platform detection to PushNotificationPermission component
- Implement native notification flow via NotificationService
- Hide push server setting on native platforms (not needed)
- Add time conversion (AM/PM to 24-hour) for native plugin
- Add comprehensive documentation

Breaking Changes: None (backward compatible)
2026-01-23 19:06:16 +08:00

8.0 KiB

Notification Permissions & Rollover Handling

Date: 2026-01-23
Purpose: Answers to questions about permission requests and rollover handling


Question 1: Where does the notification permission request happen?

Permission Request Flow

The permission request flows through multiple layers:

User clicks "Turn on Daily Message"
    ↓
PushNotificationPermission.vue
    ↓ (line 715)
service.requestPermissions()
    ↓
NotificationService.getInstance()
    ↓ (platform detection)
NativeNotificationService.requestPermissions()
    ↓ (line 53)
DailyNotification.requestPermissions()
    ↓
Plugin Native Code
    ↓
┌─────────────────────┬─────────────────────┐
│   iOS Platform      │   Android Platform   │
├─────────────────────┼─────────────────────┤
│ UNUserNotification  │ ActivityCompat       │
│ Center.current()    │ .requestPermissions()│
│ .requestAuthorization│                     │
│ (options: [.alert, │ (POST_NOTIFICATIONS) │
│  .sound, .badge])   │                     │
└─────────────────────┴─────────────────────┘
    ↓
Native OS Permission Dialog
    ↓
User grants/denies
    ↓
Result returned to app

Code Locations

1. UI Entry Point (src/components/PushNotificationPermission.vue):

// Line 715
const granted = await service.requestPermissions();

2. Service Layer (src/services/notifications/NativeNotificationService.ts):

// Lines 49-68
async requestPermissions(): Promise<boolean> {
  const result = await DailyNotification.requestPermissions();
  return result.allPermissionsGranted;
}

3. Plugin Registration (src/plugins/DailyNotificationPlugin.ts):

// Line 30-36
const DailyNotification = registerPlugin<DailyNotificationPluginType>(
  "DailyNotification"
);

4. iOS Native Implementation (node_modules/@timesafari/daily-notification-plugin/ios/Plugin/DailyNotificationScheduler.swift):

// Lines 113-115
func requestPermissions() async -> Bool {
    let granted = try await notificationCenter.requestAuthorization(
        options: [.alert, .sound, .badge]
    )
    return granted
}

5. Android Native Implementation (node_modules/@timesafari/daily-notification-plugin/android/src/main/java/com/timesafari/dailynotification/PermissionManager.java):

// Line 87
ActivityCompat.requestPermissions(
    activity,
    new String[]{Manifest.permission.POST_NOTIFICATIONS},
    REQUEST_CODE
);

Platform-Specific Details

iOS

  • API Used: UNUserNotificationCenter.requestAuthorization()
  • Options Requested: .alert, .sound, .badge
  • Dialog: System-native iOS permission dialog
  • Location: First time user enables notifications
  • Result: Returns true if granted, false if denied

Android

  • API Used: ActivityCompat.requestPermissions()
  • Permission: POST_NOTIFICATIONS (Android 13+)
  • Dialog: System-native Android permission dialog
  • Location: First time user enables notifications
  • Result: Returns true if granted, false if denied
  • Note: Android 12 and below don't require runtime permission (declared in manifest)

When Permission Request Happens

The permission request is triggered when:

  1. User opens the notification setup dialog (PushNotificationPermission.vue)
  2. User clicks "Turn on Daily Message" button
  3. App detects native platform (isNativePlatform === true)
  4. turnOnNativeNotifications() method is called
  5. service.requestPermissions() is called (line 715)

Important: The permission dialog only appears once per app installation. After that:

  • If granted: Future calls to requestPermissions() return true immediately
  • If denied: User must manually enable in system settings

Question 2: Does the plugin handle rollovers automatically?

Yes - Rollover Handling is Automatic

The plugin automatically handles rollovers in multiple scenarios:

1. Initial Scheduling (Time Has Passed Today)

Location: ios/Plugin/DailyNotificationScheduler.swift (lines 326-329)

// If time has passed today, schedule for tomorrow
if scheduledDate <= now {
    scheduledDate = calendar.date(byAdding: .day, value: 1, to: scheduledDate) ?? scheduledDate
}

Behavior:

  • If user schedules a notification for 9:00 AM but it's already 10:00 AM today
  • Plugin automatically schedules it for 9:00 AM tomorrow
  • No manual intervention needed

2. Daily Rollover (After Notification Fires)

Location: ios/Plugin/DailyNotificationScheduler.swift (lines 437-609)

The plugin has a scheduleNextNotification() function that:

  • Automatically schedules the next day's notification after current one fires
  • Handles 24-hour rollovers with DST (Daylight Saving Time) awareness
  • Prevents duplicate rollovers with state tracking

Key Function: calculateNextScheduledTime() (lines 397-435)

// Add 24 hours (handles DST transitions automatically)
guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else {
    // Fallback to simple 24-hour addition
    return currentScheduledTime + (24 * 60 * 60 * 1000)
}

Features:

  • DST-safe: Uses Calendar API to handle daylight saving transitions
  • Automatic: No manual scheduling needed
  • Persistent: Survives app restarts and device reboots
  • Duplicate prevention: Tracks rollover state to prevent duplicates

3. Rollover State Tracking

Location: ios/Plugin/DailyNotificationStorage.swift (lines 161-195)

The plugin tracks rollover state to prevent duplicate scheduling:

// Check if rollover was processed recently (< 1 hour ago)
if let lastTime = lastRolloverTime,
   (currentTime - lastTime) < (60 * 60 * 1000) {
    // Skip - already processed
    return false
}

Purpose: Prevents multiple rollover attempts if notification fires multiple times

4. Android Rollover Handling

Android implementation also handles rollovers:

  • Uses AlarmManager with setRepeating() or schedules next alarm after current fires
  • Handles timezone changes and DST transitions
  • Persists across device reboots via BootReceiver

Rollover Scenarios Handled

Scenario Handled? How
Time passed today Yes Schedules for tomorrow automatically
Daily rollover Yes Schedules next day after notification fires
DST transitions Yes Uses Calendar API for DST-aware calculations
Device reboot Yes BootReceiver restores schedules
App restart Yes Schedules persist in database
Duplicate prevention Yes State tracking prevents duplicate rollovers

Verification

You can verify rollover handling by:

  1. Check iOS logs for rollover messages:

    DNP-ROLLOVER: START id=... current_time=... scheduled_time=...
    DNP-ROLLOVER: CALC_NEXT current=... next=... diff_hours=24.00
    
  2. Test scenario: Schedule notification for a time that's already passed today

    • Expected: Notification scheduled for tomorrow at same time
  3. Test scenario: Wait for notification to fire

    • Expected: Next day's notification automatically scheduled

Summary

Permission Request: Happens in native plugin code via platform-specific APIs:

  • iOS: UNUserNotificationCenter.requestAuthorization()
  • Android: ActivityCompat.requestPermissions()

Rollover Handling: Fully automatic:

  • Initial scheduling: If time passed, schedules for tomorrow
  • Daily rollover: Automatically schedules next day after notification fires
  • DST handling: Calendar-aware calculations
  • Duplicate prevention: State tracking prevents issues
  • Persistence: Survives app restarts and device reboots

No manual intervention needed - the plugin handles all rollover scenarios automatically!


Last Updated: 2026-01-23