forked from trent_larson/crowd-funder-for-time-pwa
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)
This commit is contained in:
238
doc/notification-permissions-and-rollovers.md
Normal file
238
doc/notification-permissions-and-rollovers.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user