# Cross-Platform Storage Pattern for Starred Plans

**Author**: Matthew Raymer  
**Date**: 2025-10-31  
**Status**: 🎯 **REFERENCE** - Pattern for platform-specific storage consistency

## Problem

Native fetcher implementations need to read starred plan IDs that are stored by the plugin. However:
- **Android**: Uses `SharedPreferences`
- **iOS**: Uses `UserDefaults`
- **Background workers**: Cannot use Capacitor bridge (unreliable in background)

This creates a requirement that **both plugin and native fetcher** use the **same platform storage mechanism** on each platform.

## Architecture Flow

### How Plan IDs Flow Through the System

```
TypeScript/Vue (Foreground)
  │
  ├─► DailyNotification.updateStarredPlans({ planIds: [...] })
  │   └─► Capacitor Bridge (JS → Native)
  │       │
  │       └─► Plugin.updateStarredPlans() (Native Code)
  │           │
  │           ├─► Android: SharedPreferences.put("starredPlanIds", json)
  │           └─► iOS: UserDefaults.set("starredPlanIds", json)
  │
Background Worker (WorkManager/BGTaskScheduler)
  │
  └─► Native Fetcher.fetchContent(context)
      │
      ├─► Android: SharedPreferences.get("starredPlanIds")
      └─► iOS: UserDefaults.string(forKey: "starredPlanIds")
```

### Key Points

1. **TypeScript → Plugin**: Plan IDs are passed **via Capacitor bridge** from Vue/React components
2. **Plugin → Native Storage**: Plugin stores to **platform-specific storage** (SharedPreferences/UserDefaults)
3. **Native Fetcher → Native Storage**: Native fetcher **reads directly** from platform storage (NO bridge needed)

### Why This Architecture?

- **Foreground (TypeScript)**: Can reliably use Capacitor bridge → Plugin
- **Background Workers**: **Cannot** use Capacitor bridge (unreliable/blocking) → Must read from native storage directly
- **Storage as Bridge**: Native storage acts as the "communication layer" between foreground and background

## Solution: Platform-Specific Storage with Consistent Keys

### Pattern

Each platform uses its native storage API, but with **consistent key names** across plugin and native fetcher:

| Platform | Storage API | SharedPreferences Name | Key Name |
|----------|------------|------------------------|----------|
| **Android** | `SharedPreferences` | `"daily_notification_timesafari"` | `"starredPlanIds"` |
| **iOS** | `UserDefaults` | `UserDefaults.standard` (app suite) | `"starredPlanIds"` |

### Android Implementation

**Plugin Storage** (`DailyNotificationPlugin.updateStarredPlans()`):
```java
SharedPreferences preferences = getContext()
    .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE);

org.json.JSONArray jsonArray = new org.json.JSONArray();
for (String planId : planIds) {
    jsonArray.put(planId);
}

preferences.edit()
    .putString("starredPlanIds", jsonArray.toString())
    .putLong("starredPlansUpdatedAt", System.currentTimeMillis())
    .apply();
```

**Native Fetcher Reading** (`TestNativeFetcher.getStarredPlanIds()`):
```java
SharedPreferences pluginPrefs = appContext.getSharedPreferences(
    "daily_notification_timesafari", Context.MODE_PRIVATE);
String idsJson = pluginPrefs.getString("starredPlanIds", "[]");
// Parse JSON array...
```

### iOS Implementation (Required)

**Plugin Storage** (`DailyNotificationPlugin.updateStarredPlans()`):
```swift
func updateStarredPlans(call: CAPPluginCall) {
    guard let planIds = call.getArray("planIds", String.self) else {
        call.reject("planIds is required")
        return
    }
    
    // Store in UserDefaults with same key as Android
    let jsonData = try? JSONSerialization.data(withJSONObject: planIds)
    let jsonString = String(data: jsonData!, encoding: .utf8)
    
    UserDefaults.standard.set(jsonString, forKey: "starredPlanIds")
    UserDefaults.standard.set(Date().timeIntervalSince1970 * 1000, 
                              forKey: "starredPlansUpdatedAt")
    
    call.resolve(["success": true, "planIdsCount": planIds.count])
}
```

**Native Fetcher Reading** (`TimeSafariNativeFetcher.getStarredPlanIds()`):
```swift
private func getStarredPlanIds() -> [String] {
    guard let jsonString = UserDefaults.standard.string(forKey: "starredPlanIds"),
          let jsonData = jsonString.data(using: .utf8),
          let planIds = try? JSONSerialization.jsonObject(with: jsonData) as? [String] else {
        return []
    }
    return planIds
}
```

## iOS Implementation Checklist

When implementing iOS, follow this exact pattern:

### Step 1: Add Plugin Method (`DailyNotificationPlugin.swift`)

```swift
@objc func updateStarredPlans(_ call: CAPPluginCall) {
    guard let planIds = call.getArray("planIds", String.self) else {
        call.reject("planIds is required")
        return
    }
    
    // Store in UserDefaults with same key as Android
    do {
        let jsonData = try JSONSerialization.data(withJSONObject: planIds)
        let jsonString = String(data: jsonData, encoding: .utf8) ?? "[]"
        
        UserDefaults.standard.set(jsonString, forKey: "starredPlanIds")
        UserDefaults.standard.set(Date().timeIntervalSince1970 * 1000, 
                                  forKey: "starredPlansUpdatedAt")
        
        print("DNP|UPDATE_STARRED_PLANS count=\(planIds.count)")
        
        call.resolve([
            "success": true,
            "planIdsCount": planIds.count,
            "updatedAt": Int64(Date().timeIntervalSince1970 * 1000)
        ])
    } catch {
        call.reject("Failed to serialize plan IDs: \(error.localizedDescription)")
    }
}

@objc func getStarredPlans(_ call: CAPPluginCall) {
    guard let jsonString = UserDefaults.standard.string(forKey: "starredPlanIds"),
          let jsonData = jsonString.data(using: .utf8),
          let planIds = try? JSONSerialization.jsonObject(with: jsonData) as? [String] else {
        call.resolve([
            "planIds": [],
            "count": 0,
            "updatedAt": 0
        ])
        return
    }
    
    let updatedAt = UserDefaults.standard.double(forKey: "starredPlansUpdatedAt")
    
    call.resolve([
        "planIds": planIds,
        "count": planIds.count,
        "updatedAt": Int64(updatedAt)
    ])
}
```

### Step 2: Add Native Fetcher Reading (`TimeSafariNativeFetcher.swift`)

```swift
private func getStarredPlanIds() -> [String] {
    guard let jsonString = UserDefaults.standard.string(forKey: "starredPlanIds"),
          let jsonData = jsonString.data(using: .utf8),
          let planIds = try? JSONSerialization.jsonObject(with: jsonData) as? [String] else {
        print("TestNativeFetcher: No starred plan IDs found in UserDefaults")
        return []
    }
    
    print("TestNativeFetcher: Loaded \(planIds.count) starred plan IDs from UserDefaults")
    return planIds
}
```

### Step 3: Register Plugin Methods

Add to `DailyNotificationPlugin.swift` `load()` method:
```swift
override public func load() {
    super.load()
    setupBackgroundTasks()
    // Register methods
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(updateStarredPlans),
        name: NSNotification.Name("DailyNotification.updateStarredPlans"),
        object: nil
    )
}
```

## Key Consistency Rules

### ✅ DO:
- Use **same SharedPreferences name** on Android: `"daily_notification_timesafari"`
- Use **same UserDefaults key** on iOS: `"starredPlanIds"` (same key name!)
- Store as **JSON array string** for cross-language compatibility
- Use **same key names** across plugin and native fetcher
- **TypeScript passes planIds via Capacitor bridge** → Plugin stores to native storage → Native fetcher reads from native storage

### ❌ DON'T:
- Use different storage locations for plugin vs native fetcher
- Use Capacitor Preferences API in native fetchers (background workers can't access bridge)
- Store in different formats (e.g., JSON vs array) between plugin and fetcher
- Try to use Capacitor bridge in background workers (unreliable/blocking)

## Testing Checklist

When implementing iOS native fetcher:

- [ ] Plugin `updateStarredPlans()` stores to `UserDefaults.standard` with key `"starredPlanIds"`
- [ ] Native fetcher `getStarredPlanIds()` reads from `UserDefaults.standard` with key `"starredPlanIds"`
- [ ] Both use same JSON array string format
- [ ] Storage format matches Android (JSON array string)
- [ ] TypeScript `updateStarredPlans()` call works via Capacitor bridge
- [ ] Background worker can read plan IDs without Capacitor bridge

## Migration Notes

If storage format changes:
- Both Android and iOS implementations must be updated together
- Migration code should handle both platforms consistently
- Test plan ID persistence across app restarts on both platforms

## Summary

**Question**: Are planIds passed via TypeScript in Vue over the Capacitor bridge?

**Answer**: **YES** - Here's the flow:

1. **Vue/TypeScript** (Foreground): `DailyNotification.updateStarredPlans({ planIds: [...] })`
2. **Capacitor Bridge**: Transfers data from JS → Native (Android/iOS)
3. **Plugin Native Code**: Receives planIds, stores to platform storage
   - Android: `SharedPreferences.put("starredPlanIds", json)`
   - iOS: `UserDefaults.set("starredPlanIds", json)`
4. **Background Worker**: Reads directly from platform storage (NO bridge)
   - Android: `SharedPreferences.get("starredPlanIds")`
   - iOS: `UserDefaults.string(forKey: "starredPlanIds")`

**Key Insight**: The Capacitor bridge is used **only for foreground operations** (TypeScript → Plugin). Background workers **cannot use the bridge**, so they read from the same native storage where the plugin wrote the data.

This is why **consistent key names** are critical - it's the "contract" between plugin storage and native fetcher reading.

