Restructure Android project from nested module layout to standard Capacitor plugin structure following community conventions. Structure Changes: - Move plugin code from android/plugin/ to android/src/main/java/ - Move test app from android/app/ to test-apps/android-test-app/app/ - Remove nested android/plugin module structure - Remove nested android/app test app structure Build Infrastructure: - Add Gradle wrapper files (gradlew, gradlew.bat, gradle/wrapper/) - Transform android/build.gradle from root project to library module - Update android/settings.gradle for standalone plugin builds - Add android/gradle.properties with AndroidX configuration - Add android/consumer-rules.pro for ProGuard rules Configuration Updates: - Add prepare script to package.json for automatic builds on npm install - Update package.json version to 1.0.1 - Update android/build.gradle to properly resolve Capacitor dependencies - Update test-apps/android-test-app/settings.gradle with correct paths - Remove android/variables.gradle (hardcode values in build.gradle) Documentation: - Update BUILDING.md with new structure and build process - Update INTEGRATION_GUIDE.md to reflect standard structure - Update README.md to remove path fix warnings - Add test-apps/BUILD_PROCESS.md documenting test app build flows Test App Configuration: - Fix android-test-app to correctly reference plugin and Capacitor - Remove capacitor-cordova-android-plugins dependency (not needed) - Update capacitor.settings.gradle path verification in fix script BREAKING CHANGE: Plugin now uses standard Capacitor Android structure. Consuming apps must update their capacitor.settings.gradle to reference android/ instead of android/plugin/. This is automatically handled by Capacitor CLI for apps using standard plugin installation.
2076 lines
67 KiB
Markdown
2076 lines
67 KiB
Markdown
# TimeSafari Daily Notification Plugin Integration Guide
|
|
|
|
**Author**: Matthew Raymer
|
|
**Version**: 2.2.0
|
|
**Created**: 2025-01-27 12:00:00 UTC
|
|
**Last Updated**: 2025-10-08 06:02:45 UTC
|
|
|
|
## Overview
|
|
|
|
This document provides comprehensive step-by-step instructions for integrating the TimeSafari Daily Notification Plugin into the TimeSafari application. The plugin features a **native-first architecture** with robust polling interface where the host app defines the inputs and response format, and the plugin provides a reliable polling routine optimized for **Android, iOS, and Electron platforms**.
|
|
|
|
### New Generic Polling Architecture
|
|
|
|
The plugin provides a **structured request/response polling system** where:
|
|
|
|
1. **Host App Defines**: Request schema, response schema, transformation logic, notification logic
|
|
2. **Plugin Provides**: Generic polling routine with retry logic, authentication, scheduling, storage pressure management
|
|
3. **Benefits**: Platform-agnostic, flexible, testable, maintainable
|
|
|
|
### TimeSafari Community Features
|
|
|
|
The Daily Notification Plugin supports TimeSafari's community-building goals by providing reliable daily notifications for:
|
|
|
|
**Offers**
|
|
- New offers directed to me
|
|
- Changed offers directed to me
|
|
- New offers to my projects
|
|
- Changed offers to my projects
|
|
- New offers to my favorited projects
|
|
- Changed offers to my favorited projects
|
|
|
|
**Projects**
|
|
- Local projects that are new
|
|
- Local projects that have changed
|
|
- Projects with content of interest that are new
|
|
- Favorited projects that have changed
|
|
|
|
**People**
|
|
- Local people who are new
|
|
- Local people who have changed
|
|
- People with content of interest who are new
|
|
- Favorited people who have changed
|
|
- People in my contacts who have changed
|
|
|
|
**Items**
|
|
- Local items that are new
|
|
- Local items that have changed
|
|
- Favorited items that have changed
|
|
|
|
All notifications are delivered through a single route that can be queried or bundled for efficient delivery while maintaining privacy-preserving communication.
|
|
|
|
This plugin provides enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms with native-first architecture.
|
|
|
|
### Native-First Architecture
|
|
|
|
The plugin has been optimized for **native-first deployment** with the following key changes:
|
|
|
|
**Platform Support:**
|
|
- ✅ **Android**: WorkManager + AlarmManager + SQLite
|
|
- ✅ **iOS**: BGTaskScheduler + UNUserNotificationCenter + Core Data
|
|
- ✅ **Electron**: Desktop notifications + SQLite/LocalStorage
|
|
- ❌ **Web (PWA)**: Removed for native-first focus
|
|
|
|
**Key Benefits:**
|
|
- **Simplified Architecture**: Focused on mobile and desktop platforms
|
|
- **Better Performance**: Optimized for native platform capabilities
|
|
- **Reduced Complexity**: Fewer platform-specific code paths
|
|
- **Cleaner Codebase**: Removed unused web-specific code (~90 lines)
|
|
|
|
**Storage Strategy:**
|
|
- **Native Platforms**: SQLite integration with host-managed storage
|
|
- **Electron**: SQLite or LocalStorage fallback
|
|
- **No Browser Storage**: IndexedDB support removed
|
|
|
|
## Prerequisites
|
|
|
|
- Node.js 18+ and npm installed
|
|
- Android Studio (for Android development)
|
|
- Xcode 14+ (for iOS development)
|
|
- Git access to the TimeSafari daily-notification-plugin repository
|
|
- Understanding of Capacitor plugin architecture
|
|
- Basic knowledge of TypeScript and Vue.js (for TimeSafari integration)
|
|
- Understanding of TimeSafari's privacy-preserving claims architecture
|
|
- Familiarity with decentralized identifiers (DIDs) and cryptographic verification
|
|
|
|
## Plugin Repository Structure
|
|
|
|
The TimeSafari Daily Notification Plugin follows the standard Capacitor plugin structure:
|
|
```
|
|
daily-notification-plugin/
|
|
├── android/
|
|
│ ├── build.gradle # Plugin build configuration
|
|
│ ├── src/main/java/com/timesafari/dailynotification/
|
|
│ │ ├── DailyNotificationPlugin.java
|
|
│ │ ├── DailyNotificationWorker.java
|
|
│ │ ├── DailyNotificationDatabase.java
|
|
│ │ └── ... (other plugin classes)
|
|
│ └── src/main/AndroidManifest.xml
|
|
├── ios/
|
|
│ ├── DailyNotificationPlugin.swift
|
|
│ ├── NotificationManager.swift
|
|
│ ├── ContentFetcher.swift
|
|
│ ├── CallbackRegistry.swift
|
|
│ └── DailyNotificationPlugin.podspec
|
|
├── src/
|
|
│ ├── definitions.ts
|
|
│ ├── daily-notification.ts
|
|
│ ├── callback-registry.ts
|
|
│ ├── observability.ts
|
|
│ └── (web support removed - native-first architecture)
|
|
│ ├── service-worker-manager.ts
|
|
│ └── sw.ts
|
|
├── dist/
|
|
│ ├── plugin.js
|
|
│ ├── esm/
|
|
│ └── (web support removed - native-first architecture)
|
|
├── package.json
|
|
├── capacitor.config.ts
|
|
└── README.md
|
|
```
|
|
|
|
## Generic Polling Integration
|
|
|
|
### Quick Start with Generic Polling
|
|
|
|
The new generic polling interface allows TimeSafari to define exactly what data it needs and how to process it:
|
|
|
|
```typescript
|
|
import {
|
|
GenericPollingRequest,
|
|
PollingScheduleConfig,
|
|
StarredProjectsRequest,
|
|
StarredProjectsResponse
|
|
} from '@timesafari/polling-contracts';
|
|
|
|
// 1. Define your polling request
|
|
const starredProjectsRequest: GenericPollingRequest<StarredProjectsRequest, StarredProjectsResponse> = {
|
|
endpoint: '/api/v2/report/plansLastUpdatedBetween',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0'
|
|
},
|
|
body: {
|
|
planIds: [], // Will be populated from user settings
|
|
afterId: undefined, // Will be populated from watermark
|
|
limit: 100
|
|
},
|
|
responseSchema: {
|
|
validate: (data: any): data is StarredProjectsResponse => {
|
|
return data &&
|
|
Array.isArray(data.data) &&
|
|
typeof data.hitLimit === 'boolean' &&
|
|
data.pagination &&
|
|
typeof data.pagination.hasMore === 'boolean';
|
|
},
|
|
transformError: (error: any) => ({
|
|
code: 'VALIDATION_ERROR',
|
|
message: error.message || 'Validation failed',
|
|
retryable: false
|
|
})
|
|
},
|
|
retryConfig: {
|
|
maxAttempts: 3,
|
|
backoffStrategy: 'exponential',
|
|
baseDelayMs: 1000
|
|
},
|
|
timeoutMs: 30000
|
|
};
|
|
|
|
// 2. Schedule the polling
|
|
const scheduleConfig: PollingScheduleConfig<StarredProjectsRequest, StarredProjectsResponse> = {
|
|
request: starredProjectsRequest,
|
|
schedule: {
|
|
cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily
|
|
timezone: 'UTC',
|
|
maxConcurrentPolls: 1
|
|
},
|
|
notificationConfig: {
|
|
enabled: true,
|
|
templates: {
|
|
singleUpdate: '{projectName} has been updated',
|
|
multipleUpdates: 'You have {count} new updates in your starred projects'
|
|
},
|
|
groupingRules: {
|
|
maxGroupSize: 5,
|
|
timeWindowMinutes: 5
|
|
}
|
|
},
|
|
stateConfig: {
|
|
watermarkKey: 'lastAckedStarredPlanChangesJwtId',
|
|
storageAdapter: new TimeSafariStorageAdapter()
|
|
}
|
|
};
|
|
|
|
// 3. Execute the polling
|
|
const scheduleId = await DailyNotification.schedulePoll(scheduleConfig);
|
|
```
|
|
|
|
### Host App Integration Pattern
|
|
|
|
```typescript
|
|
// TimeSafari app integration
|
|
class TimeSafariPollingService {
|
|
private pollingManager: GenericPollingManager;
|
|
|
|
constructor() {
|
|
this.pollingManager = new GenericPollingManager(jwtManager);
|
|
}
|
|
|
|
async setupStarredProjectsPolling(): Promise<string> {
|
|
// Get user's starred projects
|
|
const starredProjects = await this.getUserStarredProjects();
|
|
|
|
// Update request body with user data
|
|
starredProjectsRequest.body.planIds = starredProjects;
|
|
|
|
// Get current watermark
|
|
const watermark = await this.getCurrentWatermark();
|
|
starredProjectsRequest.body.afterId = watermark;
|
|
|
|
// Schedule the poll
|
|
const scheduleId = await this.pollingManager.schedulePoll(scheduleConfig);
|
|
|
|
return scheduleId;
|
|
}
|
|
|
|
async handlePollingResult(result: PollingResult<StarredProjectsResponse>): Promise<void> {
|
|
if (result.success && result.data) {
|
|
const changes = result.data.data;
|
|
|
|
if (changes.length > 0) {
|
|
// Generate notifications
|
|
await this.generateNotifications(changes);
|
|
|
|
// Update watermark with CAS
|
|
const latestJwtId = changes[changes.length - 1].planSummary.jwtId;
|
|
await this.updateWatermark(latestJwtId);
|
|
|
|
// Acknowledge changes with server
|
|
await this.acknowledgeChanges(changes.map(c => c.planSummary.jwtId));
|
|
}
|
|
} else if (result.error) {
|
|
console.error('Polling failed:', result.error);
|
|
// Handle error (retry, notify user, etc.)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Integration Steps
|
|
|
|
### 1. Install Plugin and Contracts Package
|
|
|
|
Add the plugin and contracts package to your `package.json` dependencies:
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"@timesafari/daily-notification-plugin": "git+https://github.com/timesafari/daily-notification-plugin.git#main",
|
|
"@timesafari/polling-contracts": "file:./packages/polling-contracts"
|
|
}
|
|
}
|
|
```
|
|
|
|
Or install directly via npm:
|
|
```bash
|
|
npm install git+https://github.com/timesafari/daily-notification-plugin.git#main
|
|
npm install ./packages/polling-contracts
|
|
```
|
|
|
|
### 2. Configure Capacitor
|
|
|
|
Update `capacitor.config.ts` to include the plugin configuration:
|
|
|
|
```typescript
|
|
import { CapacitorConfig } from '@capacitor/cli';
|
|
|
|
const config: CapacitorConfig = {
|
|
appId: 'app.timesafari',
|
|
appName: 'TimeSafari',
|
|
webDir: 'dist', // For Capacitor web builds (not browser PWA)
|
|
server: {
|
|
cleartext: true
|
|
},
|
|
plugins: {
|
|
// Existing TimeSafari plugins...
|
|
App: {
|
|
appUrlOpen: {
|
|
handlers: [
|
|
{
|
|
url: 'timesafari://*',
|
|
autoVerify: true
|
|
}
|
|
]
|
|
}
|
|
},
|
|
SplashScreen: {
|
|
launchShowDuration: 3000,
|
|
launchAutoHide: true,
|
|
backgroundColor: '#ffffff',
|
|
androidSplashResourceName: 'splash',
|
|
androidScaleType: 'CENTER_CROP',
|
|
showSpinner: false,
|
|
androidSpinnerStyle: 'large',
|
|
iosSpinnerStyle: 'small',
|
|
spinnerColor: '#999999',
|
|
splashFullScreen: true,
|
|
splashImmersive: true
|
|
},
|
|
CapSQLite: {
|
|
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
|
iosIsEncryption: false,
|
|
iosBiometric: {
|
|
biometricAuth: false,
|
|
biometricTitle: 'Biometric login for TimeSafari'
|
|
},
|
|
androidIsEncryption: false,
|
|
androidBiometric: {
|
|
biometricAuth: false,
|
|
biometricTitle: 'Biometric login for TimeSafari'
|
|
},
|
|
electronIsEncryption: false
|
|
},
|
|
// Add Daily Notification Plugin configuration with generic polling support
|
|
DailyNotification: {
|
|
// Plugin-specific configuration
|
|
defaultChannel: 'timesafari_community',
|
|
enableSound: true,
|
|
enableVibration: true,
|
|
enableLights: true,
|
|
priority: 'high',
|
|
|
|
// Generic Polling Support
|
|
genericPolling: {
|
|
enabled: true,
|
|
schedules: [
|
|
// Starred Projects Polling
|
|
{
|
|
request: {
|
|
endpoint: '/api/v2/report/plansLastUpdatedBetween',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0'
|
|
},
|
|
body: {
|
|
planIds: [], // Populated from user settings
|
|
afterId: undefined, // Populated from watermark
|
|
limit: 100
|
|
},
|
|
responseSchema: {
|
|
validate: (data: any) => data && Array.isArray(data.data),
|
|
transformError: (error: any) => ({
|
|
code: 'VALIDATION_ERROR',
|
|
message: error.message,
|
|
retryable: false
|
|
})
|
|
},
|
|
retryConfig: {
|
|
maxAttempts: 3,
|
|
backoffStrategy: 'exponential',
|
|
baseDelayMs: 1000
|
|
},
|
|
timeoutMs: 30000
|
|
},
|
|
schedule: {
|
|
cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily
|
|
timezone: 'UTC',
|
|
maxConcurrentPolls: 1
|
|
},
|
|
notificationConfig: {
|
|
enabled: true,
|
|
templates: {
|
|
singleUpdate: '{projectName} has been updated',
|
|
multipleUpdates: 'You have {count} new updates in your starred projects'
|
|
},
|
|
groupingRules: {
|
|
maxGroupSize: 5,
|
|
timeWindowMinutes: 5
|
|
}
|
|
},
|
|
stateConfig: {
|
|
watermarkKey: 'lastAckedStarredPlanChangesJwtId',
|
|
storageAdapter: 'timesafari' // Use TimeSafari's storage
|
|
}
|
|
}
|
|
],
|
|
maxConcurrentPolls: 3,
|
|
globalRetryConfig: {
|
|
maxAttempts: 3,
|
|
backoffStrategy: 'exponential',
|
|
baseDelayMs: 1000
|
|
}
|
|
},
|
|
|
|
// Legacy dual scheduling configuration (for backward compatibility)
|
|
contentFetch: {
|
|
enabled: true,
|
|
schedule: '0 8 * * *', // 8 AM daily - fetch community updates
|
|
url: 'https://endorser.ch/api/v2/report/notifications/bundle',
|
|
headers: {
|
|
'Authorization': 'Bearer your-jwt-token',
|
|
'Content-Type': 'application/json',
|
|
'X-Privacy-Level': 'user-controlled'
|
|
},
|
|
ttlSeconds: 3600,
|
|
timeout: 30000,
|
|
retryAttempts: 3,
|
|
retryDelay: 5000
|
|
},
|
|
userNotification: {
|
|
enabled: true,
|
|
schedule: '0 9 * * *',
|
|
title: 'TimeSafari Community Update',
|
|
body: 'New offers, projects, people, and items await your attention!',
|
|
sound: true,
|
|
vibration: true,
|
|
priority: 'high'
|
|
},
|
|
|
|
// Observability configuration
|
|
observability: {
|
|
enableLogging: true,
|
|
logLevel: 'info',
|
|
enableMetrics: true,
|
|
enableHealthChecks: true,
|
|
telemetryConfig: {
|
|
lowCardinalityMetrics: true,
|
|
piiRedaction: true,
|
|
retentionDays: 30
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// ... rest of your config
|
|
};
|
|
|
|
export default config;
|
|
```
|
|
|
|
### 3. Android Integration
|
|
|
|
#### 3.1 Update Android Settings
|
|
|
|
Modify `android/settings.gradle` to include the plugin:
|
|
|
|
```gradle
|
|
include ':app'
|
|
include ':capacitor-cordova-android-plugins'
|
|
include ':daily-notification-plugin'
|
|
|
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
|
project(':daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/')
|
|
|
|
apply from: 'capacitor.settings.gradle'
|
|
```
|
|
|
|
#### 3.2 Update Android App Build Configuration
|
|
|
|
Modify `android/app/build.gradle` to include the plugin dependency:
|
|
|
|
```gradle
|
|
dependencies {
|
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
|
implementation project(':capacitor-android')
|
|
implementation project(':capacitor-community-sqlite')
|
|
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
|
|
|
// Add Daily Notification Plugin
|
|
implementation project(':daily-notification-plugin')
|
|
|
|
// Required dependencies for the plugin
|
|
implementation "androidx.room:room-runtime:2.6.1"
|
|
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
annotationProcessor "androidx.room:room-compiler:2.6.1"
|
|
|
|
testImplementation "junit:junit:$junitVersion"
|
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
implementation project(':capacitor-cordova-android-plugins')
|
|
}
|
|
```
|
|
|
|
#### 3.3 Update Android Manifest
|
|
|
|
Add required permissions to `android/app/src/main/AndroidManifest.xml`:
|
|
|
|
```xml
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
<!-- Existing permissions -->
|
|
|
|
<!-- Daily Notification Plugin permissions -->
|
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
|
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
<uses-permission android:name="android.permission.VIBRATE" />
|
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
|
|
<application>
|
|
<!-- Existing application configuration -->
|
|
|
|
<!-- Daily Notification Plugin receivers and services -->
|
|
<receiver android:name="com.timesafari.dailynotification.NotifyReceiver"
|
|
android:enabled="true"
|
|
android:exported="false" />
|
|
<receiver android:name="com.timesafari.dailynotification.BootReceiver"
|
|
android:enabled="true"
|
|
android:exported="false">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
</intent-filter>
|
|
</receiver>
|
|
|
|
<!-- WorkManager constraints -->
|
|
<provider android:name="androidx.startup.InitializationProvider"
|
|
android:authorities="${applicationId}.androidx-startup"
|
|
android:exported="false"
|
|
tools:node="merge">
|
|
<meta-data android:name="androidx.work.WorkManagerInitializer"
|
|
android:value="androidx.startup" />
|
|
</provider>
|
|
</application>
|
|
</manifest>
|
|
```
|
|
|
|
### 4. iOS Integration
|
|
|
|
#### 4.1 Update Podfile
|
|
|
|
Modify `ios/App/Podfile` to include the plugin:
|
|
|
|
```ruby
|
|
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
|
|
|
platform :ios, '13.0'
|
|
use_frameworks!
|
|
|
|
# workaround to avoid Xcode caching of Pods that requires
|
|
# Product -> Clean Build Folder after new Cordova plugins installed
|
|
# Requires CocoaPods 1.6 or newer
|
|
install! 'cocoapods', :disable_input_output_paths => true
|
|
|
|
def capacitor_pods
|
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
|
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
|
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
|
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
|
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
|
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
|
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
|
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
|
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
|
|
|
# Add Daily Notification Plugin
|
|
pod 'DailyNotificationPlugin', :path => '../../node_modules/@timesafari/daily-notification-plugin/ios'
|
|
end
|
|
|
|
target 'App' do
|
|
capacitor_pods
|
|
# Add your Pods here
|
|
end
|
|
|
|
post_install do |installer|
|
|
assertDeploymentTarget(installer)
|
|
installer.pods_project.targets.each do |target|
|
|
target.build_configurations.each do |config|
|
|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 4.2 Update iOS Info.plist
|
|
|
|
Add required permissions to `ios/App/App/Info.plist`:
|
|
|
|
```xml
|
|
<dict>
|
|
<!-- Existing configuration -->
|
|
|
|
<!-- Daily Notification Plugin background modes -->
|
|
<key>UIBackgroundModes</key>
|
|
<array>
|
|
<string>background-app-refresh</string>
|
|
<string>background-processing</string>
|
|
<string>background-fetch</string>
|
|
</array>
|
|
|
|
<!-- BGTaskScheduler identifiers -->
|
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
|
<array>
|
|
<string>com.timesafari.dailynotification.content-fetch</string>
|
|
<string>com.timesafari.dailynotification.notification-delivery</string>
|
|
</array>
|
|
|
|
<!-- Notification usage description -->
|
|
<key>NSUserNotificationsUsageDescription</key>
|
|
<string>TimeSafari needs permission to send you notifications about important updates and reminders.</string>
|
|
|
|
<!-- Background processing usage description -->
|
|
<key>NSBackgroundTasksUsageDescription</key>
|
|
<string>TimeSafari uses background processing to fetch and deliver daily notifications.</string>
|
|
</dict>
|
|
```
|
|
|
|
#### 4.3 Enable iOS Capabilities
|
|
|
|
1. Open your project in Xcode
|
|
2. Select your app target
|
|
3. Go to "Signing & Capabilities"
|
|
4. Add the following capabilities:
|
|
- **Background Modes**
|
|
- Enable "Background App Refresh"
|
|
- Enable "Background Processing"
|
|
- **Push Notifications** (if using push notifications)
|
|
|
|
### 5. TypeScript Integration
|
|
|
|
#### 5.1 Create Plugin Service
|
|
|
|
Create `src/services/DailyNotificationService.ts`:
|
|
|
|
```typescript
|
|
import { DailyNotification } from '@timesafari/daily-notification-plugin';
|
|
import {
|
|
DualScheduleConfiguration,
|
|
ContentFetchConfig,
|
|
UserNotificationConfig,
|
|
CallbackEvent
|
|
} from '@timesafari/daily-notification-plugin';
|
|
import {
|
|
GenericPollingRequest,
|
|
PollingScheduleConfig,
|
|
PollingResult,
|
|
StarredProjectsRequest,
|
|
StarredProjectsResponse,
|
|
calculateBackoffDelay,
|
|
createDefaultOutboxPressureManager
|
|
} from '@timesafari/polling-contracts';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
/**
|
|
* Service for managing daily notifications in TimeSafari
|
|
* Supports community building through gifts, gratitude, and collaborative projects
|
|
* Provides privacy-preserving notification delivery with user-controlled visibility
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 2.0.0
|
|
* @since 2025
|
|
*/
|
|
export class DailyNotificationService {
|
|
private static instance: DailyNotificationService;
|
|
private isInitialized = false;
|
|
private callbacks: Map<string, Function> = new Map();
|
|
|
|
private constructor() {}
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
public static getInstance(): DailyNotificationService {
|
|
if (!DailyNotificationService.instance) {
|
|
DailyNotificationService.instance = new DailyNotificationService();
|
|
}
|
|
return DailyNotificationService.instance;
|
|
}
|
|
|
|
/**
|
|
* Initialize the daily notification service
|
|
* Must be called before using any notification features
|
|
*/
|
|
public async initialize(): Promise<void> {
|
|
if (this.isInitialized) {
|
|
logger.debug('[DailyNotificationService] Already initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Request permissions
|
|
const permissionResult = await DailyNotification.requestPermissions();
|
|
logger.debug('[DailyNotificationService] Permission result:', permissionResult);
|
|
|
|
if (!permissionResult.granted) {
|
|
throw new Error('Notification permissions not granted');
|
|
}
|
|
|
|
// Configure the plugin for TimeSafari community features
|
|
await DailyNotification.configure({
|
|
dbPath: 'timesafari_community_notifications.db',
|
|
storage: 'tiered',
|
|
ttlSeconds: 3600,
|
|
prefetchLeadMinutes: 30,
|
|
maxNotificationsPerDay: 5,
|
|
retentionDays: 30
|
|
});
|
|
|
|
// Register default callbacks
|
|
await this.registerDefaultCallbacks();
|
|
|
|
this.isInitialized = true;
|
|
logger.debug('[DailyNotificationService] Successfully initialized');
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Initialization failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule a basic daily notification (backward compatible)
|
|
* @param options Notification options
|
|
*/
|
|
public async scheduleDailyNotification(options: {
|
|
title: string;
|
|
body: string;
|
|
schedule: string; // Cron expression
|
|
url?: string;
|
|
actions?: Array<{ id: string; title: string }>;
|
|
}): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.scheduleDailyNotification({
|
|
title: options.title,
|
|
body: options.body,
|
|
time: this.cronToTime(options.schedule),
|
|
url: options.url,
|
|
sound: true,
|
|
priority: 'high',
|
|
retryCount: 3,
|
|
retryInterval: 5000,
|
|
offlineFallback: true
|
|
});
|
|
|
|
logger.debug('[DailyNotificationService] Daily notification scheduled:', options.title);
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to schedule daily notification:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule dual notification (content fetch + user notification)
|
|
* @param config Dual scheduling configuration
|
|
*/
|
|
public async scheduleDualNotification(config: DualScheduleConfiguration): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.scheduleDualNotification(config);
|
|
logger.debug('[DailyNotificationService] Dual notification scheduled');
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to schedule dual notification:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule content fetching separately
|
|
* @param config Content fetch configuration
|
|
*/
|
|
public async scheduleContentFetch(config: ContentFetchConfig): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.scheduleContentFetch(config);
|
|
logger.debug('[DailyNotificationService] Content fetch scheduled');
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to schedule content fetch:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule user notification separately
|
|
* @param config User notification configuration
|
|
*/
|
|
public async scheduleUserNotification(config: UserNotificationConfig): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.scheduleUserNotification(config);
|
|
logger.debug('[DailyNotificationService] User notification scheduled');
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to schedule user notification:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a callback function
|
|
* @param name Callback name
|
|
* @param callback Callback function
|
|
*/
|
|
public async registerCallback(name: string, callback: Function): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.registerCallback(name, callback);
|
|
this.callbacks.set(name, callback);
|
|
logger.debug('[DailyNotificationService] Callback registered:', name);
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to register callback:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregister a callback function
|
|
* @param name Callback name
|
|
*/
|
|
public async unregisterCallback(name: string): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.unregisterCallback(name);
|
|
this.callbacks.delete(name);
|
|
logger.debug('[DailyNotificationService] Callback unregistered:', name);
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to unregister callback:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get dual schedule status
|
|
*/
|
|
public async getDualScheduleStatus(): Promise<any> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
const status = await DailyNotification.getDualScheduleStatus();
|
|
logger.debug('[DailyNotificationService] Status retrieved:', status);
|
|
return status;
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to get status:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel all notifications
|
|
*/
|
|
public async cancelAllNotifications(): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.cancelDualSchedule();
|
|
logger.debug('[DailyNotificationService] All notifications cancelled');
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to cancel notifications:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get battery status and optimization info
|
|
*/
|
|
public async getBatteryStatus(): Promise<any> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
const batteryStatus = await DailyNotification.getBatteryStatus();
|
|
logger.debug('[DailyNotificationService] Battery status:', batteryStatus);
|
|
return batteryStatus;
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to get battery status:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request battery optimization exemption
|
|
*/
|
|
public async requestBatteryOptimizationExemption(): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
await DailyNotification.requestBatteryOptimizationExemption();
|
|
logger.debug('[DailyNotificationService] Battery optimization exemption requested');
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to request battery exemption:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register default callbacks for TimeSafari notification types
|
|
*/
|
|
private async registerDefaultCallbacks(): Promise<void> {
|
|
// Offers notification callback
|
|
await this.registerCallback('offers', async (event: CallbackEvent) => {
|
|
try {
|
|
await this.handleOffersNotification(event);
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Offers callback failed:', error);
|
|
}
|
|
});
|
|
|
|
// Projects notification callback
|
|
await this.registerCallback('projects', async (event: CallbackEvent) => {
|
|
try {
|
|
await this.handleProjectsNotification(event);
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Projects callback failed:', error);
|
|
}
|
|
});
|
|
|
|
// People notification callback
|
|
await this.registerCallback('people', async (event: CallbackEvent) => {
|
|
try {
|
|
await this.handlePeopleNotification(event);
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] People callback failed:', error);
|
|
}
|
|
});
|
|
|
|
// Items notification callback
|
|
await this.registerCallback('items', async (event: CallbackEvent) => {
|
|
try {
|
|
await this.handleItemsNotification(event);
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Items callback failed:', error);
|
|
}
|
|
});
|
|
|
|
// Community analytics callback
|
|
await this.registerCallback('communityAnalytics', async (event: CallbackEvent) => {
|
|
try {
|
|
// Send community events to analytics service
|
|
await fetch('https://analytics.timesafari.com/community-events', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer your-analytics-token'
|
|
},
|
|
body: JSON.stringify({
|
|
event: 'community_notification',
|
|
data: event,
|
|
timestamp: new Date().toISOString(),
|
|
privacyLevel: 'aggregated' // Respect privacy-preserving architecture
|
|
})
|
|
});
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Community analytics callback failed:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process Endorser.ch notification bundle using parallel API requests
|
|
* @param data Notification bundle data
|
|
*/
|
|
private async processEndorserNotificationBundle(data: any): Promise<void> {
|
|
try {
|
|
const { userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId } = data;
|
|
|
|
// Make parallel requests to Endorser.ch API endpoints
|
|
const requests = [
|
|
// Offers to person
|
|
fetch(`https://endorser.ch/api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}`, {
|
|
headers: { 'Authorization': 'Bearer your-jwt-token' }
|
|
}),
|
|
|
|
// Offers to user's projects
|
|
fetch(`https://endorser.ch/api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}`, {
|
|
headers: { 'Authorization': 'Bearer your-jwt-token' }
|
|
}),
|
|
|
|
// Changes to starred projects
|
|
fetch('https://endorser.ch/api/v2/report/plansLastUpdatedBetween', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer your-jwt-token',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
planIds: starredPlanIds,
|
|
afterId: lastKnownPlanId
|
|
})
|
|
})
|
|
];
|
|
|
|
const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests);
|
|
|
|
const notificationData = {
|
|
offersToPerson: await offersToPerson.json(),
|
|
offersToProjects: await offersToProjects.json(),
|
|
starredChanges: await starredChanges.json()
|
|
};
|
|
|
|
// Process each notification type
|
|
await this.handleOffersNotification(notificationData.offersToPerson);
|
|
await this.handleProjectsNotification(notificationData.starredChanges);
|
|
|
|
logger.debug('[DailyNotificationService] Processed Endorser.ch notification bundle');
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to process Endorser.ch bundle:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle offers notification events from Endorser.ch API
|
|
* @param event Callback event
|
|
*/
|
|
private async handleOffersNotification(event: CallbackEvent): Promise<void> {
|
|
// Handle offers notifications: new/changed offers to me, my projects, favorited projects
|
|
logger.debug('[DailyNotificationService] Handling offers notification:', event);
|
|
|
|
if (event.data && event.data.length > 0) {
|
|
// Process OfferSummaryArrayMaybeMoreBody format
|
|
event.data.forEach((offer: any) => {
|
|
logger.debug('[DailyNotificationService] Processing offer:', {
|
|
jwtId: offer.jwtId,
|
|
handleId: offer.handleId,
|
|
offeredByDid: offer.offeredByDid,
|
|
recipientDid: offer.recipientDid,
|
|
objectDescription: offer.objectDescription
|
|
});
|
|
});
|
|
|
|
// Check if there are more offers to fetch
|
|
if (event.hitLimit) {
|
|
const lastOffer = event.data[event.data.length - 1];
|
|
logger.debug('[DailyNotificationService] More offers available, last JWT ID:', lastOffer.jwtId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle projects notification events from Endorser.ch API
|
|
* @param event Callback event
|
|
*/
|
|
private async handleProjectsNotification(event: CallbackEvent): Promise<void> {
|
|
// Handle projects notifications: local new/changed, content of interest, favorited
|
|
logger.debug('[DailyNotificationService] Handling projects notification:', event);
|
|
|
|
if (event.data && event.data.length > 0) {
|
|
// Process PlanSummaryAndPreviousClaimArrayMaybeMore format
|
|
event.data.forEach((planData: any) => {
|
|
const { plan, wrappedClaimBefore } = planData;
|
|
logger.debug('[DailyNotificationService] Processing project change:', {
|
|
jwtId: plan.jwtId,
|
|
handleId: plan.handleId,
|
|
name: plan.name,
|
|
issuerDid: plan.issuerDid,
|
|
hasPreviousClaim: !!wrappedClaimBefore
|
|
});
|
|
});
|
|
|
|
// Check if there are more project changes to fetch
|
|
if (event.hitLimit) {
|
|
const lastPlan = event.data[event.data.length - 1];
|
|
logger.debug('[DailyNotificationService] More project changes available, last JWT ID:', lastPlan.plan.jwtId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle people notification events
|
|
* @param event Callback event
|
|
*/
|
|
private async handlePeopleNotification(event: CallbackEvent): Promise<void> {
|
|
// Handle people notifications: local new/changed, content of interest, favorited, contacts
|
|
logger.debug('[DailyNotificationService] Handling people notification:', event);
|
|
// Implementation would process people data and update local state
|
|
}
|
|
|
|
/**
|
|
* Handle items notification events
|
|
* @param event Callback event
|
|
*/
|
|
private async handleItemsNotification(event: CallbackEvent): Promise<void> {
|
|
// Handle items notifications: local new/changed, favorited
|
|
logger.debug('[DailyNotificationService] Handling items notification:', event);
|
|
// Implementation would process items data and update local state
|
|
}
|
|
|
|
/**
|
|
* Update trust network with notification events
|
|
* @param event Callback event
|
|
*/
|
|
private async updateTrustNetwork(event: CallbackEvent): Promise<void> {
|
|
// Implement trust network update logic here
|
|
// This would integrate with TimeSafari's DID-based trust system
|
|
logger.debug('[DailyNotificationService] Updating trust network:', event);
|
|
}
|
|
|
|
/**
|
|
* Handle privacy-preserving notification delivery
|
|
* @param event Callback event
|
|
*/
|
|
private async handlePrivacyPreservingNotification(event: CallbackEvent): Promise<void> {
|
|
// Implement privacy-preserving notification logic here
|
|
// This would respect user-controlled visibility settings
|
|
logger.debug('[DailyNotificationService] Handling privacy-preserving notification:', event);
|
|
}
|
|
|
|
/**
|
|
* Save notification event to database
|
|
* @param event Callback event
|
|
*/
|
|
private async saveToDatabase(event: CallbackEvent): Promise<void> {
|
|
// Implement your database save logic here
|
|
logger.debug('[DailyNotificationService] Saving to database:', event);
|
|
}
|
|
|
|
/**
|
|
* Convert cron expression to time string
|
|
* @param cron Cron expression (e.g., "0 9 * * *")
|
|
*/
|
|
private cronToTime(cron: string): string {
|
|
const parts = cron.split(' ');
|
|
if (parts.length >= 2) {
|
|
const hour = parts[1].padStart(2, '0');
|
|
const minute = parts[0].padStart(2, '0');
|
|
return `${hour}:${minute}`;
|
|
}
|
|
return '09:00'; // Default to 9 AM
|
|
}
|
|
|
|
/**
|
|
* Check if the service is initialized
|
|
*/
|
|
public isServiceInitialized(): boolean {
|
|
return this.isInitialized;
|
|
}
|
|
|
|
/**
|
|
* Get service version
|
|
*/
|
|
public getVersion(): string {
|
|
return '2.0.0';
|
|
}
|
|
|
|
/**
|
|
* Setup generic polling for starred projects
|
|
* @param starredProjectIds Array of starred project IDs
|
|
* @param currentWatermark Current watermark JWT ID
|
|
*/
|
|
public async setupStarredProjectsPolling(
|
|
starredProjectIds: string[],
|
|
currentWatermark?: string
|
|
): Promise<string> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
// Create the polling request
|
|
const starredProjectsRequest: GenericPollingRequest<StarredProjectsRequest, StarredProjectsResponse> = {
|
|
endpoint: '/api/v2/report/plansLastUpdatedBetween',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0',
|
|
'Authorization': `Bearer ${await this.getJwtToken()}`
|
|
},
|
|
body: {
|
|
planIds: starredProjectIds,
|
|
afterId: currentWatermark,
|
|
limit: 100
|
|
},
|
|
responseSchema: {
|
|
validate: (data: any): data is StarredProjectsResponse => {
|
|
return data &&
|
|
Array.isArray(data.data) &&
|
|
typeof data.hitLimit === 'boolean' &&
|
|
data.pagination &&
|
|
typeof data.pagination.hasMore === 'boolean';
|
|
},
|
|
transformError: (error: any) => ({
|
|
code: 'VALIDATION_ERROR',
|
|
message: error.message || 'Validation failed',
|
|
retryable: false
|
|
})
|
|
},
|
|
retryConfig: {
|
|
maxAttempts: 3,
|
|
backoffStrategy: 'exponential',
|
|
baseDelayMs: 1000
|
|
},
|
|
timeoutMs: 30000
|
|
};
|
|
|
|
// Create the schedule configuration
|
|
const scheduleConfig: PollingScheduleConfig<StarredProjectsRequest, StarredProjectsResponse> = {
|
|
request: starredProjectsRequest,
|
|
schedule: {
|
|
cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily
|
|
timezone: 'UTC',
|
|
maxConcurrentPolls: 1
|
|
},
|
|
notificationConfig: {
|
|
enabled: true,
|
|
templates: {
|
|
singleUpdate: '{projectName} has been updated',
|
|
multipleUpdates: 'You have {count} new updates in your starred projects'
|
|
},
|
|
groupingRules: {
|
|
maxGroupSize: 5,
|
|
timeWindowMinutes: 5
|
|
}
|
|
},
|
|
stateConfig: {
|
|
watermarkKey: 'lastAckedStarredPlanChangesJwtId',
|
|
storageAdapter: 'timesafari'
|
|
}
|
|
};
|
|
|
|
// Schedule the polling
|
|
const scheduleId = await DailyNotification.schedulePoll(scheduleConfig);
|
|
|
|
logger.debug('[DailyNotificationService] Starred projects polling scheduled:', scheduleId);
|
|
return scheduleId;
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to setup starred projects polling:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle polling results
|
|
* @param result Polling result
|
|
*/
|
|
public async handlePollingResult(result: PollingResult<StarredProjectsResponse>): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('DailyNotificationService not initialized');
|
|
}
|
|
|
|
try {
|
|
if (result.success && result.data) {
|
|
const changes = result.data.data;
|
|
|
|
if (changes.length > 0) {
|
|
// Generate notifications
|
|
await this.generateNotifications(changes);
|
|
|
|
// Update watermark with CAS
|
|
const latestJwtId = changes[changes.length - 1].planSummary.jwtId;
|
|
await this.updateWatermark(latestJwtId);
|
|
|
|
// Acknowledge changes with server
|
|
await this.acknowledgeChanges(changes.map(c => c.planSummary.jwtId));
|
|
|
|
logger.debug('[DailyNotificationService] Processed polling result:', {
|
|
changeCount: changes.length,
|
|
latestJwtId
|
|
});
|
|
}
|
|
} else if (result.error) {
|
|
logger.error('[DailyNotificationService] Polling failed:', result.error);
|
|
// Handle error (retry, notify user, etc.)
|
|
await this.handlePollingError(result.error);
|
|
}
|
|
} catch (error) {
|
|
logger.error('[DailyNotificationService] Failed to handle polling result:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get JWT token for authentication
|
|
*/
|
|
private async getJwtToken(): Promise<string> {
|
|
// Implementation would get JWT token from TimeSafari's auth system
|
|
return 'your-jwt-token';
|
|
}
|
|
|
|
/**
|
|
* Generate notifications from polling results
|
|
*/
|
|
private async generateNotifications(changes: any[]): Promise<void> {
|
|
// Implementation would generate notifications based on changes
|
|
logger.debug('[DailyNotificationService] Generating notifications for changes:', changes.length);
|
|
}
|
|
|
|
/**
|
|
* Update watermark with compare-and-swap
|
|
*/
|
|
private async updateWatermark(jwtId: string): Promise<void> {
|
|
// Implementation would update watermark using CAS
|
|
logger.debug('[DailyNotificationService] Updating watermark:', jwtId);
|
|
}
|
|
|
|
/**
|
|
* Acknowledge changes with server
|
|
*/
|
|
private async acknowledgeChanges(jwtIds: string[]): Promise<void> {
|
|
// Implementation would acknowledge changes with server
|
|
logger.debug('[DailyNotificationService] Acknowledging changes:', jwtIds.length);
|
|
}
|
|
|
|
/**
|
|
* Handle polling errors
|
|
*/
|
|
private async handlePollingError(error: any): Promise<void> {
|
|
// Implementation would handle polling errors
|
|
logger.error('[DailyNotificationService] Handling polling error:', error);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5.2 Add to PlatformServiceMixin
|
|
|
|
Update `src/utils/PlatformServiceMixin.ts` to include notification methods:
|
|
|
|
```typescript
|
|
import { DailyNotificationService } from '@/services/DailyNotificationService';
|
|
|
|
// Add to the mixin object
|
|
export const PlatformServiceMixin = {
|
|
// ... existing methods
|
|
|
|
/**
|
|
* Schedule a daily notification
|
|
* @param options Notification options
|
|
*/
|
|
async $scheduleDailyNotification(options: {
|
|
title: string;
|
|
body: string;
|
|
schedule: string;
|
|
url?: string;
|
|
actions?: Array<{ id: string; title: string }>;
|
|
}): Promise<void> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.scheduleDailyNotification(options);
|
|
},
|
|
|
|
/**
|
|
* Schedule dual notification (content fetch + user notification)
|
|
* @param config Dual scheduling configuration
|
|
*/
|
|
async $scheduleDualNotification(config: any): Promise<void> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.scheduleDualNotification(config);
|
|
},
|
|
|
|
/**
|
|
* Register a notification callback
|
|
* @param name Callback name
|
|
* @param callback Callback function
|
|
*/
|
|
async $registerNotificationCallback(name: string, callback: Function): Promise<void> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.registerCallback(name, callback);
|
|
},
|
|
|
|
/**
|
|
* Get notification status
|
|
*/
|
|
async $getNotificationStatus(): Promise<any> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.getDualScheduleStatus();
|
|
},
|
|
|
|
/**
|
|
* Cancel all notifications
|
|
*/
|
|
async $cancelAllNotifications(): Promise<void> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.cancelAllNotifications();
|
|
},
|
|
|
|
/**
|
|
* Get battery status
|
|
*/
|
|
async $getBatteryStatus(): Promise<any> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.getBatteryStatus();
|
|
},
|
|
|
|
/**
|
|
* Request battery optimization exemption
|
|
*/
|
|
async $requestBatteryOptimizationExemption(): Promise<void> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.requestBatteryOptimizationExemption();
|
|
},
|
|
|
|
/**
|
|
* Setup generic polling for starred projects
|
|
* @param starredProjectIds Array of starred project IDs
|
|
* @param currentWatermark Current watermark JWT ID
|
|
*/
|
|
async $setupStarredProjectsPolling(starredProjectIds: string[], currentWatermark?: string): Promise<string> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.setupStarredProjectsPolling(starredProjectIds, currentWatermark);
|
|
},
|
|
|
|
/**
|
|
* Handle polling results
|
|
* @param result Polling result
|
|
*/
|
|
async $handlePollingResult(result: any): Promise<void> {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
return await notificationService.handlePollingResult(result);
|
|
},
|
|
|
|
// ... rest of existing methods
|
|
};
|
|
```
|
|
|
|
#### 5.3 Update TypeScript Declarations
|
|
|
|
Add to the Vue module declaration in `src/utils/PlatformServiceMixin.ts`:
|
|
|
|
```typescript
|
|
declare module "@vue/runtime-core" {
|
|
interface ComponentCustomProperties {
|
|
// ... existing methods
|
|
|
|
// Daily Notification methods
|
|
$scheduleDailyNotification(options: {
|
|
title: string;
|
|
body: string;
|
|
schedule: string;
|
|
url?: string;
|
|
actions?: Array<{ id: string; title: string }>;
|
|
}): Promise<void>;
|
|
$scheduleDualNotification(config: any): Promise<void>;
|
|
$registerNotificationCallback(name: string, callback: Function): Promise<void>;
|
|
$getNotificationStatus(): Promise<any>;
|
|
$cancelAllNotifications(): Promise<void>;
|
|
$getBatteryStatus(): Promise<any>;
|
|
$requestBatteryOptimizationExemption(): Promise<void>;
|
|
$setupStarredProjectsPolling(starredProjectIds: string[], currentWatermark?: string): Promise<string>;
|
|
$handlePollingResult(result: any): Promise<void>;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Initialization in App
|
|
|
|
#### 6.1 Initialize in Main App Component
|
|
|
|
Update your main app component (e.g., `src/App.vue` or `src/main.ts`) to initialize the notification service:
|
|
|
|
```typescript
|
|
import { DailyNotificationService } from '@/services/DailyNotificationService';
|
|
|
|
// In your app initialization
|
|
async function initializeApp() {
|
|
try {
|
|
// Initialize other services first
|
|
await initializeDatabase();
|
|
await initializePlatformService();
|
|
|
|
// Initialize daily notifications
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
await notificationService.initialize();
|
|
|
|
logger.debug('[App] All services initialized successfully');
|
|
} catch (error) {
|
|
logger.error('[App] Failed to initialize services:', error);
|
|
// Handle initialization error
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 6.2 Initialize in Platform Service
|
|
|
|
Alternatively, initialize in your platform service startup:
|
|
|
|
```typescript
|
|
// In src/services/platforms/CapacitorPlatformService.ts
|
|
import { DailyNotificationService } from '@/services/DailyNotificationService';
|
|
|
|
export class CapacitorPlatformService implements PlatformService {
|
|
// ... existing methods
|
|
|
|
private async initializeDatabase(): Promise<void> {
|
|
// ... existing database initialization
|
|
|
|
// Initialize daily notifications after database is ready
|
|
try {
|
|
const notificationService = DailyNotificationService.getInstance();
|
|
await notificationService.initialize();
|
|
logger.debug('[CapacitorPlatformService] Daily notifications initialized');
|
|
} catch (error) {
|
|
logger.warn('[CapacitorPlatformService] Failed to initialize daily notifications:', error);
|
|
// Don't fail the entire initialization for notification errors
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7. Usage Examples
|
|
|
|
#### 7.1 Generic Polling for Starred Projects
|
|
|
|
```typescript
|
|
// In a Vue component
|
|
export default {
|
|
data() {
|
|
return {
|
|
starredProjects: [],
|
|
currentWatermark: null,
|
|
pollingScheduleId: null
|
|
};
|
|
},
|
|
|
|
async mounted() {
|
|
await this.initializePolling();
|
|
},
|
|
|
|
methods: {
|
|
async initializePolling() {
|
|
try {
|
|
// Get user's starred projects
|
|
this.starredProjects = await this.getUserStarredProjects();
|
|
|
|
// Get current watermark
|
|
this.currentWatermark = await this.getCurrentWatermark();
|
|
|
|
// Setup polling
|
|
this.pollingScheduleId = await this.$setupStarredProjectsPolling(
|
|
this.starredProjects,
|
|
this.currentWatermark
|
|
);
|
|
|
|
this.$notify('Starred projects polling initialized successfully');
|
|
} catch (error) {
|
|
this.$notify('Failed to initialize polling: ' + error.message);
|
|
}
|
|
},
|
|
|
|
async getUserStarredProjects() {
|
|
// Implementation would get starred projects from TimeSafari's database
|
|
return ['project-1', 'project-2', 'project-3'];
|
|
},
|
|
|
|
async getCurrentWatermark() {
|
|
// Implementation would get current watermark from storage
|
|
return '1704067200_abc123_12345678';
|
|
},
|
|
|
|
async handlePollingResult(result) {
|
|
try {
|
|
await this.$handlePollingResult(result);
|
|
|
|
if (result.success && result.data && result.data.data.length > 0) {
|
|
this.$notify(`Received ${result.data.data.length} project updates`);
|
|
}
|
|
} catch (error) {
|
|
this.$notify('Failed to handle polling result: ' + error.message);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
#### 7.2 Community Update Notification
|
|
|
|
```typescript
|
|
// In a Vue component
|
|
export default {
|
|
methods: {
|
|
async scheduleCommunityUpdate() {
|
|
try {
|
|
await this.$scheduleDailyNotification({
|
|
title: 'TimeSafari Community Update',
|
|
body: 'New offers, projects, people, and items await your attention!',
|
|
schedule: '0 9 * * *', // 9 AM daily
|
|
url: 'https://timesafari.com/notifications/bundle',
|
|
actions: [
|
|
{ id: 'view_offers', title: 'View Offers' },
|
|
{ id: 'view_projects', title: 'See Projects' },
|
|
{ id: 'view_people', title: 'Check People' },
|
|
{ id: 'view_items', title: 'Browse Items' },
|
|
{ id: 'dismiss', title: 'Dismiss' }
|
|
]
|
|
});
|
|
|
|
this.$notify('Community update notification scheduled successfully');
|
|
} catch (error) {
|
|
this.$notify('Failed to schedule community update: ' + error.message);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
#### 7.2 Community Content Fetch + Notification
|
|
|
|
```typescript
|
|
async scheduleCommunityContentFetch() {
|
|
try {
|
|
const config = {
|
|
contentFetch: {
|
|
enabled: true,
|
|
schedule: '0 8 * * *', // Fetch community content at 8 AM
|
|
url: 'https://endorser.ch/api/v2/report/notifications/bundle', // Single route for all notification types
|
|
headers: {
|
|
'Authorization': 'Bearer your-jwt-token',
|
|
'Content-Type': 'application/json',
|
|
'X-Privacy-Level': 'user-controlled'
|
|
},
|
|
ttlSeconds: 3600, // 1 hour TTL for community data
|
|
timeout: 30000,
|
|
retryAttempts: 3,
|
|
retryDelay: 5000,
|
|
callbacks: {
|
|
onSuccess: async (data) => {
|
|
console.log('Community notifications fetched successfully:', data);
|
|
// Process bundled notifications using Endorser.ch API patterns
|
|
await this.processEndorserNotificationBundle(data);
|
|
},
|
|
onError: async (error) => {
|
|
console.error('Community content fetch failed:', error);
|
|
}
|
|
}
|
|
},
|
|
userNotification: {
|
|
enabled: true,
|
|
schedule: '0 9 * * *', // Notify at 9 AM
|
|
title: 'TimeSafari Community Update Ready',
|
|
body: 'New offers, projects, people, and items are available!',
|
|
sound: true,
|
|
vibration: true,
|
|
priority: 'high',
|
|
actions: [
|
|
{ id: 'view_offers', title: 'View Offers' },
|
|
{ id: 'view_projects', title: 'See Projects' },
|
|
{ id: 'view_people', title: 'Check People' },
|
|
{ id: 'view_items', title: 'Browse Items' },
|
|
{ id: 'dismiss', title: 'Dismiss' }
|
|
]
|
|
},
|
|
relationship: {
|
|
autoLink: true,
|
|
contentTimeout: 300000, // 5 minutes
|
|
fallbackBehavior: 'show_default'
|
|
}
|
|
};
|
|
|
|
await this.$scheduleDualNotification(config);
|
|
this.$notify('Community content fetch scheduled successfully');
|
|
} catch (error) {
|
|
this.$notify('Failed to schedule community content fetch: ' + error.message);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 7.3 Endorser.ch API Integration
|
|
|
|
```typescript
|
|
async integrateWithEndorserAPI() {
|
|
try {
|
|
// Register offers callback using Endorser.ch API endpoints
|
|
await this.$registerNotificationCallback('offers', async (event) => {
|
|
try {
|
|
// Handle offers notifications using Endorser.ch API patterns
|
|
const { userDid, lastKnownOfferId } = event;
|
|
|
|
// Fetch offers to person
|
|
const offersToPerson = await fetch(`https://endorser.ch/api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}`, {
|
|
headers: { 'Authorization': 'Bearer your-jwt-token' }
|
|
});
|
|
|
|
// Fetch offers to user's projects
|
|
const offersToProjects = await fetch(`https://endorser.ch/api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}`, {
|
|
headers: { 'Authorization': 'Bearer your-jwt-token' }
|
|
});
|
|
|
|
const [offersToPersonData, offersToProjectsData] = await Promise.all([
|
|
offersToPerson.json(),
|
|
offersToProjects.json()
|
|
]);
|
|
|
|
// Process OfferSummaryArrayMaybeMoreBody format
|
|
const allOffers = [...offersToPersonData.data, ...offersToProjectsData.data];
|
|
|
|
console.log('Processed offers:', allOffers.map(offer => ({
|
|
jwtId: offer.jwtId,
|
|
handleId: offer.handleId,
|
|
offeredByDid: offer.offeredByDid,
|
|
objectDescription: offer.objectDescription
|
|
})));
|
|
|
|
} catch (error) {
|
|
console.error('Offers callback failed:', error);
|
|
}
|
|
});
|
|
|
|
// Register projects callback using Endorser.ch API endpoints
|
|
await this.$registerNotificationCallback('projects', async (event) => {
|
|
try {
|
|
// Handle projects notifications using Endorser.ch API patterns
|
|
const { starredPlanIds, lastKnownPlanId } = event;
|
|
|
|
// Fetch changes to starred projects
|
|
const starredChanges = await fetch('https://endorser.ch/api/v2/report/plansLastUpdatedBetween', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer your-jwt-token',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
planIds: starredPlanIds,
|
|
afterId: lastKnownPlanId
|
|
})
|
|
});
|
|
|
|
const starredChangesData = await starredChanges.json();
|
|
|
|
// Process PlanSummaryAndPreviousClaimArrayMaybeMore format
|
|
console.log('Processed project changes:', starredChangesData.data.map(planData => ({
|
|
jwtId: planData.plan.jwtId,
|
|
handleId: planData.plan.handleId,
|
|
name: planData.plan.name,
|
|
issuerDid: planData.plan.issuerDid,
|
|
hasPreviousClaim: !!planData.wrappedClaimBefore
|
|
})));
|
|
|
|
} catch (error) {
|
|
console.error('Projects callback failed:', error);
|
|
}
|
|
});
|
|
|
|
this.$notify('Endorser.ch API integration registered successfully');
|
|
} catch (error) {
|
|
this.$notify('Failed to register Endorser.ch API integration: ' + error.message);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 7.4 Battery Optimization Management
|
|
|
|
```typescript
|
|
async checkBatteryOptimization() {
|
|
try {
|
|
const batteryStatus = await this.$getBatteryStatus();
|
|
|
|
if (!batteryStatus.isOptimizationExempt) {
|
|
// Request exemption from battery optimization
|
|
await this.$requestBatteryOptimizationExemption();
|
|
this.$notify('Battery optimization exemption requested');
|
|
} else {
|
|
this.$notify('App is already exempt from battery optimization');
|
|
}
|
|
} catch (error) {
|
|
this.$notify('Failed to check battery optimization: ' + error.message);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8. Endorser.ch API Integration Patterns
|
|
|
|
The TimeSafari Daily Notification Plugin integrates with the Endorser.ch API to fetch community activity using pagination-based filtering. The API provides several endpoints for retrieving "new" or recent activity using `afterId` and `beforeId` parameters.
|
|
|
|
#### 8.1 Core Pagination Pattern
|
|
|
|
All Endorser.ch "new" activity endpoints use the same pagination pattern:
|
|
- **`afterId`**: JWT ID of the entry after which to look (exclusive) - gets newer entries
|
|
- **`beforeId`**: JWT ID of the entry before which to look (exclusive) - gets older entries
|
|
- **Results**: Returned in reverse chronological order (newest first)
|
|
- **Response Format**: `{ data: [...], hitLimit: boolean }`
|
|
|
|
#### 8.2 Key Endpoints
|
|
|
|
**Offers to Person**
|
|
```typescript
|
|
GET /api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}
|
|
```
|
|
|
|
**Offers to User's Projects**
|
|
```typescript
|
|
GET /api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}
|
|
```
|
|
|
|
**Changes to Starred Projects**
|
|
```typescript
|
|
POST /api/v2/report/plansLastUpdatedBetween
|
|
{
|
|
"planIds": ["plan-123", "plan-456"],
|
|
"afterId": "01HSE3R9MAC0FT3P3KZ382TWV7"
|
|
}
|
|
```
|
|
|
|
#### 8.3 Parallel Requests Implementation
|
|
|
|
```typescript
|
|
async function getNewActivity(userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId) {
|
|
const requests = [
|
|
// Offers to person
|
|
fetch(`https://endorser.ch/api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}`, {
|
|
headers: { 'Authorization': 'Bearer your-jwt-token' }
|
|
}),
|
|
|
|
// Offers to user's projects
|
|
fetch(`https://endorser.ch/api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}`, {
|
|
headers: { 'Authorization': 'Bearer your-jwt-token' }
|
|
}),
|
|
|
|
// Changes to starred projects
|
|
fetch('https://endorser.ch/api/v2/report/plansLastUpdatedBetween', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer your-jwt-token',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
planIds: starredPlanIds,
|
|
afterId: lastKnownPlanId
|
|
})
|
|
})
|
|
];
|
|
|
|
const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests);
|
|
|
|
return {
|
|
offersToPerson: await offersToPerson.json(),
|
|
offersToProjects: await offersToProjects.json(),
|
|
starredChanges: await starredChanges.json()
|
|
};
|
|
}
|
|
```
|
|
|
|
#### 8.4 Pagination Handling
|
|
|
|
```typescript
|
|
function handlePagination(response) {
|
|
if (response.hitLimit) {
|
|
// There may be more data - use the last item's jwtId as afterId for next request
|
|
const lastItem = response.data[response.data.length - 1];
|
|
return {
|
|
hasMore: true,
|
|
nextAfterId: lastItem.jwtId
|
|
};
|
|
}
|
|
return { hasMore: false };
|
|
}
|
|
```
|
|
|
|
### 9. Build and Sync
|
|
|
|
After making all changes, run the following commands:
|
|
|
|
```bash
|
|
# Install dependencies
|
|
npm install
|
|
|
|
# Build the Capacitor app
|
|
npm run build:capacitor
|
|
|
|
# Sync with native platforms
|
|
npx cap sync
|
|
|
|
# For iOS, update pods
|
|
cd ios/App && pod install && cd ../..
|
|
|
|
# For Android, clean and rebuild
|
|
cd android && ./gradlew clean && cd ..
|
|
```
|
|
|
|
### 10. Testing
|
|
|
|
#### 10.1 Test on Android
|
|
|
|
```bash
|
|
# Build and run on Android
|
|
npm run build:android
|
|
npx cap run android
|
|
```
|
|
|
|
#### 10.2 Test on iOS
|
|
|
|
```bash
|
|
# Build and run on iOS
|
|
npm run build:ios
|
|
npx cap run ios
|
|
```
|
|
|
|
#### 10.3 Test on Electron
|
|
|
|
```bash
|
|
# Build and run on Electron
|
|
npm run build:electron
|
|
npm run electron:serve
|
|
```
|
|
|
|
### 11. Troubleshooting
|
|
|
|
#### 11.1 Common Issues
|
|
|
|
1. **Plugin not found**: Ensure the plugin is properly installed and the path is correct
|
|
2. **Permissions denied**: Check that all required permissions are added to manifests
|
|
3. **Build errors**: Clean and rebuild the project after adding the plugin
|
|
4. **TypeScript errors**: Ensure the plugin exports proper TypeScript definitions
|
|
5. **Background tasks not running**: Check battery optimization settings and background app refresh
|
|
6. **Endorser.ch API errors**: Verify JWT token authentication and endpoint availability
|
|
|
|
#### 11.2 Debug Steps
|
|
|
|
1. Check console logs for initialization errors
|
|
2. Verify plugin is loaded in `capacitor.plugins.json`
|
|
3. Test permissions manually in device settings
|
|
4. Use Electron dev tools for desktop platform testing
|
|
5. Check WorkManager logs on Android
|
|
6. Check BGTaskScheduler logs on iOS
|
|
7. Verify Endorser.ch API responses and pagination handling
|
|
|
|
#### 11.3 Platform-Specific Issues
|
|
|
|
**Android:**
|
|
- Ensure WorkManager is properly configured
|
|
- Check battery optimization settings
|
|
- Verify exact alarm permissions
|
|
- Check Room database initialization
|
|
|
|
**iOS:**
|
|
- Verify background modes are enabled
|
|
- Check BGTaskScheduler identifiers
|
|
- Ensure Core Data model is compatible
|
|
- Verify notification permissions
|
|
|
|
**Electron:**
|
|
- Ensure Electron main process is configured
|
|
- Check desktop notification permissions
|
|
- Verify SQLite/LocalStorage compatibility
|
|
- Check native notification setup
|
|
|
|
**Endorser.ch API:**
|
|
- Verify JWT token authentication
|
|
- Check pagination parameters (afterId, beforeId)
|
|
- Monitor rate limiting and hitLimit responses
|
|
- Ensure proper error handling for API failures
|
|
|
|
### 12. Security Considerations
|
|
|
|
- Ensure notification data doesn't contain sensitive personal information
|
|
- Validate all notification inputs and callback URLs
|
|
- Implement proper error handling and logging
|
|
- Respect user privacy preferences and visibility settings
|
|
- Follow platform-specific notification guidelines
|
|
- Use HTTPS for all network operations
|
|
- Implement proper authentication for callbacks
|
|
- Respect TimeSafari's privacy-preserving claims architecture
|
|
- Ensure user-controlled visibility for all notification data
|
|
- Use cryptographic verification for sensitive notification content
|
|
|
|
### 13. Performance Considerations
|
|
|
|
- Limit the number of scheduled notifications
|
|
- Clean up old notifications regularly
|
|
- Use efficient notification IDs
|
|
- Consider battery impact on mobile devices
|
|
- Implement proper caching strategies
|
|
- Use circuit breaker patterns for callbacks
|
|
- Monitor memory usage and database performance
|
|
- Implement efficient Endorser.ch API pagination handling
|
|
- Cache JWT tokens and API responses appropriately
|
|
- Monitor API rate limits and implement backoff strategies
|
|
|
|
### 14. Enterprise Integration Examples
|
|
|
|
#### 14.1 Community Analytics Integration
|
|
|
|
```typescript
|
|
// Register community analytics callback
|
|
await this.$registerNotificationCallback('communityAnalytics', async (event) => {
|
|
try {
|
|
// Send community events to analytics service
|
|
await fetch('https://analytics.timesafari.com/community-events', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Privacy-Level': 'aggregated'
|
|
},
|
|
body: JSON.stringify({
|
|
client_id: 'your-client-id',
|
|
events: [{
|
|
name: 'community_notification',
|
|
params: {
|
|
notification_id: event.id,
|
|
action: event.action,
|
|
timestamp: event.timestamp,
|
|
community_type: event.communityType,
|
|
privacy_level: 'aggregated'
|
|
}
|
|
}]
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('Community analytics callback failed:', error);
|
|
}
|
|
});
|
|
```
|
|
|
|
#### 14.2 Trust Network Integration
|
|
|
|
```typescript
|
|
// Register trust network callback
|
|
await this.$registerNotificationCallback('trustNetwork', async (event) => {
|
|
try {
|
|
await fetch('https://api.timesafari.com/trust-network/events', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer your-trust-token',
|
|
'Content-Type': 'application/json',
|
|
'X-Privacy-Level': 'user-controlled'
|
|
},
|
|
body: JSON.stringify({
|
|
Name: event.id,
|
|
Action__c: event.action,
|
|
Timestamp__c: new Date(event.timestamp).toISOString(),
|
|
UserDid__c: event.userDid,
|
|
TrustLevel__c: event.trustLevel,
|
|
Data__c: JSON.stringify(event.data)
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('Trust network callback failed:', error);
|
|
}
|
|
});
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
This guide provides a comprehensive approach to integrating the TimeSafari Daily Notification Plugin into the TimeSafari application. The integration supports TimeSafari's core mission of fostering community building through gifts, gratitude, and collaborative projects.
|
|
|
|
The plugin offers advanced features specifically designed for community engagement:
|
|
- **Dual Scheduling**: Separate content fetch and user notification scheduling for community updates
|
|
- **TTL-at-Fire Logic**: Content validity checking at notification time for community data
|
|
- **Circuit Breaker Pattern**: Automatic failure detection and recovery for community services
|
|
- **Privacy-Preserving Architecture**: Respects TimeSafari's user-controlled visibility and DID-based identity system
|
|
- **Trust Network Integration**: Supports building and maintaining trust networks through notifications
|
|
- **Comprehensive Observability**: Structured logging and health monitoring for community features
|
|
|
|
The integration follows TimeSafari's development principles:
|
|
- **Platform Services**: Uses abstracted platform services via interfaces
|
|
- **Type Safety**: Implements strict TypeScript with type guards
|
|
- **Modern Architecture**: Follows current platform service patterns
|
|
- **Privacy-First**: Respects privacy-preserving claims architecture
|
|
- **Community-Focused**: Supports community building and trust network development
|
|
|
|
For questions or issues, refer to the plugin's documentation or contact the TimeSafari development team.
|
|
|
|
---
|
|
|
|
**Version**: 2.1.0
|
|
**Last Updated**: 2025-10-07 04:32:12 UTC
|
|
**Status**: Production Ready
|
|
**Author**: Matthew Raymer
|