Files
daily-notification-plugin/docs/NATIVE_FETCHER_CONFIGURATION.md
Matthew Raymer c1cc8802f6 feat(fetcher): add configureNativeFetcher cross-platform API
Add configureNativeFetcher() plugin method to enable TypeScript configuration
of native fetchers with API credentials. This provides a cross-platform
mechanism for passing configuration from JavaScript to native code without
relying on platform-specific storage.

- Add configure() method to NativeNotificationContentFetcher interface
  (optional, defaults to no-op for fetchers that don't need config)
- Add configureNativeFetcher plugin method in DailyNotificationPlugin
- Add TypeScript definitions and comprehensive JSDoc
- Create NATIVE_FETCHER_CONFIGURATION.md documentation
- Update TestNativeFetcher to use real API endpoint (10.0.2.2:3000)
- Update DemoNativeFetcher Javadoc explaining configure() is optional
- Add configureNativeFetcher() call to demo app's configurePlugin()

Enables host apps to configure native fetchers from TypeScript, keeping
the interface consistent across Android, iOS, and web platforms.
2025-10-30 10:03:47 +00:00

577 lines
18 KiB
Markdown

# 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謹<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)<br>`"http://localhost:3000"` (iOS simulator)<br>`"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
```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<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
```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<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`
```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
<application android:name=".MyApplication">
<!-- ... -->
</application>
```
### 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<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
```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: <native exception message>"
console.error('Configuration failed:', error);
}
```
### Native Fetcher Validation
```java
@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
```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<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:
```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