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:
@@ -3,6 +3,7 @@
|
||||
**Purpose:** Integrate the daily-notification-plugin’s 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**
|
||||
- **Confirm iOS native fetcher / dual schedule**
|
||||
Plugin exposes `configureNativeFetcher` on iOS. Confirm whether the plugin expects an iOS-specific native fetcher registration (similar to Android’s `setNativeFetcher`) and, if so, register a TimeSafari fetcher implementation for iOS so API-driven notifications work on iPhone.
|
||||
|
||||
- [ ] **Verify dual schedule on iOS**
|
||||
- **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**
|
||||
- **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**
|
||||
- **Test full flow on iOS**
|
||||
Same as Android: enable, set time, verify prefetch and notification delivery and content.
|
||||
|
||||
- [ ] **Test with no starred plans**
|
||||
- **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**
|
||||
- **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**
|
||||
- **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**
|
||||
- **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` |
|
||||
| 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` |
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export { NativeNotificationService } from "./NativeNotificationService";
|
||||
export { WebPushNotificationService } from "./WebPushNotificationService";
|
||||
|
||||
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||
export { syncStarredPlansToNativePlugin } from "./syncStarredPlansToNativePlugin";
|
||||
export {
|
||||
buildDualScheduleConfig,
|
||||
timeToCron,
|
||||
|
||||
30
src/services/notifications/syncStarredPlansToNativePlugin.ts
Normal file
30
src/services/notifications/syncStarredPlansToNativePlugin.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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.");
|
||||
|
||||
Reference in New Issue
Block a user