docs: reorganize docs into subdirs and fix links
- Keep only index, getting-started, invariants, performance, troubleshooting, and file-organization-summary in docs/ root - Add docs/architecture/ (storage, database interfaces, native fetcher) - Add docs/deployment/ (deployment-guide, DEPLOYMENT_CHECKLIST) - Add docs/compliance/ (accessibility, legal, observability) - Move integration guides and host-app docs to docs/integration/ - Move design/planning and prefetch docs to docs/design/ - Move Android consuming-app and comparison docs to docs/platform/android/ - Move DEPLOYMENT_SUMMARY and TODO-CLASSIFICATION to docs/progress/ - Archive deprecated platform-capability-reference to docs/_archive/ - Point platform-capability links to alarms/01-platform-capability-reference.md - Update docs/00-INDEX.md with new sections and paths - Fix cross-references in README, deployment, progress, design, testing, and test-app docs - Remove one-off COMMIT_MESSAGE.txt
This commit is contained in:
576
docs/architecture/NATIVE_FETCHER_CONFIGURATION.md
Normal file
576
docs/architecture/NATIVE_FETCHER_CONFIGURATION.md
Normal file
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user