You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

18 KiB

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()?

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

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่ฌน<void>;
}

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<void> resolves
  • Failure: Promise<void> rejects with error message

Example Usage

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:

public interface NativeNotificationContentFetcher {
    CompletableFuture<List<NotificationContent>> 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

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<List<NotificationContent>> 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

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:

<application android:name=".MyApplication">
    <!-- ... -->
</application>

2. Configure from TypeScript

File: src/services/NotificationService.ts

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

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<List<NotificationContent>> 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<NotificationContent> 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

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: <native exception message>"
  console.error('Configuration failed:', error);
}

Native Fetcher Validation

@Override
public CompletableFuture<List<NotificationContent>> 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
// โœ… 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:

// 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:

// โœ… 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:

@Override
public CompletableFuture<List<NotificationContent>> 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:

@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:

@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
// 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:

// 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:

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():

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:

// In app initialization
platform.whenReady(() => {
  setupDailyNotifications(); // Calls configureNativeFetcher()
});


Last Updated: 2025-01-28
Version: 1.0.0