# Integration Point Refactor Analysis

**Author**: Matthew Raymer
**Date**: 2025-10-29
**Status**: 🎯 **ANALYSIS** - Architectural refactoring proposal

> **See also:** [REFACTOR_NOTES.md](./REFACTOR_NOTES.md) for implementation context | [REFACTOR_NOTES_QUICK_START.md](./REFACTOR_NOTES_QUICK_START.md) for quick start

## Objective

Refactor the Daily Notification Plugin architecture so that **TimeSafari-specific integration logic is implemented by the Capacitor host app** rather than hardcoded in the plugin. This makes the plugin generic and reusable for other applications.

## Current Architecture

### Plugin-Side Integration (Current State)

The plugin currently contains TimeSafari-specific code:

1. **`TimeSafariIntegrationManager.java`**
   - Manages API server URL and DID lifecycle
   - Coordinates fetching and scheduling
   - Converts TimeSafari data to `NotificationContent`

2. **`EnhancedDailyNotificationFetcher.java`**
   - Makes HTTP calls to TimeSafari API endpoints:
     - `POST /api/v2/offers/person`
     - `POST /api/v2/offers/plans`
     - `POST /api/v2/report/plansLastUpdatedBetween`
   - Handles JWT authentication
   - Parses TimeSafari response formats

3. **`DailyNotificationJWTManager.java`**
   - Generates JWT tokens for TimeSafari API authentication
   - Manages JWT expiration and refresh

4. **TimeSafari Data Models**
   - Plugin imports `PlanSummary`, `OffersResponse`, `TimeSafariNotificationBundle`
   - Converts TimeSafari data structures to `NotificationContent[]`

### Current Data Flow

```
Host App (Capacitor)
  ↓ configure() with activeDidIntegration config
Plugin (DailyNotificationPlugin)
  ↓ initializes TimeSafariIntegrationManager
TimeSafariIntegrationManager
  ↓ creates EnhancedDailyNotificationFetcher
EnhancedDailyNotificationFetcher
  ↓ makes HTTP calls with JWT
TimeSafari API Server
  ↓ returns TimeSafariNotificationBundle
EnhancedDailyNotificationFetcher
  ↓ converts to NotificationContent[]
TimeSafariIntegrationManager
  ↓ saves & schedules
Plugin Storage & Scheduler
```

## Proposed Architecture

### App-Side Integration (Target State)

The plugin becomes a **generic notification scheduler** that accepts a **content fetcher callback** from the host app:

1. **Plugin exposes interface** for content fetching
2. **Host app implements** the TimeSafari API integration
3. **Plugin calls app's implementation** when fetching is needed
4. **Plugin remains generic** - no TimeSafari-specific code

### Proposed Data Flow

```
Host App (Capacitor)
  ↓ implements ContentFetcher interface
  ↓ provides fetcher callback to plugin
Plugin (DailyNotificationPlugin)
  ↓ calls app's fetcher callback
Host App's TimeSafari Integration
  ↓ makes HTTP calls, handles JWT, parses responses
TimeSafari API Server
  ↓ returns TimeSafariNotificationBundle
Host App's Integration
  ↓ converts to NotificationContent[]
  ↓ returns to plugin
Plugin
  ↓ saves & schedules (no TimeSafari knowledge)
Plugin Storage & Scheduler
```

## What Needs to Change

### 1. Plugin Interface Changes

#### New TypeScript Interface

```typescript
/**
 * Content fetcher callback that host app must implement
 */
export interface NotificationContentFetcher {
  /**
   * Fetch notification content from external source
   * 
   * This is called by the plugin when:
   * - Background fetch work is triggered
   * - Prefetch is scheduled before notification time
   * - Manual refresh is requested
   * 
   * @param context Context about why fetch was triggered
   * @returns Promise with notification content array
   */
  fetchContent(context: FetchContext): Promise<NotificationContent[]>;
}

export interface FetchContext {
  /**
   * Why the fetch was triggered
   */
  trigger: 'background_work' | 'prefetch' | 'manual' | 'scheduled';
  
  /**
   * Scheduled notification time (millis since epoch)
   */
  scheduledTime?: number;
  
  /**
   * When fetch was triggered (millis since epoch)
   */
  fetchTime: number;
  
  /**
   * Any additional context from plugin
   */
  metadata?: Record<string, unknown>;
}
```

#### New Plugin Method

```typescript
export interface DailyNotificationPlugin {
  // ... existing methods ...
  
  /**
   * Register content fetcher callback from host app
   * 
   * @param fetcher Implementation provided by host app
   */
  setContentFetcher(fetcher: NotificationContentFetcher): void;
}
```

### 2. Android Plugin Changes

#### New Java Interface

```java
/**
 * Content fetcher interface for host app to implement
 */
public interface NotificationContentFetcher {
    /**
     * Fetch notification content from external source
     * 
     * Called by plugin when background fetch is needed.
     * Host app implements TimeSafari API calls here.
     * 
     * @param context Fetch context with trigger info
     * @return Future with list of notification content
     */
    CompletableFuture<List<NotificationContent>> fetchContent(
        FetchContext context
    );
}

public class FetchContext {
    public final String trigger; // "background_work", "prefetch", "manual", "scheduled"
    public final Long scheduledTime; // Optional: when notification is scheduled
    public final long fetchTime; // When fetch was triggered
    public final Map<String, Object> metadata; // Additional context
    
    // Constructor, getters...
}
```

#### Plugin Implementation

```java
public class DailyNotificationPlugin extends Plugin {
    private @Nullable NotificationContentFetcher contentFetcher;
    
    @PluginMethod
    public void setContentFetcher(PluginCall call) {
        // Receive callback reference from JavaScript
        // Store for use in background workers
        // Implementation depends on how to bridge JS -> Java callback
    }
    
    // In DailyNotificationFetchWorker:
    private NotificationContent fetchContentWithTimeout() {
        if (contentFetcher == null) {
            // Fallback: return null, use cached content
            return null;
        }
        
        FetchContext context = new FetchContext(
            "background_work",
            scheduledTime,
            System.currentTimeMillis(),
            metadata
        );
        
        try {
            List<NotificationContent> results = 
                contentFetcher.fetchContent(context).get(30, TimeUnit.SECONDS);
            return results.isEmpty() ? null : results.get(0);
        } catch (Exception e) {
            // Handle timeout, errors
            return null;
        }
    }
}
```

### 3. What Gets Removed from Plugin

#### Delete These Classes:

- `TimeSafariIntegrationManager.java` - Move logic to host app
- `EnhancedDailyNotificationFetcher.java` - Move to host app
- TimeSafari-specific data model imports

#### Keep These (Generic):

- `DailyNotificationStorage` - Generic storage
- `DailyNotificationScheduler` - Generic scheduling
- `DailyNotificationPlugin` - Core plugin (modified)
- `DailyNotificationFetchWorker` - Modified to call callback
- `NotificationContent` - Generic data model

### 4. Host App Implementation

#### TypeScript Implementation

```typescript
import { DailyNotification, NotificationContentFetcher, FetchContext } 
  from '@timesafari/daily-notification-plugin';
import { TimeSafariAPI } from './timesafari-api'; // Host app's API client

class TimeSafariContentFetcher implements NotificationContentFetcher {
  constructor(
    private api: TimeSafariAPI,
    private activeDid: string
  ) {}
  
  async fetchContent(context: FetchContext): Promise<NotificationContent[]> {
    // 1. Generate JWT for authentication
    const jwt = await this.api.generateJWT(this.activeDid);
    
    // 2. Fetch TimeSafari data
    const [offersToPerson, offersToProjects, projectUpdates] = 
      await Promise.all([
        this.api.fetchOffersToPerson(jwt),
        this.api.fetchOffersToProjects(jwt),
        this.api.fetchProjectsLastUpdated(this.starredPlanIds, jwt)
      ]);
    
    // 3. Convert TimeSafari data to NotificationContent[]
    const contents: NotificationContent[] = [];
    
    // Convert offers to person
    if (offersToPerson?.data) {
      for (const offer of offersToPerson.data) {
        contents.push({
          id: `offer_${offer.id}`,
          title: `New Offer: ${offer.title}`,
          body: offer.description || '',
          scheduledTime: context.scheduledTime || Date.now() + 3600000,
          fetchTime: context.fetchTime,
          mediaUrl: offer.imageUrl
        });
      }
    }
    
    // Convert project updates
    if (projectUpdates?.data) {
      for (const update of projectUpdates.data) {
        contents.push({
          id: `project_${update.planSummary.jwtId}`,
          title: `${update.planSummary.name} Updated`,
          body: `New updates for ${update.planSummary.name}`,
          scheduledTime: context.scheduledTime || Date.now() + 3600000,
          fetchTime: context.fetchTime,
          mediaUrl: update.planSummary.image
        });
      }
    }
    
    return contents;
  }
}

// In host app initialization:
const plugin = new DailyNotification();
const fetcher = new TimeSafariContentFetcher(apiClient, activeDid);
plugin.setContentFetcher(fetcher);
```

#### Android Implementation (if needed)

If you need native Android implementation (for WorkManager background tasks):

```kotlin
// In host app's native code
class TimeSafariContentFetcher : NotificationContentFetcher {
    private val apiClient: TimeSafariAPIClient
    private val activeDid: String
    
    override fun fetchContent(context: FetchContext): CompletableFuture<List<NotificationContent>> {
        return CompletableFuture.supplyAsync {
            // Make TimeSafari API calls
            // Convert to NotificationContent[]
            // Return list
        }
    }
}
```

## Implementation Challenges

### Challenge 1: JavaScript → Java Callback Bridge

**Problem**: Capacitor plugins need to bridge JavaScript callbacks to Java for background workers.

**Solutions**:
1. **Bridge via Plugin Method** (Simplest)
   - Host app calls plugin method with callback ID
   - Plugin stores callback reference
   - Plugin calls back via Capacitor bridge when needed
   - Limitations: Callback must be serializable

2. **Bridge via Event System** (More Flexible)
   - Plugin emits events when fetch needed
   - Host app listens and responds
   - Plugin waits for response
   - More complex but more flexible

3. **Native Implementation** (Best Performance)
   - Host app provides native Java/Kotlin implementation
   - Direct Java interface (no JS bridge needed)
   - Best for background work
   - Requires host app to include native code

**Recommendation**: Start with Option 1 (Plugin Method), add Option 3 (Native) later if needed.

### Challenge 2: Background Worker Execution

**Problem**: Background WorkManager workers need to call host app's fetcher, but WorkManager runs in separate process.

**Solutions**:
1. **Store callback reference in SharedPreferences** (Limited)
   - Serialize callback config, not actual callback
   - Host app provides API endpoint URL + auth info
   - Plugin makes HTTP call to host app's endpoint
   - Host app handles TimeSafari logic server-side

2. **Use Capacitor Background Tasks** (Platform-specific)
   - iOS: BGTaskScheduler
   - Android: WorkManager with Capacitor bridge
   - Bridge JS callback from background task
   - Complex but works

3. **Native Background Implementation** (Recommended)
   - Host app provides native Java/Kotlin fetcher
   - No JS bridge needed in background
   - Direct Java interface
   - Best performance

**Recommendation**: Option 3 (Native) for background work, Option 1 for foreground.

### Challenge 3: Data Model Conversion

**Problem**: Plugin's generic `NotificationContent` vs TimeSafari's specific data structures.

**Solution**: 
- Host app owns conversion logic
- Plugin never sees TimeSafari data structures
- Plugin only knows about `NotificationContent[]`

This is actually **simpler** than current architecture.

### Challenge 4: Configuration Management

**Problem**: Plugin currently manages TimeSafari config (apiServer, activeDid, starredPlanIds).

**Solution**:
- Host app manages all TimeSafari config
- Plugin receives only fetcher callback
- Host app provides config to fetcher, not plugin
- Plugin becomes stateless regarding TimeSafari

## Migration Path

### Phase 1: Add Callback Interface (Non-Breaking)

1. Add `setContentFetcher()` method to plugin
2. Add callback interface definitions
3. Keep existing TimeSafari integration as fallback
4. Allow host app to opt-in to callback approach

### Phase 2: Move TimeSafari Code to Host App

1. Create `TimeSafariContentFetcher` in host app
2. Migrate `EnhancedDailyNotificationFetcher` logic
3. Migrate JWT generation logic
4. Test with callback approach

### Phase 3: Remove Plugin Integration (Breaking)

1. Remove `TimeSafariIntegrationManager` from plugin
2. Remove `EnhancedDailyNotificationFetcher` from plugin
3. Remove TimeSafari data model imports
4. Make callback approach required
5. Update documentation

## Effort Estimation

### Plugin Changes

- **Add callback interface**: 4-8 hours
- **Modify fetch worker**: 4-8 hours
- **Update documentation**: 2-4 hours
- **Testing**: 8-16 hours
- **Total**: 18-36 hours

### Host App Changes

- **Create TimeSafariContentFetcher**: 8-16 hours
- **Migrate API client logic**: 8-16 hours
- **Migrate JWT logic**: 4-8 hours
- **Testing**: 8-16 hours
- **Total**: 28-56 hours

### Total Effort

**Estimated**: 46-92 hours (6-12 days) for complete migration

## Benefits

1. **Generic Plugin**: Plugin can be used by any app, not just TimeSafari
2. **Clear Separation**: Plugin handles scheduling, app handles fetching
3. **Easier Testing**: Mock fetcher for unit tests
4. **Better Maintainability**: TimeSafari changes don't require plugin updates
5. **Flexibility**: Host app controls all API integration details

## Risks

1. **Breaking Changes**: Requires migration of host app
2. **Complexity**: Callback bridge adds complexity
3. **Performance**: JavaScript bridge may be slower for background work
4. **Testing Burden**: More complex integration testing

## Recommendations

### Short Term (Keep Current Architecture)

**Recommendation**: Keep current architecture for now, add callback interface as optional enhancement.

**Rationale**:
- Current architecture works
- Migration is significant effort
- Benefits may not justify costs yet
- Can add callback interface without removing existing code

### Long Term (Full Refactor)

**Recommendation**: Plan for full refactor when:
- Plugin needs to support multiple apps
- TimeSafari API changes frequently
- Plugin team wants to decouple from TimeSafari

**Approach**:
1. Phase 1: Add callback interface alongside existing code
2. Migrate TimeSafari app to use callbacks
3. Phase 3: Remove old integration code

## Alternative: Hybrid Approach

Keep TimeSafari integration in plugin but make it **optional and pluggable**:

```typescript
interface DailyNotificationPlugin {
  // Current approach (TimeSafari-specific)
  configure(options: ConfigureOptions): void;
  
  // New approach (generic callback)
  setContentFetcher(fetcher: NotificationContentFetcher): void;
  
  // Plugin uses callback if provided, falls back to TimeSafari integration
}
```

This allows:
- Existing apps continue working (no breaking changes)
- New apps can use callback approach
- Plugin gradually becomes generic

## Questions to Resolve

1. **Background Work**: How to call JS callback from background WorkManager?
   - Native implementation required?
   - Or HTTP callback to host app endpoint?

2. **Error Handling**: How does plugin handle fetcher errors?
   - Retry logic?
   - Fallback to cached content?
   - Event notifications?

3. **Configuration**: Where does TimeSafari config live?
   - Host app only?
   - Or plugin still needs some config?

4. **Testing**: How to test plugin without TimeSafari integration?
   - Mock fetcher interface?
   - Test fixtures?

---

## Improvement Directive Integration

This analysis has been superseded by the **Improvement Directive — Daily Notification Plugin "Integration Point" Refactor** (see below). The directive provides concrete, implementable specifications with clear interfaces, test contracts, and migration paths.

### Key Directive Decisions

1. **Dual-Path SPI**: Native Fetcher (required for background) + Optional JS Fetcher (foreground only)
2. **Background Reliability**: Native SPI only; JS fetcher explicitly disabled in background workers
3. **Reversibility**: Legacy TimeSafari code behind feature flag for one minor release
4. **Test Contract**: Single JSON fixture contract used by both native and JS paths

See full directive below for complete specifications.

---

## Improvement Directive — Daily Notification Plugin "Integration Point" Refactor

**Audience:** Plugin & TimeSafari app teams  
**Goal:** Convert the analysis into an implementable, low-risk refactor with crisp interfaces, background-compatibility, testability, and a reversible migration path.  
**Source reviewed:** `docs/integration-point-refactor-analysis.md`

---

## A) Decision Record (ADR-001): Architecture Shape

**Decision:** Ship a **dual-path SPI** where the plugin exposes:

1. a **Native Fetcher SPI** (Android/Kotlin, iOS/Swift) for background reliability, and
2. an **Optional JS Fetcher** for foreground/manual refresh & prototyping.

**Why:** Your analysis correctly targets decoupling, but the **WorkManager/BGTaskScheduler reality** means **native fetchers are the only dependable background path**. The JS callback remains useful for manual refresh, prefetch while the app is foregrounded, or rapid iteration. 

**Reversibility:** Keep the legacy TimeSafari path behind a feature flag for one minor release; remove in the next major.

**Exit Criteria:** Both fetcher paths produce identical `NotificationContent[]` and pass the same test contract.

---

## B) Public Interfaces (Final Form)

### B1. TypeScript (Host App)

```typescript
// core types
export interface NotificationContent {
  id: string;
  title: string;
  body?: string;
  scheduledTime?: number;  // epoch ms
  fetchTime: number;       // epoch ms
  mediaUrl?: string;
  ttlSeconds?: number;     // cache validity
  dedupeKey?: string;      // for idempotency
  priority?: 'min'|'low'|'default'|'high'|'max';
  metadata?: Record<string, unknown>;
}

export type FetchTrigger = 'background_work' | 'prefetch' | 'manual' | 'scheduled';

export interface FetchContext {
  trigger: FetchTrigger;
  scheduledTime?: number;
  fetchTime: number;
  metadata?: Record<string, unknown>;
}

export interface JsNotificationContentFetcher {
  fetchContent(context: FetchContext): Promise<NotificationContent[]>;
}
```

**Plugin API Additions**

```typescript
export interface DailyNotificationPlugin {
  setJsContentFetcher(fetcher: JsNotificationContentFetcher): void;   // foreground-only
  enableNativeFetcher(enable: boolean): Promise<void>;                // default true
  setPolicy(policy: SchedulingPolicy): Promise<void>;                 // see §C2
}
```

### B2. Android SPI (Host App native)

```kotlin
interface NativeNotificationContentFetcher {
    suspend fun fetchContent(context: FetchContext): List<NotificationContent>
}

data class FetchContext(
    val trigger: String,                 // enum mirror
    val scheduledTime: Long?,            // epoch ms
    val fetchTime: Long,                 // epoch ms
    val metadata: Map<String, Any?> = emptyMap()
)
```

**Registration:** Host app injects an implementation via a generated ServiceLoader or explicit setter in `Application.onCreate()`:

```kotlin
DailyNotification.setNativeFetcher(TimeSafariNativeFetcher())
```

> **Note:** JS fetcher remains available but **is not used** by background workers.

---

## C) Scheduling & Policy Guardrails

### C1. Background Reliability

* **Android:** WorkManager + `setRequiredNetworkType(CONNECTED)`, exponential backoff, content URI triggers optional, **no JS bridging** in background.
* **iOS:** `BGAppRefreshTask` + `BGProcessingTask` with earliestBeginDate windows; keep tasks short (< 30s), aggregate fetches.

### C2. `SchedulingPolicy`

```typescript
export interface SchedulingPolicy {
  prefetchWindowMs?: number;        // how early to prefetch before schedule
  retryBackoff: { minMs: number; maxMs: number; factor: number; jitterPct: number };
  maxBatchSize?: number;            // upper bound per run
  dedupeHorizonMs?: number;         // window for dedupeKey suppression
  cacheTtlSeconds?: number;         // default if item.ttlSeconds absent
  exactAlarmsAllowed?: boolean;     // guarded by OS version & battery policy
}
```

### C3. Idempotency & De-duplication

* The plugin maintains a **recent keys bloom/filter** using `dedupeKey` (fallback `id`) for `dedupeHorizonMs`.
* No notification is enqueued if `(dedupeKey, title, body)` unchanged within horizon.

---

## D) Security & Config Partition

### D1. Secrets & Tokens

* **JWT generation lives in the host app**, not the plugin. For extra hardening consider **DPoP** or **JWE-wrapped access tokens** if your API supports it.
* Rotate keys; keep key refs in host app secure storage; never serialize tokens through JS bridges in background.

### D2. Least-Knowledge Plugin

* Plugin stores only generic `NotificationContent`.
* **No TimeSafari DTOs** in plugin artifacts. (As your analysis recommends.) 

---

## E) Failure Taxonomy & Handling

| Class           | Examples               | Plugin Behavior                                   |
| --------------- | ---------------------- | ------------------------------------------------- |
| **Retryable**   | 5xx, network, timeouts | Backoff per policy; stop on `maxAttempts`         |
| **Unretryable** | 4xx auth, schema error | Log & surface event; skip batch; keep schedule    |
| **Partial**     | Some items bad         | Enqueue valid subset; metric + structured error   |
| **Over-quota**  | 429                    | Honor `Retry-After` if present; otherwise backoff |

**Cache fallback:** If fetch fails, reuse **unexpired cached contents** (by item `ttlSeconds` or default policy).

---

## F) Metrics, Logs, Events

* **Metrics:** `fetch_duration_ms`, `fetch_success`, `fetch_fail_class`, `items_fetched`, `items_enqueued`, `deduped_count`, `cache_hits`.
* **Structured logs:** Correlate by `run_id`, include `trigger`, backoff stage, and per-item `dedupeKey`.
* **Event bus:** Emit `onFetchStart`, `onFetchEnd`, `onFetchError`, `onEnqueue` (debug UI can subscribe).

---

## G) Test Contract & Fixtures

### G1. Golden Contract

A **single JSON fixture contract** used by both native & JS fetchers:

```json
{
  "context": {"trigger":"scheduled","scheduledTime":1730179200000,"fetchTime":1730175600000},
  "output": [
    {"id":"offer_123","title":"New Offer","body":"...","fetchTime":1730175600000,"dedupeKey":"offer_123","ttlSeconds":86400}
  ]
}
```

* CI runs the same contract through **(1) JS fetcher** (foreground test harness) and **(2) Native fetcher** (instrumented test) and compares normalized outputs.

### G2. Failure Sims

* Network chaos (drop, latency), 401→refresh→200, 429 with `Retry-After`, malformed item (should be skipped, not crash).

---

## H) Migration Plan (Minor → Major)

**Minor (X.Y):**

1. Add `setJsContentFetcher`, `enableNativeFetcher`, `setPolicy`.
2. Implement **Native Fetcher SPI** & plugin side registry.
3. Keep legacy TimeSafari code behind `useLegacyIntegration` flag (default **false**).

**Major (X+1.0):**

1. Remove legacy TimeSafari classes (`TimeSafariIntegrationManager`, `EnhancedDailyNotificationFetcher`, etc.).
2. Publish migration guide & codemod mapping old config → SPI registration. 

---

## I) Pull-Request Breakdown (7 PRs)

1. **PR1 — SPI Shell:** Kotlin/Swift interfaces, TS types, `SchedulingPolicy`, no behavior change.
2. **PR2 — Background Workers:** Wire Native SPI in WorkManager/BGTasks; metrics skeleton; cache store.
3. **PR3 — JS Fetcher Path:** Foreground bridge, event bus, dev harness screen.
4. **PR4 — Dedupe/Idempotency:** Bloom/filter store + tests; `dedupeKey` enforcement.
5. **PR5 — Failure Policy:** Retry taxonomy, backoff, cache fallback; structured errors.
6. **PR6 — Docs & Samples:** Minimal host app examples (JS + Kotlin), migration notes.
7. **PR7 — Feature Flag Legacy:** Guard & regression tests; changelog for major removal.

---

## J) Acceptance Criteria (Definition of Done)

* **Background Reliability:** Native fetcher runs with app killed; JS fetcher **never** required for background.
* **Parity:** Same inputs → same `NotificationContent[]` via JS & Native paths (contract tests green).
* **No TimeSafari in Plugin:** No imports, types, or endpoints leak into plugin modules. 
* **Safety:** No notifications emitted twice within `dedupeHorizonMs` for identical `dedupeKey`.
* **Observability:** Metrics visible; error classes distinguish retryable vs. not; logs correlated by run.
* **Docs:** Host app integration guide includes **copy-paste** Kotlin & TS skeletons; migration guide from legacy.
* **Battery & Quotas:** Backoff respected; tasks kept <30s; policy toggles documented; exact alarms gated.

---

## K) Open Questions (Resolve Before PR3)

1. **iOS capabilities:** Which BG task identifiers and permitted intervals will Product accept (refresh vs processing)?
2. **Token Hardening:** Do we want DPoP or short-lived JWE now or later?
3. **User Controls:** Should we surface a per-plan "snooze/mute" map inside `metadata` with plugin-side persistence?

---

## L) Example Skeletons

### L1. Android Native Fetcher (Host App)

```kotlin
class TimeSafariNativeFetcher(
    private val api: TimeSafariApi,
    private val tokenProvider: TokenProvider
) : NativeNotificationContentFetcher {

    override suspend fun fetchContent(context: FetchContext): List<NotificationContent> {
        val jwt = tokenProvider.freshToken()
        val updates = api.updates(jwt)     // domain-specific
        val offers  = api.offers(jwt)

        return buildList {
            for (o in offers) add(
                NotificationContent(
                    id = "offer_${o.id}",
                    title = "New Offer: ${o.title}",
                    body = o.description ?: "",
                    scheduledTime = context.scheduledTime ?: (System.currentTimeMillis() + 3_600_000),
                    fetchTime = context.fetchTime,
                    mediaUrl = o.imageUrl,
                    ttlSeconds = 86_400,
                    dedupeKey = "offer_${o.id}"
                )
            )
            // … map updates similarly …
        }
    }
}
```

### L2. JS Fetcher Registration (Foreground Only)

```typescript
const fetcher: JsNotificationContentFetcher = {
  async fetchContent(ctx) {
    const jwt = await api.generateJWT(activeDid);
    const { offers, updates } = await api.fetchEverything(jwt);
    return mapToNotificationContent(offers, updates, ctx);
  }
};

DailyNotification.setJsContentFetcher(fetcher);
await DailyNotification.setPolicy({
  retryBackoff: { minMs: 2000, maxMs: 600000, factor: 2, jitterPct: 20 },
  dedupeHorizonMs: 24*60*60*1000,
  cacheTtlSeconds: 6*60*60
});
```

---

## M) Docs to Produce

* **INTEGRATION_GUIDE.md** (host app: Native vs JS, when to use which, copy-paste snippets).
* **SCHEDULING_POLICY.md** (all knobs with OS caveats).
* **MIGRATION_GUIDE_X→X+1.md** (flag removal plan, old→new mapping).
* **TEST_CONTRACTS.md** (fixtures, CI commands).

---

## N) Risk Register & Mitigations

* **Risk:** JS callback used in background → fetch starves.
  **Mitigation:** Disable in background by design; lint rule + runtime guard.
* **Risk:** Duplicate notifications.
  **Mitigation:** `dedupeKey` + horizon filter; contract tests to assert no dupes.
* **Risk:** Token leakage through JS.
  **Mitigation:** Tokens stay in native path for background; JS path only in foreground; redact logs.
* **Risk:** Major removal shocks dependents.
  **Mitigation:** Two-release migration window + codemod and samples.

---

### Final Note

Your analysis nails the **separation of concerns** and enumerates the **callback bridge challenges**. This directive turns it into a concrete, staged plan with **native-first background fetch**, **JS optionality**, **clear policies**, and **verifiable acceptance criteria** while preserving a **rollback path**.

---

**Status**: Improvement Directive - Active Implementation Plan
**Next Steps**: Begin PR1 (SPI Shell) implementation
**Dependencies**: None
**Stakeholders**: Plugin developers, TimeSafari app developers

