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

239 lines
8.0 KiB
Markdown

# 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`):
```typescript
// Line 715
const granted = await service.requestPermissions();
```
**2. Service Layer** (`src/services/notifications/NativeNotificationService.ts`):
```typescript
// Lines 49-68
async requestPermissions(): Promise<boolean> {
const result = await DailyNotification.requestPermissions();
return result.allPermissionsGranted;
}
```
**3. Plugin Registration** (`src/plugins/DailyNotificationPlugin.ts`):
```typescript
// Line 30-36
const DailyNotification = registerPlugin<DailyNotificationPluginType>(
"DailyNotification"
);
```
**4. iOS Native Implementation** (`node_modules/@timesafari/daily-notification-plugin/ios/Plugin/DailyNotificationScheduler.swift`):
```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`):
```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)
```swift
// 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)
```swift
// 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:
```swift
// 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