forked from trent_larson/crowd-funder-for-time-pwa
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)
This commit is contained in:
@@ -36,6 +36,30 @@
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</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>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
@@ -35,10 +35,6 @@
|
||||
"pkg": "@capawesome/capacitor-file-picker",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@timesafari/daily-notification-plugin",
|
||||
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@timesafari/daily-notification-plugin",
|
||||
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
|
||||
|
||||
@@ -241,7 +241,7 @@ export interface ScheduleOptions {
|
||||
|
||||
export interface NativeFetcherConfig {
|
||||
apiServer: string;
|
||||
jwt: string;
|
||||
jwt: string; // Ignored - generated automatically by configureNativeFetcher
|
||||
starredPlanHandleIds: string[];
|
||||
}
|
||||
```
|
||||
@@ -250,6 +250,63 @@ export interface NativeFetcherConfig {
|
||||
- **Capacitor**: Full implementation, all methods functional
|
||||
- **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
|
||||
|
||||
#### Views Structure
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -152,7 +152,7 @@
|
||||
},
|
||||
"../daily-notification-plugin": {
|
||||
"name": "@timesafari/daily-notification-plugin",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
@@ -206,9 +206,146 @@ export default class DailyNotificationSection extends Vue {
|
||||
/**
|
||||
* Initialize component state on mount
|
||||
* 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> {
|
||||
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.notificationStatus = status;
|
||||
|
||||
// CRITICAL: Sync with plugin state first (source of truth)
|
||||
// Plugin may have an existing schedule even if settings don't
|
||||
// Plugin state is the source of truth
|
||||
if (status.isScheduled && status.scheduledTime) {
|
||||
// Plugin has a scheduled notification - sync UI to match
|
||||
this.nativeNotificationEnabled = true;
|
||||
@@ -244,31 +380,11 @@ export default class DailyNotificationSection extends Vue {
|
||||
this.nativeNotificationTime = formatTimeForDisplay(
|
||||
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 {
|
||||
// No plugin schedule - check settings for user preference
|
||||
const settings = await this.$accountSettings();
|
||||
const nativeNotificationTime = settings.nativeNotificationTime || "";
|
||||
this.nativeNotificationEnabled = !!nativeNotificationTime;
|
||||
this.nativeNotificationTimeStorage = nativeNotificationTime;
|
||||
|
||||
if (nativeNotificationTime) {
|
||||
this.nativeNotificationTime = formatTimeForDisplay(
|
||||
nativeNotificationTime,
|
||||
);
|
||||
}
|
||||
// No plugin schedule - UI defaults to disabled
|
||||
this.nativeNotificationEnabled = false;
|
||||
this.nativeNotificationTimeStorage = "";
|
||||
this.nativeNotificationTime = "";
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[DailyNotificationSection] Failed to initialize:", error);
|
||||
@@ -452,16 +568,14 @@ export default class DailyNotificationSection extends Vue {
|
||||
priority: "high",
|
||||
});
|
||||
|
||||
// Save to settings
|
||||
await this.$saveSettings({
|
||||
nativeNotificationTime: this.nativeNotificationTimeStorage,
|
||||
nativeNotificationTitle: this.nativeNotificationTitle,
|
||||
nativeNotificationMessage: this.nativeNotificationMessage,
|
||||
});
|
||||
|
||||
// Update UI state
|
||||
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(
|
||||
"Daily notification scheduled successfully",
|
||||
TIMEOUTS.SHORT,
|
||||
@@ -492,13 +606,6 @@ export default class DailyNotificationSection extends Vue {
|
||||
// Cancel notification via PlatformService
|
||||
await platformService.cancelDailyNotification();
|
||||
|
||||
// Clear settings
|
||||
await this.$saveSettings({
|
||||
nativeNotificationTime: "",
|
||||
nativeNotificationTitle: "",
|
||||
nativeNotificationMessage: "",
|
||||
});
|
||||
|
||||
// Update UI state
|
||||
this.nativeNotificationEnabled = false;
|
||||
this.nativeNotificationTime = "";
|
||||
@@ -558,10 +665,7 @@ export default class DailyNotificationSection extends Vue {
|
||||
if (this.nativeNotificationEnabled) {
|
||||
await this.updateNotificationTime(this.nativeNotificationTimeStorage);
|
||||
} else {
|
||||
// Just save the time preference
|
||||
await this.$saveSettings({
|
||||
nativeNotificationTime: this.nativeNotificationTimeStorage,
|
||||
});
|
||||
// Just update local state (time preference stored in component)
|
||||
this.showTimeEdit = false;
|
||||
this.notify.success("Notification time saved", TIMEOUTS.SHORT);
|
||||
}
|
||||
@@ -574,12 +678,9 @@ export default class DailyNotificationSection extends Vue {
|
||||
async updateNotificationTime(newTime: string): Promise<void> {
|
||||
// newTime is in "HH:mm" format from HTML5 time input
|
||||
if (!this.nativeNotificationEnabled) {
|
||||
// If notification is disabled, just save the time preference
|
||||
// If notification is disabled, just update local state
|
||||
this.nativeNotificationTimeStorage = newTime;
|
||||
this.nativeNotificationTime = formatTimeForDisplay(newTime);
|
||||
await this.$saveSettings({
|
||||
nativeNotificationTime: newTime,
|
||||
});
|
||||
this.showTimeEdit = false;
|
||||
return;
|
||||
}
|
||||
@@ -605,11 +706,6 @@ export default class DailyNotificationSection extends Vue {
|
||||
this.nativeNotificationTimeStorage = newTime;
|
||||
this.nativeNotificationTime = formatTimeForDisplay(newTime);
|
||||
|
||||
// 4. Save to settings
|
||||
await this.$saveSettings({
|
||||
nativeNotificationTime: newTime,
|
||||
});
|
||||
|
||||
this.notify.success(
|
||||
"Notification time updated successfully",
|
||||
TIMEOUTS.SHORT,
|
||||
|
||||
@@ -199,14 +199,6 @@ const MIGRATIONS = [
|
||||
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;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,11 +53,6 @@ export type Settings = {
|
||||
notifyingReminderMessage?: string; // set to their chosen message 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
|
||||
|
||||
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
|
||||
|
||||
@@ -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
|
||||
* @param jwtUrlText The URL containing the JWT
|
||||
|
||||
@@ -1543,6 +1543,51 @@ export class CapacitorPlatformService
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async configureNativeFetcher(
|
||||
@@ -1553,16 +1598,57 @@ export class CapacitorPlatformService
|
||||
"@timesafari/daily-notification-plugin"
|
||||
);
|
||||
|
||||
// Plugin expects apiBaseUrl, activeDid, and jwtToken
|
||||
// We'll need to get activeDid from somewhere - for now pass empty string
|
||||
// Components should provide activeDid when calling this
|
||||
// Step 1: Get activeDid from database (single source of truth)
|
||||
// This ensures we're using the correct user identity for authentication
|
||||
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({
|
||||
apiBaseUrl: config.apiServer,
|
||||
activeDid: "", // Should be provided by caller
|
||||
jwtToken: config.jwt,
|
||||
apiBaseUrl: apiServer,
|
||||
activeDid,
|
||||
jwtToken,
|
||||
});
|
||||
|
||||
logger.info("[CapacitorPlatformService] Configured native fetcher");
|
||||
logger.info("[CapacitorPlatformService] Configured native fetcher", {
|
||||
activeDid,
|
||||
apiServer,
|
||||
tokenExpirationHours: 72,
|
||||
tokenExpirationMinutes: expirationMinutes,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Failed to configure native fetcher:",
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
import { defineConfig } from "vite";
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user