# Native Fetcher Configuration Guide **Author**: Matthew Raymer **Date**: 2025-01-28 **Status**: 🎯 **REFERENCE** - Complete guide for configuring native fetchers ## Overview The `configureNativeFetcher()` method provides a **cross-platform** mechanism for passing API credentials from TypeScript/JavaScript code to native fetcher implementations. This guide explains how to use this feature, why it exists, and provides complete examples. ## Table of Contents - [Why `configureNativeFetcher()`?](#why-configurenativefetcher) - [Architecture](#architecture) - [TypeScript Interface](#typescript-interface) - [Native Implementation](#native-implementation) - [Complete Example](#complete-example) - [Error Handling](#error-handling) - [Thread Safety](#thread-safety) - [Best Practices](#best-practices) - [Troubleshooting](#troubleshooting) --- ## Why `configureNativeFetcher()`? ### The Problem Native fetchers (implementing `NativeNotificationContentFetcher`) are called from background workers (WorkManager on Android, BGTaskScheduler on iOS). These background workers cannot access: - JavaScript/TypeScript variables - Capacitor bridge (unreliable in background) - React/Vue component state - WebView storage ### The Solution `configureNativeFetcher()` provides a **direct injection** mechanism: - βœ… **Cross-platform**: Same TypeScript interface on Android, iOS, and web - βœ… **Simple**: Only 3 required parameters (apiBaseUrl, activeDid, jwtSecret) - βœ… **Type-safe**: TypeScript types ensure correct usage - βœ… **No storage dependency**: Doesn't require SharedPreferences, UserDefaults, etc. ### Alternatives (and why we don't use them) | Approach | Why Not | |----------|---------| | Hardcode in native code | ❌ Can't switch environments, credentials in code | | SharedPreferences/UserDefaults | ❌ Platform-specific, complex sync | | JS bridge at fetch time | ❌ Unreliable in background workers | | Capacitor preferences plugin | ❌ Overhead, async issues in background | --- ## Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ TypeScript/JavaScript (Host App) β”‚ β”‚ β”‚ β”‚ await DailyNotification.configureNativeFetcher({ β”‚ β”‚ apiBaseUrl: 'http://10.0.2.2:3000', β”‚ β”‚ activeDid: 'did:ethr:0x...', β”‚ β”‚ jwtSecret: 'secret' β”‚ β”‚ }); β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Capacitor Bridge β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DailyNotificationPlugin (Android/iOS) β”‚ β”‚ β”‚ β”‚ @PluginMethod β”‚ β”‚ configureNativeFetcher(PluginCall call) { β”‚ β”‚ fetcher.configure(apiBaseUrl, activeDid, jwtSecret); β”‚ β”‚ } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Direct method call β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ NativeNotificationContentFetcher (Host App Native Code) β”‚ β”‚ β”‚ β”‚ @Override β”‚ β”‚ void configure(String apiBaseUrl, β”‚ β”‚ String activeDid, β”‚ β”‚ String jwtSecret) { β”‚ β”‚ this.apiBaseUrl = apiBaseUrl; β”‚ β”‚ this.activeDid = activeDid; β”‚ β”‚ this.jwtSecret = jwtSecret; β”‚ β”‚ } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## TypeScript Interface ### Method Signature ```typescript interface DailyNotificationPlugin { configureNativeFetcher(options: { apiBaseUrl: string; // Base URL for API server activeDid: string; // Active DID for authentication jwtSecret: string; // JWT secret for signing tokens }): Promiseθ¬Ή; } ``` ### Parameters | Parameter | Type | Required | Description | Example | |-----------|------|----------|-------------|---------| | `apiBaseUrl` | `string` | βœ… Yes | Base URL for API server. For Android emulator, use `10.0.2.2` to access host machine's `localhost`. | `"http://10.0.2.2:3000"` (Android emulator)
`"http://localhost:3000"` (iOS simulator)
`"https://api.timesafari.com"` (production) | | `activeDid` | `string` | βœ… Yes | Active DID (Decentralized Identifier) for authentication. Used as JWT issuer/subject. | `"did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"` | | `jwtSecret` | `string` | βœ… Yes | JWT secret key for signing authentication tokens. **Keep secure!** | `"test-jwt-secret-for-development"` | ### Return Value - **Success**: `Promise` resolves - **Failure**: `Promise` rejects with error message ### Example Usage ```typescript import { DailyNotification } from '@capacitor-community/daily-notification'; import { TEST_USER_ZERO_CONFIG } from './config/test-user-zero'; async function initializeNotificationPlugin() { try { // Configure native fetcher with User Zero credentials await DailyNotification.configureNativeFetcher({ apiBaseUrl: TEST_USER_ZERO_CONFIG.getApiServerUrl(), activeDid: TEST_USER_ZERO_CONFIG.identity.did, jwtSecret: 'test-jwt-secret-for-user-zero-development-only' }); console.log('βœ… Native fetcher configured successfully'); } catch (error) { console.error('❌ Failed to configure native fetcher:', error); throw error; } } ``` --- ## Native Implementation ### Java/Kotlin Interface The `NativeNotificationContentFetcher` interface includes an optional `configure()` method: ```java public interface NativeNotificationContentFetcher { CompletableFuture> fetchContent(FetchContext context); /** * Optional: Configure the native fetcher with API credentials */ default void configure(String apiBaseUrl, String activeDid, String jwtSecret) { // Default no-op - fetchers that need config can override } } ``` ### Implementation Pattern ```java public class MyNativeFetcher implements NativeNotificationContentFetcher { private static final String TAG = "MyNativeFetcher"; // Use volatile for thread-safe access from background workers private volatile String apiBaseUrl; private volatile String activeDid; private volatile String jwtSecret; @Override public void configure(String apiBaseUrl, String activeDid, String jwtSecret) { this.apiBaseUrl = apiBaseUrl; this.activeDid = activeDid; this.jwtSecret = jwtSecret; Log.i(TAG, "Fetcher configured - API: " + apiBaseUrl + ", DID: " + activeDid.substring(0, Math.min(30, activeDid.length())) + "..."); } @Override public CompletableFuture> fetchContent(FetchContext context) { return CompletableFuture.supplyAsync(() -> { // Check if configured if (apiBaseUrl == null || activeDid == null || jwtSecret == null) { Log.e(TAG, "Not configured! Call configureNativeFetcher() from TypeScript first."); return Collections.emptyList(); } // Use configured values to make API calls // ... }); } } ``` ### When to Override `configure()` **Override if:** - βœ… Your fetcher needs API credentials from TypeScript - βœ… Configuration should come from app config (not hardcoded) - βœ… You want to avoid platform-specific storage **Use default no-op if:** - βœ… Credentials come from platform storage (SharedPreferences, Keychain) - βœ… You have hardcoded test credentials - βœ… Configuration is handled internally --- ## Complete Example ### 1. Register Native Fetcher (Android) **File**: `android/app/src/main/java/com/example/app/MyApplication.java` ```java package com.example.app; import android.app.Application; import com.timesafari.dailynotification.DailyNotificationPlugin; import com.timesafari.dailynotification.NativeNotificationContentFetcher; public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // Register native fetcher NativeNotificationContentFetcher fetcher = new MyNativeFetcher(); DailyNotificationPlugin.setNativeFetcher(fetcher); } } ``` **AndroidManifest.xml**: ```xml ``` ### 2. Configure from TypeScript **File**: `src/services/NotificationService.ts` ```typescript import { DailyNotification } from '@capacitor-community/daily-notification'; import { getApiServerUrl, getActiveDid, getJwtSecret } from './config'; export async function setupDailyNotifications() { // Step 1: Configure native fetcher await DailyNotification.configureNativeFetcher({ apiBaseUrl: getApiServerUrl(), activeDid: getActiveDid(), jwtSecret: getJwtSecret() }); // Step 2: Configure plugin policy await DailyNotification.setPolicy({ prefetchWindowMs: 5 * 60 * 1000, // 5 minutes retryBackoff: { minMs: 2000, maxMs: 600000, factor: 2, jitterPct: 20 } }); // Step 3: Schedule notifications // ... } ``` ### 3. Native Fetcher Implementation **File**: `android/app/src/main/java/com/example/app/MyNativeFetcher.java` ```java package com.example.app; import android.util.Log; import com.timesafari.dailynotification.*; import java.util.*; public class MyNativeFetcher implements NativeNotificationContentFetcher { private static final String TAG = "MyNativeFetcher"; private static final String ENDORSER_ENDPOINT = "/api/v2/report/plansLastUpdatedBetween"; // Thread-safe configuration storage private volatile String apiBaseUrl; private volatile String activeDid; private volatile String jwtSecret; @Override public void configure(String apiBaseUrl, String activeDid, String jwtSecret) { this.apiBaseUrl = apiBaseUrl; this.activeDid = activeDid; this.jwtSecret = jwtSecret; Log.i(TAG, "Configured with API: " + apiBaseUrl); } @Override public CompletableFuture> fetchContent(FetchContext context) { return CompletableFuture.supplyAsync(() -> { try { // Validate configuration if (apiBaseUrl == null || activeDid == null || jwtSecret == null) { Log.e(TAG, "Not configured! Call configureNativeFetcher() first."); return Collections.emptyList(); } // Generate JWT token String jwt = generateJWT(activeDid, jwtSecret); // Make API call String url = apiBaseUrl + reconstituir ENDORSER_ENDPOINT; // ... HTTP request logic ... // Convert response to NotificationContent List contents = parseResponse(response); return contents; } catch (Exception e) { Log.e(TAG, "Fetch failed", e); return Collections.emptyList(); } }); } private String generateJWT(String did, String secret) { // JWT generation logic // ... } } ``` --- ## Error Handling ### TypeScript Errors ```typescript try { await DailyNotification.configureNativeFetcher({ apiBaseUrl: 'http://10.0.2.2:3000', activeDid: 'did:ethr:0x...', jwtSecret: 'secret' }); } catch (error) { // Possible errors: // - "Missing required parameters: apiBaseUrl, activeDid, and jwtSecret are required" // - "No native fetcher registered. Register one in Application.onCreate() before configuring." // - "Failed to configure native fetcher: " console.error('Configuration failed:', error); } ``` ### Native Fetcher Validation ```java @Override public CompletableFuture> fetchContent(FetchContext context) { return CompletableFuture.supplyAsync(() -> { // Always check configuration before use if (apiBaseUrl == null || activeDid == null || jwtSecret == null) { Log.e(TAG, "Not configured! Returning empty list."); return Collections.emptyList(); } // Proceed with fetch... }); } ``` --- ## Thread Safety ### Background Worker Context Native fetchers are called from **background worker threads** (WorkManager, BGTaskScheduler). Configuration may be set from the **main thread** (TypeScript call). ### Volatile Fields Use `volatile` for configuration fields to ensure: - βœ… Changes are immediately visible to all threads - βœ… No caching of stale values - βœ… Thread-safe access without synchronization ```java // βœ… Correct: Volatile fields private volatile String apiBaseUrl; private volatile String activeDid; private volatile String jwtSecret; // ❌ Incorrect: Non-volatile fields (may see stale values) private String apiBaseUrl; private String activeDid; private String jwtSecret; ``` ### Reconfiguration Configuration can be updated at any time: ```typescript // Update configuration when user switches accounts await DailyNotification.configureNativeFetcher({ apiBaseUrl: newApiUrl, activeDid: newActiveDid, jwtSecret: newJwtSecret }); ``` Subsequent `fetchContent()` calls will use the new configuration immediately. --- ## Best Practices ### 1. Configure Early Call `configureNativeFetcher()` as soon as credentials are available: ```typescript // βœ… Good: Configure immediately after app startup await appInit(); await DailyNotification.configureNativeFetcher(config); // ❌ Bad: Wait until first fetch // (background worker might trigger before configuration) ``` ### 2. Validate in Native Code Always validate configuration before use: ```java @Override public CompletableFuture> fetchContent(FetchContext context) { if (apiBaseUrl == null || activeDid == null || jwtSecret == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } // ... } ``` ### 3. Log Configuration (Sanitized) Log configuration for debugging, but sanitize sensitive data: ```java @Override public void configure(String apiBaseUrl, String activeDid, String jwtSecret) { this.apiBaseUrl = apiBaseUrl; this.activeDid = activeDid; this.jwtSecret = jwtSecret; Log.i(TAG, "Configured - API: " + apiBaseUrl + ", DID: " + activeDid.substring(0, Math.min(30, activeDid.length())) + "..."); // Don't log jwtSecret! } ``` ### 4. Handle Reconfiguration If configuration changes (e.g., user switches accounts), update gracefully: ```java @Override public void configure(String apiBaseUrl, String activeDid, String jwtSecret) { // Clear any cached state if DID changes if (this.activeDid != null && !this.activeDid.equals(activeDid)) { Log.i(TAG, "ActiveDid changed - clearing cache"); clearCache(); } this.apiBaseUrl = apiBaseUrl; this.activeDid = activeDid; this.jwtSecret = jwtSecret; } ``` ### 5. Secure JWT Secrets In production, consider: - Using secure storage (Android Keystore, iOS Keychain) - Deriving secret from device-specific keys - Using short-lived secrets with rotation ```typescript // Example: Get secret from secure storage const jwtSecret = await SecureStorage.get({ key: 'jwt_secret' }); await DailyNotification.configureNativeFetcher({ apiBaseUrl: apiUrl, activeDid: activeDid, jwtSecret: jwtSecret.value }); ``` --- ## Troubleshooting ### Error: "No native fetcher registered" **Cause**: Native fetcher not registered in `Application.onCreate()` **Solution**: ```java // In MyApplication.java @Override public void onCreate() { super.onCreate(); DailyNotificationPlugin.setNativeFetcher(new MyNativeFetcher()); } ``` ### Error: "Missing required parameters" **Cause**: One or more parameters not provided **Solution**: Ensure all three parameters are provided: ```typescript await DailyNotification.configureNativeFetcher({ apiBaseUrl: 'http://10.0.2.2:3000', // βœ… Required activeDid: 'did:ethr:0x...', // βœ… Required jwtSecret: 'secret' // βœ… Required }); ``` ### Fetcher Not Using Configuration **Cause**: Native fetcher not checking configuration before use **Solution**: Validate in `fetchContent()`: ```java if (apiBaseUrl == null || activeDid == null || jwtSecret == null) { Log.e(TAG, "Not configured!"); return Collections.emptyList(); } ``` ### Configuration Lost After App Restart **Cause**: Configuration is in-memory only (by design) **Solution**: Reconfigure after app startup: ```typescript // In app initialization platform.whenReady(() => { setupDailyNotifications(); // Calls configureNativeFetcher() }); ``` --- ## Related Documentation - **[Integration Point Refactor Guide](./INTEGRATION_REFACTOR_QUICK_START.md)** - Overall architecture - **[API Reference](./API.md)** - Complete plugin API - **[Native Fetcher Examples](../examples/native-fetcher-android.kt)** - Kotlin implementation examples - **[Test Implementation](../test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java)** - Complete reference implementation --- **Last Updated**: 2025-01-28 **Version**: 1.0.0