diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 3aa15877..6392c86b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,30 @@
android:grantUriPermissions="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json
index d89dc7dd..721bea0d 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -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"
diff --git a/doc/daily-notification-plugin-integration-plan.md b/doc/daily-notification-plugin-integration-plan.md
index 4637554f..35c8aa5a 100644
--- a/doc/daily-notification-plugin-integration-plan.md
+++ b/doc/daily-notification-plugin-integration-plan.md
@@ -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
diff --git a/package-lock.json b/package-lock.json
index dec7d1a5..1ca0e8a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -152,7 +152,7 @@
},
"../daily-notification-plugin": {
"name": "@timesafari/daily-notification-plugin",
- "version": "1.0.0",
+ "version": "1.0.1",
"license": "MIT",
"workspaces": [
"packages/*"
diff --git a/src/components/notifications/DailyNotificationSection.vue b/src/components/notifications/DailyNotificationSection.vue
index 48533a95..9858014d 100644
--- a/src/components/notifications/DailyNotificationSection.vue
+++ b/src/components/notifications/DailyNotificationSection.vue
@@ -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 {
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 {
+ 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 {
+ 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 {
// 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,
diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts
index 7db68437..4a177786 100644
--- a/src/db-sql/migration.ts
+++ b/src/db-sql/migration.ts
@@ -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;
- `,
- },
];
/**
diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts
index 0af24058..493e4596 100644
--- a/src/db/tables/settings.ts
+++ b/src/db/tables/settings.ts
@@ -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
diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts
index b8ff2d57..ac17c392 100644
--- a/src/libs/crypto/index.ts
+++ b/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} 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 => {
+ 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
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 30b8d8b4..6ff9b503 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -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:",
diff --git a/vite.config.capacitor.mts b/vite.config.capacitor.mts
index b47e5abe..e3bfd9f1 100644
--- a/vite.config.capacitor.mts
+++ b/vite.config.capacitor.mts
@@ -1,4 +1,23 @@
import { defineConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
-export default defineConfig(async () => createBuildConfig('capacitor'));
\ No newline at end of file
+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,
+ }
+ }
+ }
+ };
+});
\ No newline at end of file