# TimeSafari Daily Notification Plugin Integration Guide
**Author**: Matthew Raymer
**Version**: 2.0.0
**Created**: 2025-01-27 12:00:00 UTC
**Last Updated**: 2025-01-27 12:00:00 UTC
## Overview
This document provides comprehensive step-by-step instructions for integrating the TimeSafari Daily Notification Plugin into the TimeSafari application. TimeSafari is designed to foster community building through gifts, gratitude, and collaborative projects, making it easy for users to recognize contributions, build trust networks, and organize collective action.
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 Web (PWA), Mobile (Capacitor), and Desktop (Electron) platforms.
## 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 this structure:
```
daily-notification-plugin/
├── android/
│ ├── build.gradle
│ ├── src/main/java/com/timesafari/dailynotification/
│ │ ├── DailyNotificationPlugin.java
│ │ ├── NotificationWorker.java
│ │ ├── DatabaseManager.java
│ │ └── CallbackRegistry.java
│ └── 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/
│ ├── index.ts
│ ├── service-worker-manager.ts
│ └── sw.ts
├── dist/
│ ├── plugin.js
│ ├── esm/
│ └── web/
├── package.json
├── capacitor.config.ts
└── README.md
```
## Integration Steps
### 1. Install Plugin from Git Repository
Add the plugin to your `package.json` dependencies:
```json
{
"dependencies": {
"@timesafari/daily-notification-plugin": "git+https://github.com/timesafari/daily-notification-plugin.git#main"
}
}
```
Or install directly via npm:
```bash
npm install git+https://github.com/timesafari/daily-notification-plugin.git#main
```
### 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',
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 for TimeSafari community features
DailyNotification: {
// Plugin-specific configuration
defaultChannel: 'timesafari_community',
enableSound: true,
enableVibration: true,
enableLights: true,
priority: 'high',
// Dual scheduling configuration for community updates
contentFetch: {
enabled: true,
schedule: '0 8 * * *', // 8 AM daily - fetch community updates
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, // 30 second timeout
retryAttempts: 3,
retryDelay: 5000
},
userNotification: {
enabled: true,
schedule: '0 9 * * *', // 9 AM daily - notify users of community updates
title: 'TimeSafari Community Update',
body: 'New offers, projects, people, and items await your attention!',
sound: true,
vibration: true,
priority: 'high'
},
// Callback configuration for community features
callbacks: {
offers: {
enabled: true,
localHandler: 'handleOffersNotification'
},
projects: {
enabled: true,
localHandler: 'handleProjectsNotification'
},
people: {
enabled: true,
localHandler: 'handlePeopleNotification'
},
items: {
enabled: true,
localHandler: 'handleItemsNotification'
},
communityAnalytics: {
enabled: true,
endpoint: 'https://analytics.timesafari.com/community-events',
headers: {
'Authorization': 'Bearer your-analytics-token',
'Content-Type': 'application/json'
}
}
},
// Observability configuration
observability: {
enableLogging: true,
logLevel: 'debug',
enableMetrics: true,
enableHealthChecks: true
}
}
},
// ... 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
```
### 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
UIBackgroundModes
background-app-refresh
background-processing
background-fetch
BGTaskSchedulerPermittedIdentifiers
com.timesafari.dailynotification.content-fetch
com.timesafari.dailynotification.notification-delivery
NSUserNotificationsUsageDescription
TimeSafari needs permission to send you notifications about important updates and reminders.
NSBackgroundTasksUsageDescription
TimeSafari uses background processing to fetch and deliver daily notifications.
```
#### 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 { 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 = 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
// 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 {
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 {
// 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 {
// 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 {
// 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 {
// 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 {
// 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 {
// 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 {
// 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';
}
}
```
#### 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 {
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 {
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 {
const notificationService = DailyNotificationService.getInstance();
return await notificationService.registerCallback(name, callback);
},
/**
* Get notification status
*/
async $getNotificationStatus(): Promise {
const notificationService = DailyNotificationService.getInstance();
return await notificationService.getDualScheduleStatus();
},
/**
* Cancel all notifications
*/
async $cancelAllNotifications(): Promise {
const notificationService = DailyNotificationService.getInstance();
return await notificationService.cancelAllNotifications();
},
/**
* Get battery status
*/
async $getBatteryStatus(): Promise {
const notificationService = DailyNotificationService.getInstance();
return await notificationService.getBatteryStatus();
},
/**
* Request battery optimization exemption
*/
async $requestBatteryOptimizationExemption(): Promise {
const notificationService = DailyNotificationService.getInstance();
return await notificationService.requestBatteryOptimizationExemption();
},
// ... 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;
$scheduleDualNotification(config: any): Promise;
$registerNotificationCallback(name: string, callback: Function): Promise;
$getNotificationStatus(): Promise;
$cancelAllNotifications(): Promise;
$getBatteryStatus(): Promise;
$requestBatteryOptimizationExemption(): Promise;
}
}
```
### 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 or WebPlatformService.ts
import { DailyNotificationService } from '@/services/DailyNotificationService';
export class CapacitorPlatformService implements PlatformService {
// ... existing methods
private async initializeDatabase(): Promise {
// ... 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 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 web 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 Web
```bash
# Build and run on web
npm run build:web
npm run serve:web
```
### 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 browser dev tools for web 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
**Web:**
- Ensure Service Worker is registered
- Check HTTPS requirements
- Verify IndexedDB compatibility
- Check push 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.0.0
**Last Updated**: 2025-01-27 12:00:00 UTC
**Status**: Production Ready
**Author**: Matthew Raymer