Browse Source

feat: implement 72-hour JWT token refresh for daily notification plugin

- Add accessTokenForBackground() with 72-hour default expiration
  - Supports offline-first prefetch operations
  - Balances security with offline capability

- Implement proactive token refresh strategy
  - Refresh on component mount (DailyNotificationSection)
  - Refresh on app resume (Capacitor 'resume' event)
  - Refresh when notifications are enabled
  - Automatic refresh without user interaction

- Update CapacitorPlatformService.configureNativeFetcher()
  - Automatically retrieves activeDid from database
  - Generates 72-hour JWT tokens for background operations
  - Includes starred plans in configuration

- Add BroadcastReceivers to AndroidManifest.xml
  - DailyNotificationReceiver for scheduled notifications
  - BootReceiver for rescheduling after device reboot

- Add comprehensive documentation
  - JSDoc comments for all token-related functions
  - Inline comments explaining refresh strategy
  - Documentation section on authentication & token management

Benefits:
- No app wake-up required (refresh when app already open)
- Works offline (72-hour validity supports extended periods)
- Automatic (no user interaction required)
- Graceful degradation (uses cached content if refresh fails)
pull/214/head
Matthew Raymer 7 days ago
parent
commit
831532739c
  1. 24
      android/app/src/main/AndroidManifest.xml
  2. 4
      android/app/src/main/assets/capacitor.plugins.json
  3. 59
      doc/daily-notification-plugin-integration-plan.md
  4. 2
      package-lock.json
  5. 202
      src/components/notifications/DailyNotificationSection.vue
  6. 8
      src/db-sql/migration.ts
  7. 5
      src/db/tables/settings.ts
  8. 65
      src/libs/crypto/index.ts
  9. 100
      src/services/platforms/CapacitorPlatformService.ts
  10. 21
      vite.config.capacitor.mts

24
android/app/src/main/AndroidManifest.xml

@ -36,6 +36,30 @@
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider> </provider>
<!-- Daily Notification Plugin Receivers -->
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:directBootAware="true"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="1000">
<!-- Delivered very early after reboot (before unlock) -->
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<!-- Delivered after the user unlocks / credential-encrypted storage is available -->
<action android:name="android.intent.action.BOOT_COMPLETED" />
<!-- Delivered after app update; great for rescheduling alarms without reboot -->
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application> </application>
<!-- Permissions --> <!-- Permissions -->

4
android/app/src/main/assets/capacitor.plugins.json

@ -35,10 +35,6 @@
"pkg": "@capawesome/capacitor-file-picker", "pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin" "classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
}, },
{
"pkg": "@timesafari/daily-notification-plugin",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
},
{ {
"pkg": "@timesafari/daily-notification-plugin", "pkg": "@timesafari/daily-notification-plugin",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin" "classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"

59
doc/daily-notification-plugin-integration-plan.md

@ -241,7 +241,7 @@ export interface ScheduleOptions {
export interface NativeFetcherConfig { export interface NativeFetcherConfig {
apiServer: string; apiServer: string;
jwt: string; jwt: string; // Ignored - generated automatically by configureNativeFetcher
starredPlanHandleIds: string[]; starredPlanHandleIds: string[];
} }
``` ```
@ -250,6 +250,63 @@ export interface NativeFetcherConfig {
- **Capacitor**: Full implementation, all methods functional - **Capacitor**: Full implementation, all methods functional
- **Web/Electron**: Status/permission methods return `null`, scheduling methods throw errors with clear messages - **Web/Electron**: Status/permission methods return `null`, scheduling methods throw errors with clear messages
### Authentication & Token Management
#### Background Prefetch Authentication
The daily notification plugin requires authentication tokens for background prefetch operations. The implementation uses a **hybrid token refresh strategy** that balances security with offline capability.
**Token Generation** (`src/libs/crypto/index.ts`):
- Function: `accessTokenForBackground(did, expirationMinutes?)`
- Default expiration: **72 hours** (4320 minutes)
- Token type: JWT with ES256K signing
- Payload: `{ exp, iat, iss: did }`
**Why 72 Hours?**
- Balances security (read-only prefetch operations) with offline capability
- Reduces need for app to wake itself for token refresh
- Allows plugin to work offline for extended periods (e.g., weekend trips)
- Longer than typical prefetch windows (5 minutes before notification)
**Token Refresh Strategy (Hybrid Approach)**:
1. **Proactive Refresh Triggers**:
- Component mount (`DailyNotificationSection.mounted()`)
- App resume (Capacitor `resume` event)
- Notification enabled (when user enables daily notifications)
2. **Refresh Implementation** (`DailyNotificationSection.refreshNativeFetcherConfig()`):
- Checks if notifications are supported and enabled
- Retrieves API server URL from settings
- Retrieves starred plans from settings
- Calls `configureNativeFetcher()` to generate fresh token
- Errors are logged but don't interrupt user experience
3. **Offline Behavior**:
- If token expires while offline → plugin uses cached content
- Next time app opens → token automatically refreshed
- No app wake-up required (refresh happens when app is already open)
**Configuration Flow** (`CapacitorPlatformService.configureNativeFetcher()`):
1. Retrieves active DID from `active_identity` table (single source of truth)
2. Generates JWT token with 72-hour expiration using `accessTokenForBackground()`
3. Configures plugin with API server URL, active DID, and JWT token
4. Plugin stores token in its Room database for background workers
**Security Considerations**:
- Tokens are used only for read-only prefetch operations
- Tokens are stored securely in plugin's Room database
- Tokens are refreshed proactively to minimize exposure window
- No private keys are exposed to native code
- Token generation happens in TypeScript (no Java crypto compatibility issues)
**Error Handling**:
- Returns `null` if active DID not found (no user logged in)
- Returns `null` if JWT generation fails
- Logs errors but doesn't throw (allows graceful degradation)
- Refresh failures don't interrupt user experience (plugin uses cached content)
### Component Architecture ### Component Architecture
#### Views Structure #### Views Structure

2
package-lock.json

@ -152,7 +152,7 @@
}, },
"../daily-notification-plugin": { "../daily-notification-plugin": {
"name": "@timesafari/daily-notification-plugin", "name": "@timesafari/daily-notification-plugin",
"version": "1.0.0", "version": "1.0.1",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"

202
src/components/notifications/DailyNotificationSection.vue

@ -206,9 +206,146 @@ export default class DailyNotificationSection extends Vue {
/** /**
* Initialize component state on mount * Initialize component state on mount
* Checks platform support and syncs with plugin state * Checks platform support and syncs with plugin state
*
* **Token Refresh on Mount:**
* - Refreshes native fetcher configuration to ensure plugin has valid token
* - This handles cases where app was closed for extended periods
* - Token refresh happens automatically without user interaction
*
* **App Resume Listener:**
* - Listens for Capacitor 'resume' event to refresh token when app comes to foreground
* - Ensures plugin always has fresh token for background prefetch operations
* - Cleaned up in `beforeDestroy()` lifecycle hook
*/ */
async mounted(): Promise<void> { async mounted(): Promise<void> {
await this.initializeState(); await this.initializeState();
// Refresh native fetcher configuration on mount
// This ensures plugin has valid token even if app was closed for extended periods
await this.refreshNativeFetcherConfig();
// Listen for app resume events to refresh token when app comes to foreground
// This is part of the proactive token refresh strategy
document.addEventListener("resume", this.handleAppResume);
}
/**
* Cleanup on component destroy
*/
beforeDestroy(): void {
document.removeEventListener("resume", this.handleAppResume);
}
/**
* Handle app resume event - refresh native fetcher configuration
*
* This method is called when the app comes to foreground (via Capacitor 'resume' event).
* It proactively refreshes the JWT token to ensure the plugin has valid authentication
* for background prefetch operations.
*
* **Why refresh on resume?**
* - Tokens expire after 72 hours
* - App may have been closed for extended periods
* - Refreshing ensures plugin has valid token for next prefetch cycle
* - No user interaction required - happens automatically
*
* @see {@link refreshNativeFetcherConfig} For implementation details
*/
async handleAppResume(): Promise<void> {
logger.debug(
"[DailyNotificationSection] App resumed, refreshing native fetcher config",
);
await this.refreshNativeFetcherConfig();
}
/**
* Refresh native fetcher configuration with fresh JWT token
*
* This method ensures the daily notification plugin has a valid authentication token
* for background prefetch operations. It's called proactively to prevent token expiration
* issues during offline periods.
*
* **Refresh Triggers:**
* - Component mount (when notification settings page loads)
* - App resume (when app comes to foreground)
* - Notification enabled (when user enables daily notifications)
*
* **Token Refresh Strategy (Hybrid Approach):**
* - Tokens are valid for 72 hours (see `accessTokenForBackground`)
* - Tokens are refreshed proactively when app is already open
* - If token expires while offline, plugin uses cached content
* - Next time app opens, token is automatically refreshed
*
* **Why This Approach?**
* - No app wake-up required (tokens refresh when app is already open)
* - Works offline (72-hour validity supports extended offline periods)
* - Automatic (no user interaction required)
* - Includes starred plans (fetcher receives user's starred plans for prefetch)
* - Graceful degradation (if refresh fails, cached content still works)
*
* **Error Handling:**
* - Errors are logged but not shown to user (background operation)
* - Returns early if notifications not supported or disabled
* - Returns early if API server not configured
* - Failures don't interrupt user experience
*
* @returns Promise that resolves when refresh completes (or fails silently)
*
* @example
* ```typescript
* // Called automatically on mount/resume
* await this.refreshNativeFetcherConfig();
* ```
*
* @see {@link CapacitorPlatformService.configureNativeFetcher} For token generation
* @see {@link accessTokenForBackground} For 72-hour token generation
*/
async refreshNativeFetcherConfig(): Promise<void> {
try {
const platformService = PlatformServiceFactory.getInstance();
// Early return: Only refresh if notifications are supported and enabled
// This prevents unnecessary work when notifications aren't being used
if (!this.notificationsSupported || !this.nativeNotificationEnabled) {
return;
}
// Get settings for API server and starred plans
// API server tells plugin where to fetch content from
// Starred plans tell plugin which plans to prefetch
const settings = await this.$accountSettings();
const apiServer = settings.apiServer || "";
if (!apiServer) {
logger.warn(
"[DailyNotificationSection] No API server configured, skipping native fetcher refresh",
);
return;
}
// Get starred plans from settings
// These are passed to the plugin so it knows which plans to prefetch
const starredPlanHandleIds = settings.starredPlanHandleIds || [];
// Configure native fetcher with fresh token
// The jwt parameter is ignored - configureNativeFetcher generates it automatically
// This ensures we always have a fresh token with current expiration time
await platformService.configureNativeFetcher({
apiServer,
jwt: "", // Will be generated automatically by configureNativeFetcher
starredPlanHandleIds,
});
logger.info(
"[DailyNotificationSection] Native fetcher configuration refreshed",
);
} catch (error) {
// Don't show error to user - this is a background operation
// Failures are logged for debugging but don't interrupt user experience
// If refresh fails, plugin will use existing token (if still valid) or cached content
logger.error(
"[DailyNotificationSection] Failed to refresh native fetcher config:",
error,
);
}
} }
/** /**
@ -235,8 +372,7 @@ export default class DailyNotificationSection extends Vue {
this.notificationsSupported = true; this.notificationsSupported = true;
this.notificationStatus = status; this.notificationStatus = status;
// CRITICAL: Sync with plugin state first (source of truth) // Plugin state is the source of truth
// Plugin may have an existing schedule even if settings don't
if (status.isScheduled && status.scheduledTime) { if (status.isScheduled && status.scheduledTime) {
// Plugin has a scheduled notification - sync UI to match // Plugin has a scheduled notification - sync UI to match
this.nativeNotificationEnabled = true; this.nativeNotificationEnabled = true;
@ -244,31 +380,11 @@ export default class DailyNotificationSection extends Vue {
this.nativeNotificationTime = formatTimeForDisplay( this.nativeNotificationTime = formatTimeForDisplay(
status.scheduledTime, status.scheduledTime,
); );
// Also sync settings to match plugin state
const settings = await this.$accountSettings();
if (settings.nativeNotificationTime !== status.scheduledTime) {
await this.$saveSettings({
nativeNotificationTime: status.scheduledTime,
nativeNotificationTitle:
settings.nativeNotificationTitle || this.nativeNotificationTitle,
nativeNotificationMessage:
settings.nativeNotificationMessage ||
this.nativeNotificationMessage,
});
}
} else { } else {
// No plugin schedule - check settings for user preference // No plugin schedule - UI defaults to disabled
const settings = await this.$accountSettings(); this.nativeNotificationEnabled = false;
const nativeNotificationTime = settings.nativeNotificationTime || ""; this.nativeNotificationTimeStorage = "";
this.nativeNotificationEnabled = !!nativeNotificationTime; this.nativeNotificationTime = "";
this.nativeNotificationTimeStorage = nativeNotificationTime;
if (nativeNotificationTime) {
this.nativeNotificationTime = formatTimeForDisplay(
nativeNotificationTime,
);
}
} }
} catch (error) { } catch (error) {
logger.error("[DailyNotificationSection] Failed to initialize:", error); logger.error("[DailyNotificationSection] Failed to initialize:", error);
@ -452,16 +568,14 @@ export default class DailyNotificationSection extends Vue {
priority: "high", priority: "high",
}); });
// Save to settings
await this.$saveSettings({
nativeNotificationTime: this.nativeNotificationTimeStorage,
nativeNotificationTitle: this.nativeNotificationTitle,
nativeNotificationMessage: this.nativeNotificationMessage,
});
// Update UI state // Update UI state
this.nativeNotificationEnabled = true; this.nativeNotificationEnabled = true;
// Refresh native fetcher configuration with fresh token
// This ensures plugin has valid authentication when notifications are first enabled
// Token will be valid for 72 hours, supporting offline prefetch operations
await this.refreshNativeFetcherConfig();
this.notify.success( this.notify.success(
"Daily notification scheduled successfully", "Daily notification scheduled successfully",
TIMEOUTS.SHORT, TIMEOUTS.SHORT,
@ -492,13 +606,6 @@ export default class DailyNotificationSection extends Vue {
// Cancel notification via PlatformService // Cancel notification via PlatformService
await platformService.cancelDailyNotification(); await platformService.cancelDailyNotification();
// Clear settings
await this.$saveSettings({
nativeNotificationTime: "",
nativeNotificationTitle: "",
nativeNotificationMessage: "",
});
// Update UI state // Update UI state
this.nativeNotificationEnabled = false; this.nativeNotificationEnabled = false;
this.nativeNotificationTime = ""; this.nativeNotificationTime = "";
@ -558,10 +665,7 @@ export default class DailyNotificationSection extends Vue {
if (this.nativeNotificationEnabled) { if (this.nativeNotificationEnabled) {
await this.updateNotificationTime(this.nativeNotificationTimeStorage); await this.updateNotificationTime(this.nativeNotificationTimeStorage);
} else { } else {
// Just save the time preference // Just update local state (time preference stored in component)
await this.$saveSettings({
nativeNotificationTime: this.nativeNotificationTimeStorage,
});
this.showTimeEdit = false; this.showTimeEdit = false;
this.notify.success("Notification time saved", TIMEOUTS.SHORT); this.notify.success("Notification time saved", TIMEOUTS.SHORT);
} }
@ -574,12 +678,9 @@ export default class DailyNotificationSection extends Vue {
async updateNotificationTime(newTime: string): Promise<void> { async updateNotificationTime(newTime: string): Promise<void> {
// newTime is in "HH:mm" format from HTML5 time input // newTime is in "HH:mm" format from HTML5 time input
if (!this.nativeNotificationEnabled) { if (!this.nativeNotificationEnabled) {
// If notification is disabled, just save the time preference // If notification is disabled, just update local state
this.nativeNotificationTimeStorage = newTime; this.nativeNotificationTimeStorage = newTime;
this.nativeNotificationTime = formatTimeForDisplay(newTime); this.nativeNotificationTime = formatTimeForDisplay(newTime);
await this.$saveSettings({
nativeNotificationTime: newTime,
});
this.showTimeEdit = false; this.showTimeEdit = false;
return; return;
} }
@ -605,11 +706,6 @@ export default class DailyNotificationSection extends Vue {
this.nativeNotificationTimeStorage = newTime; this.nativeNotificationTimeStorage = newTime;
this.nativeNotificationTime = formatTimeForDisplay(newTime); this.nativeNotificationTime = formatTimeForDisplay(newTime);
// 4. Save to settings
await this.$saveSettings({
nativeNotificationTime: newTime,
});
this.notify.success( this.notify.success(
"Notification time updated successfully", "Notification time updated successfully",
TIMEOUTS.SHORT, TIMEOUTS.SHORT,

8
src/db-sql/migration.ts

@ -199,14 +199,6 @@ const MIGRATIONS = [
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT; ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
`, `,
}, },
{
name: "006_add_nativeNotificationSettings_to_settings",
sql: `
ALTER TABLE settings ADD COLUMN nativeNotificationTime TEXT;
ALTER TABLE settings ADD COLUMN nativeNotificationTitle TEXT;
ALTER TABLE settings ADD COLUMN nativeNotificationMessage TEXT;
`,
},
]; ];
/** /**

5
src/db/tables/settings.ts

@ -53,11 +53,6 @@ export type Settings = {
notifyingReminderMessage?: string; // set to their chosen message for a daily reminder notifyingReminderMessage?: string; // set to their chosen message for a daily reminder
notifyingReminderTime?: string; // set to their chosen time for a daily reminder notifyingReminderTime?: string; // set to their chosen time for a daily reminder
// Native notification settings (Capacitor only)
nativeNotificationTime?: string; // "09:00" format (24-hour) - scheduled time for daily notification
nativeNotificationTitle?: string; // Default: "Daily Update" - notification title
nativeNotificationMessage?: string; // Default message - notification body text
partnerApiServer?: string; // partner server API URL partnerApiServer?: string; // partner server API URL
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes

65
src/libs/crypto/index.ts

@ -104,6 +104,71 @@ export const accessToken = async (did?: string) => {
} }
}; };
/**
* Generate a longer-lived access token for background operations
*
* This function creates JWT tokens with extended validity (default 72 hours) for use
* in background prefetch operations. The longer expiration period allows the daily
* notification plugin to work offline for extended periods without requiring the app
* to be in the foreground to refresh tokens.
*
* **Token Refresh Strategy (Hybrid Approach):**
* - Tokens are valid for 72 hours (configurable)
* - Tokens are refreshed proactively when:
* - App comes to foreground (via Capacitor 'resume' event)
* - Component mounts (DailyNotificationSection)
* - Notifications are enabled
* - If token expires while offline, plugin uses cached content
* - Next time app opens, token is automatically refreshed
*
* **Why 72 Hours?**
* - Balances security (read-only prefetch operations) with offline capability
* - Reduces need for app to wake itself for token refresh
* - Allows plugin to work offline for extended periods (e.g., weekend trips)
* - Longer than typical prefetch windows (5 minutes before notification)
*
* **Security Considerations:**
* - Tokens are used only for read-only prefetch operations
* - Tokens are stored securely in plugin's Room database
* - Tokens are refreshed proactively to minimize exposure window
* - No private keys are exposed to native code
*
* @param {string} did - User DID (Decentralized Identifier) for token issuer
* @param {number} expirationMinutes - Optional expiration in minutes (defaults to 72 hours = 4320 minutes)
* @return {Promise<string>} JWT token with extended validity, or empty string if no DID provided
*
* @example
* ```typescript
* // Generate token with default 72-hour expiration
* const token = await accessTokenForBackground("did:ethr:0x...");
*
* // Generate token with custom expiration (24 hours)
* const token24h = await accessTokenForBackground("did:ethr:0x...", 24 * 60);
* ```
*
* @see {@link accessToken} For short-lived tokens (1 minute) for regular API requests
* @see {@link createEndorserJwtForDid} For JWT creation implementation
*/
export const accessTokenForBackground = async (
did?: string,
expirationMinutes?: number,
): Promise<string> => {
if (!did) {
return "";
}
// Use provided expiration or default to 72 hours (4320 minutes)
// This allows background prefetch operations to work offline for extended periods
const expirationSeconds = expirationMinutes
? expirationMinutes * 60
: 72 * 60 * 60; // Default 72 hours
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + expirationSeconds;
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
return createEndorserJwtForDid(did, tokenPayload);
};
/** /**
* Extract JWT from various URL formats * Extract JWT from various URL formats
* @param jwtUrlText The URL containing the JWT * @param jwtUrlText The URL containing the JWT

100
src/services/platforms/CapacitorPlatformService.ts

@ -1543,6 +1543,51 @@ export class CapacitorPlatformService
/** /**
* Configure native fetcher for background operations * Configure native fetcher for background operations
*
* This method configures the daily notification plugin's native content fetcher
* with authentication credentials for background prefetch operations. It automatically
* retrieves the active DID from the database and generates a fresh JWT token with
* 72-hour expiration.
*
* **Authentication Flow:**
* 1. Retrieves active DID from `active_identity` table (single source of truth)
* 2. Generates JWT token with 72-hour expiration using `accessTokenForBackground()`
* 3. Configures plugin with API server URL, active DID, and JWT token
* 4. Plugin stores token in its Room database for background workers
*
* **Token Management:**
* - Tokens are valid for 72 hours (4320 minutes)
* - Tokens are refreshed proactively when app comes to foreground
* - If token expires while offline, plugin uses cached content
* - Token refresh happens automatically via `DailyNotificationSection.refreshNativeFetcherConfig()`
*
* **Offline-First Design:**
* - 72-hour validity supports extended offline periods
* - Plugin can prefetch content when online and use cached content when offline
* - No app wake-up required for token refresh (happens when app is already open)
*
* **Error Handling:**
* - Returns `null` if active DID not found (no user logged in)
* - Returns `null` if JWT generation fails
* - Logs errors but doesn't throw (allows graceful degradation)
*
* @param config - Native fetcher configuration
* @param config.apiServer - API server URL (optional, uses default if not provided)
* @param config.jwt - JWT token (ignored, generated automatically)
* @param config.starredPlanHandleIds - Array of starred plan handle IDs for prefetch
* @returns Promise that resolves when configured, or `null` if configuration failed
*
* @example
* ```typescript
* await platformService.configureNativeFetcher({
* apiServer: "https://api.endorser.ch",
* jwt: "", // Generated automatically
* starredPlanHandleIds: ["plan-123", "plan-456"]
* });
* ```
*
* @see {@link accessTokenForBackground} For JWT token generation
* @see {@link DailyNotificationSection.refreshNativeFetcherConfig} For proactive token refresh
* @see PlatformService.configureNativeFetcher * @see PlatformService.configureNativeFetcher
*/ */
async configureNativeFetcher( async configureNativeFetcher(
@ -1553,16 +1598,57 @@ export class CapacitorPlatformService
"@timesafari/daily-notification-plugin" "@timesafari/daily-notification-plugin"
); );
// Plugin expects apiBaseUrl, activeDid, and jwtToken // Step 1: Get activeDid from database (single source of truth)
// We'll need to get activeDid from somewhere - for now pass empty string // This ensures we're using the correct user identity for authentication
// Components should provide activeDid when calling this const activeIdentity = await this.getActiveIdentity();
const activeDid = activeIdentity.activeDid;
if (!activeDid) {
logger.warn(
"[CapacitorPlatformService] No activeDid found, cannot configure native fetcher",
);
return null;
}
// Step 2: Generate JWT token for background operations
// Use 72-hour expiration for offline-first prefetch operations
// This allows the plugin to work offline for extended periods
const { accessTokenForBackground } = await import(
"../../libs/crypto/index"
);
// Use 72 hours (4320 minutes) for background prefetch tokens
// This is longer than passkey expiration to support offline scenarios
const expirationMinutes = 72 * 60; // 72 hours
const jwtToken = await accessTokenForBackground(
activeDid,
expirationMinutes,
);
if (!jwtToken) {
logger.error("[CapacitorPlatformService] Failed to generate JWT token");
return null;
}
// Step 3: Get API server from config or use default
// This ensures the plugin knows where to fetch content from
const apiServer =
config.apiServer ||
(await import("../../constants/app")).DEFAULT_ENDORSER_API_SERVER;
// Step 4: Configure plugin with credentials
// Plugin stores these in its Room database for background workers
await DailyNotification.configureNativeFetcher({ await DailyNotification.configureNativeFetcher({
apiBaseUrl: config.apiServer, apiBaseUrl: apiServer,
activeDid: "", // Should be provided by caller activeDid,
jwtToken: config.jwt, jwtToken,
}); });
logger.info("[CapacitorPlatformService] Configured native fetcher"); logger.info("[CapacitorPlatformService] Configured native fetcher", {
activeDid,
apiServer,
tokenExpirationHours: 72,
tokenExpirationMinutes: expirationMinutes,
});
} catch (error) { } catch (error) {
logger.error( logger.error(
"[CapacitorPlatformService] Failed to configure native fetcher:", "[CapacitorPlatformService] Failed to configure native fetcher:",

21
vite.config.capacitor.mts

@ -1,4 +1,23 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts"; import { createBuildConfig } from "./vite.config.common.mts";
export default defineConfig(async () => createBuildConfig('capacitor')); export default defineConfig(async () => {
const baseConfig = await createBuildConfig('capacitor');
return {
...baseConfig,
build: {
...baseConfig.build,
rollupOptions: {
...baseConfig.build?.rollupOptions,
// Externalize Capacitor plugins that are bundled natively
external: [
"@timesafari/daily-notification-plugin"
],
output: {
...baseConfig.build?.rollupOptions?.output,
}
}
}
};
});
Loading…
Cancel
Save