Browse Source
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.master
5 changed files with 954 additions and 30 deletions
@ -0,0 +1,576 @@ |
|||
# 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 |
|||
|
|||
Loading…
Reference in new issue