feat(notifications): sync starred plans to native plugin on star/unstar

Add syncStarredPlansToNativePlugin() and call it from AccountViewView
(schedule + initializeState) and ProjectViewView.toggleStar when New
Activity is enabled so Android prefetch uses the current starred list.

Update notification-from-api-call.md with the helper and file references.
This commit is contained in:
Jose Olarte III
2026-03-31 15:57:22 +08:00
parent 2c8aa21fa5
commit 230dc52974
5 changed files with 83 additions and 65 deletions

View File

@@ -3,6 +3,7 @@
**Purpose:** Integrate the daily-notification-plugins second feature—the **daily, API-driven message**—into the crowd-funder (TimeSafari) app. The first feature (daily static reminder) is already integrated; this document covers the plan, completed work, and remaining tasks for the API-driven flow.
**References:**
- Plugin: `daily-notification-plugin` (INTEGRATION_GUIDE.md, definitions.ts)
- Alignment outline: `doc/daily-notification-alignment-outline.md`
- Help copy: `HelpNotificationTypesView.vue` (“New Activity Notifications”)
@@ -28,32 +29,29 @@ The app must:
## Tasks Finished
- [x] **Configure native fetcher on startup and identity**
- **Configure native fetcher on startup and identity**
- Added `configureNativeFetcherIfReady()` in `src/services/notifications/nativeFetcherConfig.ts` (reads `activeDid` and `apiServer` from DB, gets JWT via `getHeaders(did)`, calls `DailyNotification.configureNativeFetcher()`).
- Called from `main.capacitor.ts` after the 2s delay (with deep link registration).
- Called from `AccountViewView.initializeState()` when on native and `activeDid` is set; when New Activity is enabled, also calls `updateStarredPlans(settings.starredPlanHandleIds)`.
- [x] **Implement real API calls in Android native fetcher**
- **Implement real API calls in Android native fetcher**
- `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java` implements `NativeNotificationContentFetcher`: POST to `/api/v2/report/plansLastUpdatedBetween` with `planIds` (from SharedPreferences `daily_notification_timesafari` / `starredPlanIds`) and `afterId`; parses response into `NotificationContent` list; updates `last_acked_jwt_id` for pagination.
- Registered in `MainActivity.onCreate()` via `DailyNotificationPlugin.setNativeFetcher(new TimeSafariNativeFetcher(this))`.
- [x] **Sync starred plan IDs**
- When user enables New Activity, `scheduleNewActivityDualNotification()` calls `DailyNotification.updateStarredPlans({ planIds: settings.starredPlanHandleIds ?? [] })`.
- When Account view loads and New Activity is on, `initializeState()` calls `updateStarredPlans(settings.starredPlanHandleIds)` so the plugin has the latest list.
- [x] **Dual schedule config and scheduling**
- **Sync starred plan IDs**
- Shared helper `syncStarredPlansToNativePlugin(planIds)` in `src/services/notifications/syncStarredPlansToNativePlugin.ts` (exported from `src/services/notifications/index.ts`) calls `DailyNotification.updateStarredPlans` on native only; ignores `UNIMPLEMENTED`.
- When user enables New Activity, `scheduleNewActivityDualNotification()` uses the helper with `settings.starredPlanHandleIds ?? []`.
- When Account view loads and New Activity is on, `initializeState()` uses the helper with the same list.
- When the user stars or unstars on a project (`ProjectViewView.toggleStar`), after a successful settings save, the helper runs if `notifyingNewActivityTime` is set so prefetch sees the current list without reopening Account.
- **Dual schedule config and scheduling**
- Added `src/services/notifications/dualScheduleConfig.ts`: `timeToCron()`, `timeToCronFiveMinutesBefore()`, `buildDualScheduleConfig({ notifyTime, title?, body? })` (contentFetch 5 min before, userNotification at chosen time).
- When user enables New Activity and picks a time, app calls `DailyNotification.scheduleDualNotification({ config })` with this config.
- When user disables New Activity, app calls `DailyNotification.cancelDualSchedule()`.
- [x] **UI for New Activity notification**
- **UI for New Activity notification**
- Unhid the “New Activity Notification” block in `AccountViewView.vue` (toggle + accessibility).
- Enable flow: time dialog → save settings → on native, `scheduleNewActivityDualNotification(timeText)` (configure fetcher, updateStarredPlans, scheduleDualNotification).
- Disable flow: on native, `cancelDualSchedule()` then save and clear settings.
- Added `starredPlanHandleIds` to `AccountSettings` in `interfaces/accountView.ts`.
- [x] **Exports**
- `src/services/notifications/index.ts` exports `configureNativeFetcherIfReady`, `buildDualScheduleConfig`, `timeToCron`, `timeToCronFiveMinutesBefore`, and `DualScheduleConfigInput`.
- **Exports**
- `src/services/notifications/index.ts` exports `configureNativeFetcherIfReady`, `syncStarredPlansToNativePlugin`, `buildDualScheduleConfig`, `timeToCron`, `timeToCronFiveMinutesBefore`, and `DualScheduleConfigInput`.
---
@@ -61,48 +59,45 @@ The app must:
### iOS
- [ ] **Confirm iOS native fetcher / dual schedule**
Plugin exposes `configureNativeFetcher` on iOS. Confirm whether the plugin expects an iOS-specific native fetcher registration (similar to Androids `setNativeFetcher`) and, if so, register a TimeSafari fetcher implementation for iOS so API-driven notifications work on iPhone.
- [ ] **Verify dual schedule on iOS**
Test `scheduleDualNotification` and `cancelDualSchedule` on iOS; ensure content fetch and user notification fire at the expected times and that foreground/background behavior matches expectations.
- **Confirm iOS native fetcher / dual schedule**
Plugin exposes `configureNativeFetcher` on iOS. Confirm whether the plugin expects an iOS-specific native fetcher registration (similar to Androids `setNativeFetcher`) and, if so, register a TimeSafari fetcher implementation for iOS so API-driven notifications work on iPhone.
- **Verify dual schedule on iOS**
Test `scheduleDualNotification` and `cancelDualSchedule` on iOS; ensure content fetch and user notification fire at the expected times and that foreground/background behavior matches expectations.
### Testing and hardening
- [ ] **Test full flow on Android**
Enable New Activity, set time, wait for prefetch and notification (or use a short rollover for testing). Confirm notification shows with API-derived or fallback content.
- [ ] **Test full flow on iOS**
Same as Android: enable, set time, verify prefetch and notification delivery and content.
- [ ] **Test with no starred plans**
Enable New Activity with empty `starredPlanHandleIds`; confirm no crash and sensible fallback (e.g. “No updates in your starred projects” or similar).
- [ ] **Test JWT expiry**
Ensure behavior when the token passed to `configureNativeFetcher` has expired (e.g. app in background for a long time); document or implement refresh (e.g. re-call `configureNativeFetcherIfReady` on foreground or when opening Account).
- **Test full flow on Android**
Enable New Activity, set time, wait for prefetch and notification (or use a short rollover for testing). Confirm notification shows with API-derived or fallback content.
- **Test full flow on iOS**
Same as Android: enable, set time, verify prefetch and notification delivery and content.
- **Test with no starred plans**
Enable New Activity with empty `starredPlanHandleIds`; confirm no crash and sensible fallback (e.g. “No updates in your starred projects” or similar).
- **Test JWT expiry**
Ensure behavior when the token passed to `configureNativeFetcher` has expired (e.g. app in background for a long time); document or implement refresh (e.g. re-call `configureNativeFetcherIfReady` on foreground or when opening Account).
### Optional enhancements
- [ ] **Offers endpoints**
Extend `TimeSafariNativeFetcher` (and any iOS fetcher) to call offers endpoints (e.g. `offers`, `offersToPlansOwnedByMe`) and merge with project-update content for richer notifications.
- [ ] **Sync starred plans on star/unstar**
When the user stars or unstars a project elsewhere in the app, call `updateStarredPlans` so the plugin always has the current list without requiring a visit to Account.
- [ ] **Documentation**
Add a short “New Activity notifications” section to BUILDING.md or a user-facing help page describing how the feature works and how to troubleshoot (e.g. no notification, wrong content, JWT/API errors).
- **Offers endpoints**
Extend `TimeSafariNativeFetcher` (and any iOS fetcher) to call offers endpoints (e.g. `offers`, `offersToPlansOwnedByMe`) and merge with project-update content for richer notifications.
- **Documentation**
Add a short “New Activity notifications” section to BUILDING.md or a user-facing help page describing how the feature works and how to troubleshoot (e.g. no notification, wrong content, JWT/API errors).
---
## File Reference
| Area | Files |
|------|--------|
| Fetcher config | `src/services/notifications/nativeFetcherConfig.ts` |
| Dual schedule config | `src/services/notifications/dualScheduleConfig.ts` |
| Notification exports | `src/services/notifications/index.ts` |
| Startup | `src/main.capacitor.ts` |
| Account UI and flow | `src/views/AccountViewView.vue` |
| Settings type | `src/interfaces/accountView.ts` |
| Area | Files |
| ---------------------- | ----------------------------------------------------------------------- |
| Fetcher config | `src/services/notifications/nativeFetcherConfig.ts` |
| Starred list → plugin | `src/services/notifications/syncStarredPlansToNativePlugin.ts` |
| Dual schedule config | `src/services/notifications/dualScheduleConfig.ts` |
| Notification exports | `src/services/notifications/index.ts` |
| Startup | `src/main.capacitor.ts` |
| Account UI and flow | `src/views/AccountViewView.vue` |
| Project star / unstar | `src/views/ProjectViewView.vue` (`toggleStar`) |
| Settings type | `src/interfaces/accountView.ts` |
| Android native fetcher | `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java` |
| Android registration | `android/app/src/main/java/app/timesafari/MainActivity.java` |
| Android registration | `android/app/src/main/java/app/timesafari/MainActivity.java` |

View File

@@ -18,6 +18,7 @@ export { NativeNotificationService } from "./NativeNotificationService";
export { WebPushNotificationService } from "./WebPushNotificationService";
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
export { syncStarredPlansToNativePlugin } from "./syncStarredPlansToNativePlugin";
export {
buildDualScheduleConfig,
timeToCron,

View File

@@ -0,0 +1,30 @@
import { Capacitor } from "@capacitor/core";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
import { logger } from "@/utils/logger";
/**
* Pushes starred plan handle IDs to the native Daily Notification plugin so
* Android TimeSafariNativeFetcher uses the current list for prefetch
* (plansLastUpdatedBetween planIds).
*
* No-op on web. Ignores UNIMPLEMENTED when the plugin omits the method on some builds.
*/
export async function syncStarredPlansToNativePlugin(
planIds: string[],
): Promise<void> {
if (!Capacitor.isNativePlatform()) {
return;
}
try {
await DailyNotification.updateStarredPlans({ planIds });
} catch (e: unknown) {
if ((e as { code?: string })?.code === "UNIMPLEMENTED") {
return;
}
logger.warn(
"[syncStarredPlansToNativePlugin] updateStarredPlans failed",
e,
);
}
}

View File

@@ -831,6 +831,7 @@ import {
NotificationService,
configureNativeFetcherIfReady,
buildDualScheduleConfig,
syncStarredPlansToNativePlugin,
} from "@/services/notifications";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
// Profile data interface (inlined from ProfileService)
@@ -1130,14 +1131,7 @@ export default class AccountViewView extends Vue {
void configureNativeFetcherIfReady(this.activeDid);
if (this.notifyingNewActivity) {
const planIds = settings?.starredPlanHandleIds ?? [];
// Capacitor proxy always exposes a function per key; native may omit the method → UNIMPLEMENTED.
void DailyNotification.updateStarredPlans({ planIds }).catch(
(e: unknown) => {
if ((e as { code?: string })?.code !== "UNIMPLEMENTED") {
logger.warn("[AccountViewView] updateStarredPlans failed", e);
}
},
);
void syncStarredPlansToNativePlugin(planIds);
}
}
}
@@ -1283,16 +1277,7 @@ export default class AccountViewView extends Vue {
await configureNativeFetcherIfReady(this.activeDid);
const settings = await this.$accountSettings();
const planIds = settings?.starredPlanHandleIds ?? [];
try {
await DailyNotification.updateStarredPlans({ planIds });
} catch (e: unknown) {
if ((e as { code?: string })?.code !== "UNIMPLEMENTED") {
throw e;
}
logger.debug(
"[AccountViewView] updateStarredPlans not on native plugin; continuing to scheduleDualNotification",
);
}
await syncStarredPlansToNativePlugin(planIds);
const config = buildDualScheduleConfig({ notifyTime: time24h });
// Diagnostic: log what Capacitor sees at call time (helps debug UNIMPLEMENTED)
const cap = (typeof window !== "undefined" &&

View File

@@ -647,6 +647,7 @@ import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import { copyToClipboard } from "../services/ClipboardService";
import { logger } from "../utils/logger";
import { syncStarredPlansToNativePlugin } from "@/services/notifications";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
@@ -1545,6 +1546,9 @@ export default class ProjectViewView extends Vue {
);
if (result) {
this.isStarred = true;
if (settings.notifyingNewActivityTime) {
void syncStarredPlansToNativePlugin(newStarredIds);
}
} else {
// eslint-disable-next-line no-console
logger.error("Got a bad result from SQL update to star a project.");
@@ -1567,6 +1571,9 @@ export default class ProjectViewView extends Vue {
);
if (result) {
this.isStarred = false;
if (settings.notifyingNewActivityTime) {
void syncStarredPlansToNativePlugin(updatedIds);
}
} else {
// eslint-disable-next-line no-console
logger.error("Got a bad result from SQL update to unstar a project.");