577 lines
18 KiB
Markdown
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 org.timesafari.dailynotification.DailyNotificationPlugin;
|
|
import org.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 org.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
|
|
|