52 KiB
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:
{
"dependencies": {
"@timesafari/daily-notification-plugin": "git+https://github.com/timesafari/daily-notification-plugin.git#main"
}
}
Or install directly via npm:
npm install git+https://github.com/timesafari/daily-notification-plugin.git#main
2. Configure Capacitor
Update capacitor.config.ts
to include the plugin configuration:
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:
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:
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
:
<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:
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
:
<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
- Open your project in Xcode
- Select your app target
- Go to "Signing & Capabilities"
- Add the following capabilities:
- Background Modes
- Enable "Background App Refresh"
- Enable "Background Processing"
- Push Notifications (if using push notifications)
- Background Modes
5. TypeScript Integration
5.1 Create Plugin Service
Create src/services/DailyNotificationService.ts
:
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<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';
}
}
5.2 Add to PlatformServiceMixin
Update src/utils/PlatformServiceMixin.ts
to include notification methods:
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();
},
// ... rest of existing methods
};
5.3 Update TypeScript Declarations
Add to the Vue module declaration in src/utils/PlatformServiceMixin.ts
:
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>;
}
}
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:
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:
// 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<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 Community Update Notification
// 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
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
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
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 entriesbeforeId
: 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
GET /api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}
Offers to User's Projects
GET /api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}
Changes to Starred Projects
POST /api/v2/report/plansLastUpdatedBetween
{
"planIds": ["plan-123", "plan-456"],
"afterId": "01HSE3R9MAC0FT3P3KZ382TWV7"
}
8.3 Parallel Requests Implementation
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
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:
# 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
# Build and run on Android
npm run build:android
npx cap run android
10.2 Test on iOS
# Build and run on iOS
npm run build:ios
npx cap run ios
10.3 Test on Web
# Build and run on web
npm run build:web
npm run serve:web
11. Troubleshooting
11.1 Common Issues
- Plugin not found: Ensure the plugin is properly installed and the path is correct
- Permissions denied: Check that all required permissions are added to manifests
- Build errors: Clean and rebuild the project after adding the plugin
- TypeScript errors: Ensure the plugin exports proper TypeScript definitions
- Background tasks not running: Check battery optimization settings and background app refresh
- Endorser.ch API errors: Verify JWT token authentication and endpoint availability
11.2 Debug Steps
- Check console logs for initialization errors
- Verify plugin is loaded in
capacitor.plugins.json
- Test permissions manually in device settings
- Use browser dev tools for web platform testing
- Check WorkManager logs on Android
- Check BGTaskScheduler logs on iOS
- 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
// 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
// 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