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:
412
doc/notification-integration-changes-outline.md
Normal file
412
doc/notification-integration-changes-outline.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# Notification Integration Changes - Implementation Outline
|
||||||
|
|
||||||
|
**Date**: 2026-01-23
|
||||||
|
**Purpose**: Detailed outline of changes needed to integrate DailyNotificationPlugin with UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines all changes required to integrate the DailyNotificationPlugin with the existing notification UI, making it work seamlessly on both native (iOS/Android) and web platforms.
|
||||||
|
|
||||||
|
**Estimated Complexity**: Medium
|
||||||
|
**Estimated Files Changed**: 3-4 files
|
||||||
|
**Breaking Changes**: None (backward compatible)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Change Summary
|
||||||
|
|
||||||
|
| File | Changes | Complexity | Risk |
|
||||||
|
|------|---------|------------|------|
|
||||||
|
| `PushNotificationPermission.vue` | Add platform detection, native flow | Medium | Low |
|
||||||
|
| `AccountViewView.vue` | Platform detection in toggles, hide push server on native | Low | Low |
|
||||||
|
| `WebPushNotificationService.ts` | Complete stub implementation (optional) | Medium | Low |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Changes
|
||||||
|
|
||||||
|
### 1. PushNotificationPermission.vue
|
||||||
|
|
||||||
|
**File**: `src/components/PushNotificationPermission.vue`
|
||||||
|
**Current Lines**: ~656 lines
|
||||||
|
**Estimated New Lines**: +50-80 lines
|
||||||
|
**Complexity**: Medium
|
||||||
|
|
||||||
|
#### Changes Required
|
||||||
|
|
||||||
|
**A. Add Imports** (Top of script section)
|
||||||
|
```typescript
|
||||||
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import { NotificationService } from "@/services/notifications";
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Add Platform Detection Property**
|
||||||
|
```typescript
|
||||||
|
// Add to class properties
|
||||||
|
private get isNativePlatform(): boolean {
|
||||||
|
return Capacitor.isNativePlatform();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C. Modify `open()` Method** (Lines 170-258)
|
||||||
|
- **Current**: Always initializes web push (VAPID key, service worker)
|
||||||
|
- **Change**: Add platform check at start
|
||||||
|
- If native: Skip VAPID/service worker, show UI immediately
|
||||||
|
- If web: Keep existing logic
|
||||||
|
|
||||||
|
**D. Modify `turnOnNotifications()` Method** (Lines 393-499)
|
||||||
|
- **Current**: Web push subscription flow
|
||||||
|
- **Change**: Split into two paths:
|
||||||
|
- **Native path**: Use `NotificationService.getInstance()` → `requestPermissions()` → `scheduleDailyNotification()`
|
||||||
|
- **Web path**: Keep existing logic
|
||||||
|
|
||||||
|
**E. Add New Method: `turnOnNativeNotifications()`**
|
||||||
|
- Request permissions via `NotificationService`
|
||||||
|
- Convert time input (AM/PM) to 24-hour format (HH:mm)
|
||||||
|
- Call `scheduleDailyNotification()` with proper options
|
||||||
|
- Save to settings
|
||||||
|
- Call callback with success/time/message
|
||||||
|
|
||||||
|
**F. Update `handleTurnOnNotifications()` Method** (Line 643)
|
||||||
|
- Add platform check
|
||||||
|
- Route to `turnOnNativeNotifications()` or `turnOnNotifications()` based on platform
|
||||||
|
|
||||||
|
**G. Update Computed Properties**
|
||||||
|
- `isSystemReady`: For native, return `true` immediately (no VAPID needed)
|
||||||
|
- `canShowNotificationForm`: For native, return `true` immediately
|
||||||
|
|
||||||
|
**H. Update Template** (Optional - for better UX)
|
||||||
|
- Add platform-specific messaging if desired
|
||||||
|
- Native: "Notifications will be scheduled on your device"
|
||||||
|
- Web: Keep existing messaging
|
||||||
|
|
||||||
|
#### Code Structure Preview
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async open(pushType: string, callback?: ...) {
|
||||||
|
this.callback = callback || this.callback;
|
||||||
|
this.isVisible = true;
|
||||||
|
this.pushType = pushType;
|
||||||
|
|
||||||
|
// Platform detection
|
||||||
|
if (this.isNativePlatform) {
|
||||||
|
// Native: No VAPID/service worker needed
|
||||||
|
this.serviceWorkerReady = true; // Fake it for UI
|
||||||
|
this.vapidKey = "native"; // Placeholder
|
||||||
|
return; // Skip web push initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing web push initialization...
|
||||||
|
// (keep all existing code)
|
||||||
|
}
|
||||||
|
|
||||||
|
async turnOnNotifications() {
|
||||||
|
if (this.isNativePlatform) {
|
||||||
|
return this.turnOnNativeNotifications();
|
||||||
|
}
|
||||||
|
// Existing web push logic...
|
||||||
|
}
|
||||||
|
|
||||||
|
private async turnOnNativeNotifications(): Promise<void> {
|
||||||
|
const service = NotificationService.getInstance();
|
||||||
|
|
||||||
|
// Request permissions
|
||||||
|
const granted = await service.requestPermissions();
|
||||||
|
if (!granted) {
|
||||||
|
// Handle permission denial
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert time to 24-hour format
|
||||||
|
const time24h = this.convertTo24HourFormat();
|
||||||
|
|
||||||
|
// Determine title and body based on pushType
|
||||||
|
const title = this.pushType === this.DAILY_CHECK_TITLE
|
||||||
|
? "Daily Check-In"
|
||||||
|
: "Daily Reminder";
|
||||||
|
const body = this.pushType === this.DIRECT_PUSH_TITLE
|
||||||
|
? this.messageInput
|
||||||
|
: "Time to check your TimeSafari activity";
|
||||||
|
|
||||||
|
// Schedule notification
|
||||||
|
const success = await service.scheduleDailyNotification({
|
||||||
|
time: time24h,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
priority: 'normal'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Save to settings
|
||||||
|
const timeText = this.notificationTimeText;
|
||||||
|
await this.$saveSettings({
|
||||||
|
[this.pushType === this.DAILY_CHECK_TITLE
|
||||||
|
? 'notifyingNewActivityTime'
|
||||||
|
: 'notifyingReminderTime']: timeText,
|
||||||
|
...(this.pushType === this.DIRECT_PUSH_TITLE && {
|
||||||
|
notifyingReminderMessage: this.messageInput
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call callback
|
||||||
|
this.callback(true, timeText, this.messageInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertTo24HourFormat(): string {
|
||||||
|
const hour = parseInt(this.hourInput);
|
||||||
|
const minute = parseInt(this.minuteInput);
|
||||||
|
|
||||||
|
let hour24 = hour;
|
||||||
|
if (!this.hourAm && hour !== 12) {
|
||||||
|
hour24 = hour + 12;
|
||||||
|
} else if (this.hourAm && hour === 12) {
|
||||||
|
hour24 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing Considerations
|
||||||
|
- Test on iOS device
|
||||||
|
- Test on Android device
|
||||||
|
- Test on web (should still work as before)
|
||||||
|
- Test permission denial flow
|
||||||
|
- Test time conversion (AM/PM → 24-hour)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. AccountViewView.vue
|
||||||
|
|
||||||
|
**File**: `src/views/AccountViewView.vue`
|
||||||
|
**Current Lines**: 2124 lines
|
||||||
|
**Estimated New Lines**: +20-30 lines
|
||||||
|
**Complexity**: Low
|
||||||
|
|
||||||
|
#### Changes Required
|
||||||
|
|
||||||
|
**A. Add Import** (Top of script section, around line 739)
|
||||||
|
```typescript
|
||||||
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. Add Computed Property** (In class, around line 888)
|
||||||
|
```typescript
|
||||||
|
private get isNativePlatform(): boolean {
|
||||||
|
return Capacitor.isNativePlatform();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C. Modify Notification Toggle Methods** (Lines 1134-1202)
|
||||||
|
|
||||||
|
**`showNewActivityNotificationChoice()`** (Lines 1134-1158)
|
||||||
|
- **Current**: Always uses `PushNotificationPermission` component
|
||||||
|
- **Change**: Add platform check
|
||||||
|
- If native: Use `NotificationService` directly (or still use component - it will handle platform)
|
||||||
|
- If web: Keep existing logic
|
||||||
|
- **Note**: Since we're updating `PushNotificationPermission` to handle both, this might not need changes, but we could add direct native path for cleaner code
|
||||||
|
|
||||||
|
**`showReminderNotificationChoice()`** (Lines 1171-1202)
|
||||||
|
- Same as above
|
||||||
|
|
||||||
|
**D. Conditionally Hide Push Server Setting** (Lines 506-549)
|
||||||
|
- Wrap the entire "Notification Push Server" section in `v-if="!isNativePlatform"`
|
||||||
|
- This hides it on iOS/Android where it's not needed
|
||||||
|
|
||||||
|
**E. Update Status Display** (Optional)
|
||||||
|
- When showing notification status, could add platform indicator
|
||||||
|
- "Native notification scheduled" vs "Web push subscription active"
|
||||||
|
|
||||||
|
#### Code Structure Preview
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add computed property
|
||||||
|
private get isNativePlatform(): boolean {
|
||||||
|
return Capacitor.isNativePlatform();
|
||||||
|
}
|
||||||
|
|
||||||
|
// In template, wrap push server section:
|
||||||
|
<section v-if="!isNativePlatform" id="sectionPushServer">
|
||||||
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||||
|
Notification Push Server
|
||||||
|
</h2>
|
||||||
|
<!-- ... existing push server UI ... -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
// Optional: Update notification choice methods
|
||||||
|
async showNewActivityNotificationChoice(): Promise<void> {
|
||||||
|
if (!this.notifyingNewActivity) {
|
||||||
|
// Component now handles platform detection, so this can stay the same
|
||||||
|
// OR we could add direct native path here for cleaner separation
|
||||||
|
(this.$refs.pushNotificationPermission as PushNotificationPermission)
|
||||||
|
.open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
|
||||||
|
// ... existing callback ...
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ... existing turn-off logic ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing Considerations
|
||||||
|
- Verify push server section hidden on iOS
|
||||||
|
- Verify push server section hidden on Android
|
||||||
|
- Verify push server section visible on web
|
||||||
|
- Test notification toggles work on all platforms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. WebPushNotificationService.ts (Optional Enhancement)
|
||||||
|
|
||||||
|
**File**: `src/services/notifications/WebPushNotificationService.ts`
|
||||||
|
**Current Lines**: 213 lines
|
||||||
|
**Estimated New Lines**: +100-150 lines
|
||||||
|
**Complexity**: Medium
|
||||||
|
**Priority**: Low (can be done later)
|
||||||
|
|
||||||
|
#### Changes Required
|
||||||
|
|
||||||
|
**A. Complete `scheduleDailyNotification()` Implementation**
|
||||||
|
- Extract logic from `PushNotificationPermission.vue`
|
||||||
|
- Subscribe to push service
|
||||||
|
- Send subscription to server
|
||||||
|
- Return success status
|
||||||
|
|
||||||
|
**B. Complete `cancelDailyNotification()` Implementation**
|
||||||
|
- Get current subscription
|
||||||
|
- Unsubscribe from push service
|
||||||
|
- Notify server to stop sending
|
||||||
|
|
||||||
|
**C. Complete `getStatus()` Implementation**
|
||||||
|
- Check settings for `notifyingNewActivityTime` / `notifyingReminderTime`
|
||||||
|
- Check service worker subscription status
|
||||||
|
- Return combined status
|
||||||
|
|
||||||
|
**Note**: This is optional because `PushNotificationPermission.vue` already handles web push. Completing this would allow using `NotificationService` directly for web too, but it's not required for the integration to work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
### Phase 1: Core Integration (Required)
|
||||||
|
1. ✅ Update `PushNotificationPermission.vue` with platform detection
|
||||||
|
2. ✅ Update `AccountViewView.vue` to hide push server on native
|
||||||
|
3. ✅ Test on native platforms
|
||||||
|
|
||||||
|
### Phase 2: Polish (Optional)
|
||||||
|
4. Complete `WebPushNotificationService.ts` implementation
|
||||||
|
5. Add platform-specific UI messaging
|
||||||
|
6. Add status indicators
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### Low Risk Changes
|
||||||
|
- ✅ Adding platform detection (read-only check)
|
||||||
|
- ✅ Conditionally hiding UI elements
|
||||||
|
- ✅ Adding new code paths (not modifying existing)
|
||||||
|
|
||||||
|
### Medium Risk Changes
|
||||||
|
- ⚠️ Modifying `turnOnNotifications()` flow (but we're adding, not replacing)
|
||||||
|
- ⚠️ Time format conversion (need to test edge cases)
|
||||||
|
|
||||||
|
### Mitigation Strategies
|
||||||
|
1. **Backward Compatibility**: All changes are additive - existing web push flow remains unchanged
|
||||||
|
2. **Feature Flags**: Could add feature flag to enable/disable native notifications
|
||||||
|
3. **Gradual Rollout**: Test on one platform first (e.g., Android), then iOS
|
||||||
|
4. **Fallback**: If native service fails, could fall back to showing error message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Functional Testing
|
||||||
|
- [ ] Native iOS: Request permissions → Schedule notification → Verify scheduled
|
||||||
|
- [ ] Native Android: Request permissions → Schedule notification → Verify scheduled
|
||||||
|
- [ ] Web: Existing flow still works (no regression)
|
||||||
|
- [ ] Permission denial: Shows appropriate error message
|
||||||
|
- [ ] Time conversion: AM/PM correctly converts to 24-hour format
|
||||||
|
- [ ] Both notification types: Daily Check and Direct Push work on native
|
||||||
|
- [ ] Settings persistence: Times saved correctly to database
|
||||||
|
|
||||||
|
### UI Testing
|
||||||
|
- [ ] Push server setting hidden on iOS
|
||||||
|
- [ ] Push server setting hidden on Android
|
||||||
|
- [ ] Push server setting visible on web
|
||||||
|
- [ ] Notification toggles work on all platforms
|
||||||
|
- [ ] Time picker UI works on native (same as web)
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- [ ] 12:00 AM conversion (should be 00:00)
|
||||||
|
- [ ] 12:00 PM conversion (should be 12:00)
|
||||||
|
- [ ] Invalid time input handling
|
||||||
|
- [ ] App restart: Notifications still scheduled
|
||||||
|
- [ ] Device reboot: Notifications still scheduled (Android)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Required
|
||||||
|
- ✅ `@capacitor/core` - Already in project
|
||||||
|
- ✅ `@timesafari/daily-notification-plugin` - Already installed
|
||||||
|
- ✅ `NotificationService` - Already created
|
||||||
|
|
||||||
|
### No New Dependencies Needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estimated Effort
|
||||||
|
|
||||||
|
| Task | Time Estimate |
|
||||||
|
|------|---------------|
|
||||||
|
| Update PushNotificationPermission.vue | 2-3 hours |
|
||||||
|
| Update AccountViewView.vue | 30 minutes - 1 hour |
|
||||||
|
| Testing on iOS | 1-2 hours |
|
||||||
|
| Testing on Android | 1-2 hours |
|
||||||
|
| Bug fixes & polish | 1-2 hours |
|
||||||
|
| **Total** | **5-10 hours** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
|
||||||
|
1. **Quick Rollback**: Revert changes to `PushNotificationPermission.vue` and `AccountViewView.vue`
|
||||||
|
2. **Partial Rollback**: Keep platform detection but disable native path (feature flag)
|
||||||
|
3. **No Data Migration Needed**: Settings structure unchanged
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions to Consider
|
||||||
|
|
||||||
|
1. **Should we keep using `PushNotificationPermission` component for native, or create separate native flow?**
|
||||||
|
- **Recommendation**: Keep using component (simpler, less code duplication)
|
||||||
|
|
||||||
|
2. **Should we show different UI messaging for native vs web?**
|
||||||
|
- **Recommendation**: Optional enhancement, not required for MVP
|
||||||
|
|
||||||
|
3. **Should we complete `WebPushNotificationService` now or later?**
|
||||||
|
- **Recommendation**: Later (not blocking, existing component works)
|
||||||
|
|
||||||
|
4. **How to handle notification cancellation on native?**
|
||||||
|
- **Recommendation**: Use `NotificationService.cancelDailyNotification()` in existing turn-off logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps After Implementation
|
||||||
|
|
||||||
|
1. Update documentation with platform-specific instructions
|
||||||
|
2. Add error handling for edge cases
|
||||||
|
3. Consider adding notification status display in UI
|
||||||
|
4. Test on real devices (critical for native notifications)
|
||||||
|
5. Monitor for any platform-specific issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2026-01-23
|
||||||
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
|
||||||
378
doc/notification-system-overview.md
Normal file
378
doc/notification-system-overview.md
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
# Notification System Overview
|
||||||
|
|
||||||
|
**Date**: 2026-01-23
|
||||||
|
**Purpose**: Understanding notification architecture and implementation guide for daily-notification-plugin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Your app has **two separate notification systems** that coexist:
|
||||||
|
|
||||||
|
1. **Web Push Notifications** (Web/PWA platforms)
|
||||||
|
- Uses service workers, VAPID keys, and a push server
|
||||||
|
- Requires the "Notification Push Server" setting
|
||||||
|
- Server-based delivery
|
||||||
|
|
||||||
|
2. **Native Notifications** (iOS/Android via DailyNotificationPlugin)
|
||||||
|
- Uses native OS notification APIs
|
||||||
|
- On-device scheduling (no server needed)
|
||||||
|
- The "Notification Push Server" setting is **NOT used** for native
|
||||||
|
|
||||||
|
The system automatically selects the correct implementation based on platform using `Capacitor.isNativePlatform()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notification Push Server Setting
|
||||||
|
|
||||||
|
### Location
|
||||||
|
- **File**: `src/views/AccountViewView.vue` (lines 506-549)
|
||||||
|
- **UI Section**: Advanced Settings → "Notification Push Server"
|
||||||
|
- **Database Field**: `settings.webPushServer`
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
The "Notification Push Server" setting **ONLY applies to Web Push notifications** (web/PWA platforms). It configures:
|
||||||
|
|
||||||
|
1. **VAPID Key Retrieval**: The server URL used to fetch VAPID (Voluntary Application Server Identification) keys
|
||||||
|
2. **Subscription Endpoint**: Where push subscriptions are sent
|
||||||
|
3. **Push Message Delivery**: The server that sends push messages to browsers
|
||||||
|
|
||||||
|
### How It Works (Web Push Flow)
|
||||||
|
|
||||||
|
```
|
||||||
|
User enables notification
|
||||||
|
↓
|
||||||
|
PushNotificationPermission.vue opens
|
||||||
|
↓
|
||||||
|
Fetches VAPID key from: {webPushServer}/web-push/vapid
|
||||||
|
↓
|
||||||
|
Subscribes to browser push service
|
||||||
|
↓
|
||||||
|
Sends subscription + time + message to: {webPushServer}/web-push/subscribe
|
||||||
|
↓
|
||||||
|
Server stores subscription and schedules push messages
|
||||||
|
↓
|
||||||
|
Server sends push messages at scheduled time via browser push service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Code Locations
|
||||||
|
|
||||||
|
**AccountViewView.vue** (lines 1473-1479):
|
||||||
|
```typescript
|
||||||
|
async onClickSavePushServer(): Promise<void> {
|
||||||
|
await this.$saveSettings({
|
||||||
|
webPushServer: this.webPushServerInput,
|
||||||
|
});
|
||||||
|
this.webPushServer = this.webPushServerInput;
|
||||||
|
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.INFO.RELOAD_VAPID);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**PushNotificationPermission.vue** (lines 177-221):
|
||||||
|
- Retrieves `webPushServer` from settings
|
||||||
|
- Fetches VAPID key from `{webPushServer}/web-push/vapid`
|
||||||
|
- Uses VAPID key to subscribe to push notifications
|
||||||
|
|
||||||
|
**PushNotificationPermission.vue** (lines 556-575):
|
||||||
|
- Sends subscription to `/web-push/subscribe` endpoint (relative URL, handled by service worker)
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
|
||||||
|
- ⚠️ **This setting is NOT used for native iOS/Android notifications**
|
||||||
|
- The setting defaults to `DEFAULT_PUSH_SERVER` if not configured
|
||||||
|
- Changing the server requires reloading VAPID keys (hence the warning message)
|
||||||
|
- Local development (`http://localhost`) skips VAPID key retrieval
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Daily Notification Plugin Integration
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
|
||||||
|
✅ **Infrastructure Complete**:
|
||||||
|
- Plugin registered (`src/plugins/DailyNotificationPlugin.ts`)
|
||||||
|
- Service abstraction layer created (`src/services/notifications/`)
|
||||||
|
- Platform detection working
|
||||||
|
- Native implementation ready (`NativeNotificationService.ts`)
|
||||||
|
|
||||||
|
🔄 **UI Integration Needed**:
|
||||||
|
- `PushNotificationPermission.vue` still uses web push logic
|
||||||
|
- AccountViewView notification toggles need platform detection
|
||||||
|
- Settings storage needs to handle both systems
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
NotificationService.getInstance()
|
||||||
|
↓
|
||||||
|
Platform Detection (Capacitor.isNativePlatform())
|
||||||
|
↓
|
||||||
|
┌─────────────────────┬─────────────────────┐
|
||||||
|
│ Native Platform │ Web Platform │
|
||||||
|
│ (iOS/Android) │ (Web/PWA) │
|
||||||
|
├─────────────────────┼─────────────────────┤
|
||||||
|
│ NativeNotification │ WebPushNotification │
|
||||||
|
│ Service │ Service │
|
||||||
|
│ │ │
|
||||||
|
│ Uses: │ Uses: │
|
||||||
|
│ - DailyNotification │ - Service Workers │
|
||||||
|
│ Plugin │ - VAPID Keys │
|
||||||
|
│ - Native OS APIs │ - Push Server │
|
||||||
|
│ - On-device alarms │ - Server scheduling │
|
||||||
|
└─────────────────────┴─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Differences
|
||||||
|
|
||||||
|
| Feature | Native (Plugin) | Web Push |
|
||||||
|
|---------|----------------|----------|
|
||||||
|
| **Server Required** | ❌ No | ✅ Yes (Notification Push Server) |
|
||||||
|
| **Scheduling** | On-device | Server-side |
|
||||||
|
| **Offline Delivery** | ✅ Yes | ❌ No (requires network) |
|
||||||
|
| **Background Support** | ✅ Full | ⚠️ Limited (browser-dependent) |
|
||||||
|
| **Permission Model** | OS-level | Browser-level |
|
||||||
|
| **Settings Storage** | Local only | Local + server subscription |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Recommendations
|
||||||
|
|
||||||
|
### 1. Update PushNotificationPermission Component
|
||||||
|
|
||||||
|
**Current State**: Only handles web push
|
||||||
|
|
||||||
|
**Recommended Changes**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In PushNotificationPermission.vue
|
||||||
|
import { NotificationService } from '@/services/notifications';
|
||||||
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
|
||||||
|
async open(pushType: string, callback?: ...) {
|
||||||
|
const isNative = Capacitor.isNativePlatform();
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
// Use native notification service
|
||||||
|
const service = NotificationService.getInstance();
|
||||||
|
const granted = await service.requestPermissions();
|
||||||
|
|
||||||
|
if (granted) {
|
||||||
|
// Show time picker UI
|
||||||
|
// Then schedule via service.scheduleDailyNotification()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Existing web push logic
|
||||||
|
// ... current implementation ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Update AccountViewView Notification Toggles
|
||||||
|
|
||||||
|
**Current State**: Always uses `PushNotificationPermission` component (web push)
|
||||||
|
|
||||||
|
**Recommended Changes**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In AccountViewView.vue
|
||||||
|
import { NotificationService } from '@/services/notifications';
|
||||||
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
|
||||||
|
async showNewActivityNotificationChoice(): Promise<void> {
|
||||||
|
const isNative = Capacitor.isNativePlatform();
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
// Use native service directly
|
||||||
|
const service = NotificationService.getInstance();
|
||||||
|
// Show time picker, then schedule
|
||||||
|
} else {
|
||||||
|
// Use existing PushNotificationPermission component
|
||||||
|
(this.$refs.pushNotificationPermission as PushNotificationPermission)
|
||||||
|
.open(DAILY_CHECK_TITLE, ...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Settings Storage Strategy
|
||||||
|
|
||||||
|
**Current Settings Fields** (from `src/db/tables/settings.ts`):
|
||||||
|
- `notifyingNewActivityTime` - Time string for daily check
|
||||||
|
- `notifyingReminderTime` - Time string for reminder
|
||||||
|
- `notifyingReminderMessage` - Reminder message text
|
||||||
|
- `webPushServer` - Push server URL (web only)
|
||||||
|
|
||||||
|
**Recommendation**: These settings work for both systems:
|
||||||
|
- ✅ `notifyingNewActivityTime` - Works for both (native stores locally, web sends to server)
|
||||||
|
- ✅ `notifyingReminderTime` - Works for both
|
||||||
|
- ✅ `notifyingReminderMessage` - Works for both
|
||||||
|
- ⚠️ `webPushServer` - Only used for web push (hide on native platforms)
|
||||||
|
|
||||||
|
### 4. Platform-Aware UI
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
|
||||||
|
1. **Hide "Notification Push Server" setting on native platforms**:
|
||||||
|
```vue
|
||||||
|
<h2 v-if="!isNativePlatform" class="text-slate-500 text-sm font-bold mb-2">
|
||||||
|
Notification Push Server
|
||||||
|
</h2>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update help text** to explain platform differences
|
||||||
|
|
||||||
|
3. **Show different messaging** based on platform:
|
||||||
|
- Native: "Notifications are scheduled on your device"
|
||||||
|
- Web: "Notifications are sent via push server"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notification Types
|
||||||
|
|
||||||
|
Your app supports two notification types:
|
||||||
|
|
||||||
|
### 1. Daily Check (`DAILY_CHECK_TITLE`)
|
||||||
|
- **Purpose**: Notify user of new activity/updates
|
||||||
|
- **Message**: Auto-generated by server (web) or app (native)
|
||||||
|
- **Settings Field**: `notifyingNewActivityTime`
|
||||||
|
|
||||||
|
### 2. Direct Push (`DIRECT_PUSH_TITLE`)
|
||||||
|
- **Purpose**: Daily reminder with custom message
|
||||||
|
- **Message**: User-provided (max 100 characters)
|
||||||
|
- **Settings Fields**: `notifyingReminderTime`, `notifyingReminderMessage`
|
||||||
|
|
||||||
|
Both types can be enabled simultaneously.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Flow Examples
|
||||||
|
|
||||||
|
### Native Notification Flow (Recommended Implementation)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Get service instance
|
||||||
|
const service = NotificationService.getInstance();
|
||||||
|
|
||||||
|
// 2. Request permissions
|
||||||
|
const granted = await service.requestPermissions();
|
||||||
|
if (!granted) {
|
||||||
|
// Show error, guide to settings
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Schedule notification
|
||||||
|
await service.scheduleDailyNotification({
|
||||||
|
time: '09:00', // HH:mm format (24-hour)
|
||||||
|
title: 'Daily Check-In',
|
||||||
|
body: 'Time to check your TimeSafari activity',
|
||||||
|
priority: 'normal'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Save to settings
|
||||||
|
await this.$saveSettings({
|
||||||
|
notifyingNewActivityTime: '09:00'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Check status
|
||||||
|
const status = await service.getStatus();
|
||||||
|
console.log('Enabled:', status.enabled);
|
||||||
|
console.log('Time:', status.scheduledTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Push Flow (Current Implementation)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Open PushNotificationPermission component
|
||||||
|
(this.$refs.pushNotificationPermission as PushNotificationPermission)
|
||||||
|
.open(DAILY_CHECK_TITLE, async (success, timeText) => {
|
||||||
|
if (success) {
|
||||||
|
// Component handles:
|
||||||
|
// - VAPID key retrieval from webPushServer
|
||||||
|
// - Service worker subscription
|
||||||
|
// - Sending subscription to server
|
||||||
|
|
||||||
|
// Just save the time
|
||||||
|
await this.$saveSettings({
|
||||||
|
notifyingNewActivityTime: timeText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Native (iOS/Android)
|
||||||
|
- [ ] Request permissions works
|
||||||
|
- [ ] Notification appears at scheduled time
|
||||||
|
- [ ] Notification survives app close
|
||||||
|
- [ ] Notification survives device reboot
|
||||||
|
- [ ] Both notification types can be enabled
|
||||||
|
- [ ] Cancellation works correctly
|
||||||
|
|
||||||
|
### Web Push
|
||||||
|
- [ ] VAPID key retrieval works
|
||||||
|
- [ ] Service worker subscription works
|
||||||
|
- [ ] Subscription sent to server
|
||||||
|
- [ ] Push messages received at scheduled time
|
||||||
|
- [ ] Works with different push server URLs
|
||||||
|
|
||||||
|
### Platform Detection
|
||||||
|
- [ ] Correct service selected on iOS
|
||||||
|
- [ ] Correct service selected on Android
|
||||||
|
- [ ] Correct service selected on web
|
||||||
|
- [ ] Settings UI shows/hides appropriately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Files Reference
|
||||||
|
|
||||||
|
### Core Notification Services
|
||||||
|
- `src/services/notifications/NotificationService.ts` - Factory/selector
|
||||||
|
- `src/services/notifications/NativeNotificationService.ts` - Native implementation
|
||||||
|
- `src/services/notifications/WebPushNotificationService.ts` - Web implementation (stub)
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
- `src/components/PushNotificationPermission.vue` - Web push UI (needs update)
|
||||||
|
- `src/views/AccountViewView.vue` - Settings UI (lines 506-549 for push server)
|
||||||
|
|
||||||
|
### Settings & Constants
|
||||||
|
- `src/db/tables/settings.ts` - Settings schema
|
||||||
|
- `src/constants/app.ts` - `DEFAULT_PUSH_SERVER` constant
|
||||||
|
- `src/libs/util.ts` - `DAILY_CHECK_TITLE`, `DIRECT_PUSH_TITLE`
|
||||||
|
|
||||||
|
### Plugin
|
||||||
|
- `src/plugins/DailyNotificationPlugin.ts` - Plugin registration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Update `PushNotificationPermission.vue`** to detect platform and use appropriate service
|
||||||
|
2. **Update `AccountViewView.vue`** notification toggles to use platform detection
|
||||||
|
3. **Hide "Notification Push Server" setting** on native platforms
|
||||||
|
4. **Test on real devices** (iOS and Android)
|
||||||
|
5. **Update documentation** with platform-specific instructions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions & Answers
|
||||||
|
|
||||||
|
**Q: Do I need to configure the Notification Push Server for native apps?**
|
||||||
|
A: No. The setting is only for web push. Native notifications are scheduled on-device.
|
||||||
|
|
||||||
|
**Q: Can both notification systems be active at the same time?**
|
||||||
|
A: No, they're mutually exclusive per platform. The app automatically selects the correct one.
|
||||||
|
|
||||||
|
**Q: How do I test native notifications?**
|
||||||
|
A: Use `NotificationService.getInstance()` and test on a real device (simulators have limitations).
|
||||||
|
|
||||||
|
**Q: What happens if I change the push server URL?**
|
||||||
|
A: Only affects web push. Users need to re-subscribe to push notifications with the new server.
|
||||||
|
|
||||||
|
**Q: Can I use the same settings fields for both systems?**
|
||||||
|
A: Yes! The time and message fields work for both. Only `webPushServer` is web-specific.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2026-01-23
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
@@ -116,6 +117,7 @@ import * as libsUtil from "../libs/util";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||||
|
import { NotificationService } from "@/services/notifications";
|
||||||
|
|
||||||
// Example interface for error
|
// Example interface for error
|
||||||
interface ErrorResponse {
|
interface ErrorResponse {
|
||||||
@@ -167,6 +169,13 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
serviceWorkerReady = false;
|
serviceWorkerReady = false;
|
||||||
vapidKey = "";
|
vapidKey = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on native platform (iOS/Android)
|
||||||
|
*/
|
||||||
|
private get isNativePlatform(): boolean {
|
||||||
|
return Capacitor.isNativePlatform();
|
||||||
|
}
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
pushType: string,
|
pushType: string,
|
||||||
callback?: (success: boolean, time: string, message?: string) => void,
|
callback?: (success: boolean, time: string, message?: string) => void,
|
||||||
@@ -174,6 +183,30 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
this.callback = callback || this.callback;
|
this.callback = callback || this.callback;
|
||||||
this.isVisible = true;
|
this.isVisible = true;
|
||||||
this.pushType = pushType;
|
this.pushType = pushType;
|
||||||
|
|
||||||
|
// Native platforms: Skip web push initialization
|
||||||
|
if (this.isNativePlatform) {
|
||||||
|
logger.debug(
|
||||||
|
"[PushNotificationPermission] Native platform detected, skipping web push initialization",
|
||||||
|
);
|
||||||
|
// For native, we don't need VAPID or service worker
|
||||||
|
this.serviceWorkerReady = true;
|
||||||
|
this.vapidKey = "native"; // Placeholder for computed properties
|
||||||
|
|
||||||
|
// Set up message input based on push type
|
||||||
|
if (this.pushType === this.DIRECT_PUSH_TITLE) {
|
||||||
|
this.messageInput = this.notificationMessagePlaceholder;
|
||||||
|
// focus on the message input
|
||||||
|
setTimeout(function () {
|
||||||
|
document.getElementById("push-message")?.focus();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this.messageInput = "";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web platform: Initialize web push (existing logic)
|
||||||
try {
|
try {
|
||||||
const settings = await this.$accountSettings();
|
const settings = await this.$accountSettings();
|
||||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||||
@@ -585,16 +618,24 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Computed property: isSystemReady
|
* Computed property: isSystemReady
|
||||||
* Returns true if serviceWorkerReady and vapidKey are set
|
* Returns true if serviceWorkerReady and vapidKey are set
|
||||||
|
* For native platforms, always returns true (no VAPID needed)
|
||||||
*/
|
*/
|
||||||
get isSystemReady(): boolean {
|
get isSystemReady(): boolean {
|
||||||
|
if (this.isNativePlatform) {
|
||||||
|
return true; // Native doesn't need VAPID/service worker
|
||||||
|
}
|
||||||
return this.serviceWorkerReady && !!this.vapidKey;
|
return this.serviceWorkerReady && !!this.vapidKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property: canShowNotificationForm
|
* Computed property: canShowNotificationForm
|
||||||
* Returns true if serviceWorkerReady and vapidKey are set
|
* Returns true if serviceWorkerReady and vapidKey are set
|
||||||
|
* For native platforms, always returns true (no VAPID needed)
|
||||||
*/
|
*/
|
||||||
get canShowNotificationForm(): boolean {
|
get canShowNotificationForm(): boolean {
|
||||||
|
if (this.isNativePlatform) {
|
||||||
|
return true; // Native doesn't need VAPID/service worker
|
||||||
|
}
|
||||||
return this.serviceWorkerReady && !!this.vapidKey;
|
return this.serviceWorkerReady && !!this.vapidKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +683,12 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
*/
|
*/
|
||||||
handleTurnOnNotifications() {
|
handleTurnOnNotifications() {
|
||||||
this.close();
|
this.close();
|
||||||
this.turnOnNotifications();
|
// Route to native or web notification flow based on platform
|
||||||
|
if (this.isNativePlatform) {
|
||||||
|
this.turnOnNativeNotifications();
|
||||||
|
} else {
|
||||||
|
this.turnOnNotifications();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -652,6 +698,168 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
get waitingMessage(): string {
|
get waitingMessage(): string {
|
||||||
return "Waiting for system initialization, which may take up to 5 seconds...";
|
return "Waiting for system initialization, which may take up to 5 seconds...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle native notification setup using DailyNotificationPlugin
|
||||||
|
*/
|
||||||
|
private async turnOnNativeNotifications(): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.debug(
|
||||||
|
"[PushNotificationPermission] Starting native notification setup",
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = NotificationService.getInstance();
|
||||||
|
|
||||||
|
// Request permissions
|
||||||
|
logger.debug(
|
||||||
|
"[PushNotificationPermission] Requesting native permissions",
|
||||||
|
);
|
||||||
|
const granted = await service.requestPermissions();
|
||||||
|
|
||||||
|
if (!granted) {
|
||||||
|
logger.warn(
|
||||||
|
"[PushNotificationPermission] Native notification permissions denied",
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: NOTIFY_PUSH_PERMISSION_ERROR.title,
|
||||||
|
text: NOTIFY_PUSH_PERMISSION_ERROR.message,
|
||||||
|
},
|
||||||
|
PUSH_NOTIFICATION_TIMEOUT_PERSISTENT,
|
||||||
|
);
|
||||||
|
this.callback(false, "", this.messageInput);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert time to 24-hour format (HH:mm)
|
||||||
|
const time24h = this.convertTo24HourFormat();
|
||||||
|
logger.debug(
|
||||||
|
"[PushNotificationPermission] Converted time to 24-hour format:",
|
||||||
|
time24h,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine title and body based on pushType
|
||||||
|
const title =
|
||||||
|
this.pushType === this.DAILY_CHECK_TITLE
|
||||||
|
? "Daily Check-In"
|
||||||
|
: "Daily Reminder";
|
||||||
|
const body =
|
||||||
|
this.pushType === this.DIRECT_PUSH_TITLE
|
||||||
|
? this.messageInput || this.notificationMessagePlaceholder
|
||||||
|
: "Time to check your TimeSafari activity";
|
||||||
|
|
||||||
|
// Schedule notification
|
||||||
|
logger.info(
|
||||||
|
"[PushNotificationPermission] Scheduling native notification:",
|
||||||
|
{
|
||||||
|
time: time24h,
|
||||||
|
title,
|
||||||
|
pushType: this.pushType,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const success = await service.scheduleDailyNotification({
|
||||||
|
time: time24h,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
priority: "normal",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
logger.error(
|
||||||
|
"[PushNotificationPermission] Failed to schedule native notification",
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: NOTIFY_PUSH_SETUP_ERROR.title,
|
||||||
|
text: NOTIFY_PUSH_SETUP_ERROR.message,
|
||||||
|
},
|
||||||
|
PUSH_NOTIFICATION_TIMEOUT_SHORT,
|
||||||
|
);
|
||||||
|
this.callback(false, "", this.messageInput);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to settings
|
||||||
|
const timeText = this.notificationTimeText;
|
||||||
|
const settingsToSave: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (this.pushType === this.DAILY_CHECK_TITLE) {
|
||||||
|
settingsToSave.notifyingNewActivityTime = timeText;
|
||||||
|
} else {
|
||||||
|
settingsToSave.notifyingReminderTime = timeText;
|
||||||
|
if (this.messageInput) {
|
||||||
|
settingsToSave.notifyingReminderMessage = this.messageInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$saveSettings(settingsToSave);
|
||||||
|
logger.debug(
|
||||||
|
"[PushNotificationPermission] Settings saved:",
|
||||||
|
settingsToSave,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: NOTIFY_PUSH_SUCCESS.title,
|
||||||
|
text: NOTIFY_PUSH_SUCCESS.message,
|
||||||
|
},
|
||||||
|
PUSH_NOTIFICATION_TIMEOUT_LONG,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call callback with success
|
||||||
|
this.callback(true, timeText, this.messageInput);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[PushNotificationPermission] Error in native notification setup:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: NOTIFY_PUSH_SETUP_ERROR.title,
|
||||||
|
text: NOTIFY_PUSH_SETUP_ERROR.message,
|
||||||
|
},
|
||||||
|
PUSH_NOTIFICATION_TIMEOUT_SHORT,
|
||||||
|
);
|
||||||
|
this.callback(false, "", this.messageInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert AM/PM time input to 24-hour format (HH:mm)
|
||||||
|
* @returns Time string in HH:mm format
|
||||||
|
*/
|
||||||
|
private convertTo24HourFormat(): string {
|
||||||
|
const hour = parseInt(this.hourInput);
|
||||||
|
const minute = parseInt(this.minuteInput);
|
||||||
|
|
||||||
|
let hour24 = hour;
|
||||||
|
|
||||||
|
// Convert to 24-hour format
|
||||||
|
if (!this.hourAm && hour !== 12) {
|
||||||
|
// PM: add 12 (except for 12 PM which stays 12)
|
||||||
|
hour24 = hour + 12;
|
||||||
|
} else if (this.hourAm && hour === 12) {
|
||||||
|
// 12 AM: convert to 0
|
||||||
|
hour24 = 0;
|
||||||
|
}
|
||||||
|
// AM (except 12): keep as is
|
||||||
|
|
||||||
|
// Format with leading zeros
|
||||||
|
const hourStr = hour24.toString().padStart(2, "0");
|
||||||
|
const minuteStr = minute.toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return `${hourStr}:${minuteStr}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -503,50 +503,53 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
<!-- Notification Push Server setting - only show on web platforms -->
|
||||||
Notification Push Server
|
<div v-if="!isNativePlatform">
|
||||||
</h2>
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||||
<div class="px-3 py-4">
|
Notification Push Server
|
||||||
<input
|
</h2>
|
||||||
v-model="webPushServerInput"
|
<div class="px-3 py-4">
|
||||||
type="text"
|
<input
|
||||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
v-model="webPushServerInput"
|
||||||
/>
|
type="text"
|
||||||
<button
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||||
v-if="webPushServerInput != webPushServer"
|
/>
|
||||||
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
<button
|
||||||
@click="onClickSavePushServer()"
|
v-if="webPushServerInput != webPushServer"
|
||||||
>
|
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||||
<font-awesome
|
@click="onClickSavePushServer()"
|
||||||
icon="floppy-disk"
|
>
|
||||||
class="fa-fw"
|
<font-awesome
|
||||||
color="white"
|
icon="floppy-disk"
|
||||||
></font-awesome>
|
class="fa-fw"
|
||||||
</button>
|
color="white"
|
||||||
<button
|
></font-awesome>
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
</button>
|
||||||
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
|
<button
|
||||||
>
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
Use Prod
|
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
|
||||||
</button>
|
>
|
||||||
<button
|
Use Prod
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
</button>
|
||||||
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
|
<button
|
||||||
>
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
Use Test 1
|
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
|
||||||
</button>
|
>
|
||||||
<button
|
Use Test 1
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
</button>
|
||||||
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
|
<button
|
||||||
>
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
Use Test 2
|
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
|
||||||
</button>
|
>
|
||||||
|
Use Test 2
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span v-if="!webPushServerInput" class="px-4 text-sm">
|
||||||
|
When that setting is blank, this app will use the default web push
|
||||||
|
server URL:
|
||||||
|
{{ DEFAULT_PUSH_SERVER }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="!webPushServerInput" class="px-4 text-sm">
|
|
||||||
When that setting is blank, this app will use the default web push
|
|
||||||
server URL:
|
|
||||||
{{ DEFAULT_PUSH_SERVER }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
|
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
|
||||||
<div class="px-3 py-4">
|
<div class="px-3 py-4">
|
||||||
@@ -887,6 +890,13 @@ export default class AccountViewView extends Vue {
|
|||||||
|
|
||||||
private notify!: ReturnType<typeof createNotifyHelpers>;
|
private notify!: ReturnType<typeof createNotifyHelpers>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on native platform (iOS/Android)
|
||||||
|
*/
|
||||||
|
private get isNativePlatform(): boolean {
|
||||||
|
return Capacitor.isNativePlatform();
|
||||||
|
}
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user