feat(notifications): add dev/test 10-minute rollover toggle and plugin spec
- Add dev-only "Use 10-minute rollover (testing)" toggle in Reminder Notifications (Account view). Visible only when not on prod API server (isNotProdServer). Toggle persists and reschedules reminder with rolloverIntervalMinutes when changed. - Extend daily notification flow to pass optional rolloverIntervalMinutes to the plugin: NotificationService/NativeNotificationService options, PushNotificationPermission dialog options, first-time and edit flows. - Add settings: reminderFastRolloverForTesting (Settings, AccountSettings, PlatformServiceMixin boolean mapping, migration 007). - Centralize isNotProdServer(apiServer) in constants/app.ts; use in AccountViewView (toggle visibility), ImportAccountView, and TestView. - Add docs/plugin-spec-rollover-interval-minutes.md for the plugin repo (configurable rollover interval and persistence after reboot). Note: Daily notification plugin dependency is currently pointed at the "rollover-interval" branch for testing this feature.
This commit is contained in:
148
docs/plugin-spec-rollover-interval-minutes.md
Normal file
148
docs/plugin-spec-rollover-interval-minutes.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Plugin spec: Configurable rollover interval (e.g. 10 minutes for testing)
|
||||||
|
|
||||||
|
**Date:** 2026-03-03
|
||||||
|
**Target repo:** daily-notification-plugin
|
||||||
|
**Consuming app:** crowd-funder-for-time-pwa (TimeSafari)
|
||||||
|
**Platforms:** iOS and Android
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The consuming app needs to support **rapid testing** of daily notification rollover on device. Today, after a notification fires, the plugin always schedules the next occurrence **24 hours** later. We need an **optional** parameter so the app can request a different interval (e.g. **10 minutes**) for dev/testing. When that parameter is present, the plugin must:
|
||||||
|
|
||||||
|
1. Use the given interval (in minutes) when scheduling the **next** occurrence after a notification fires (rollover).
|
||||||
|
2. **Persist** that interval with the schedule so that it survives **device reboot** and is used again when:
|
||||||
|
- Boot recovery reschedules alarms from stored data, and
|
||||||
|
- Any subsequent rollover runs (after the next notification fires).
|
||||||
|
|
||||||
|
If the interval is not persisted, then after a device restart the plugin would no longer know to use 10 minutes and would fall back to 24 hours; rapid testing after reboot would break. So persistence is a **required** part of this feature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API contract (app → plugin)
|
||||||
|
|
||||||
|
### Method: `scheduleDailyNotification` (or equivalent used for the app’s daily reminder)
|
||||||
|
|
||||||
|
**Add an optional parameter:**
|
||||||
|
|
||||||
|
- **Name:** `rolloverIntervalMinutes` (or equivalent, e.g. `repeatIntervalMinutes`).
|
||||||
|
- **Type:** `number` (integer), optional.
|
||||||
|
- **Meaning:** When the scheduled notification fires, schedule the **next** occurrence this many **minutes** after the current trigger time (instead of 24 hours). When **absent** or not provided, behavior is unchanged: next occurrence is **24 hours** later (current behavior).
|
||||||
|
|
||||||
|
**Example (pseudocode):**
|
||||||
|
|
||||||
|
- App calls: `scheduleDailyNotification({ id, time, title, body, ..., rolloverIntervalMinutes: 10 })`.
|
||||||
|
- Plugin stores the schedule **including** `rolloverIntervalMinutes: 10`.
|
||||||
|
- When the notification fires, plugin computes next trigger = current trigger + 10 minutes (instead of + 24 hours), and schedules that.
|
||||||
|
- When the device reboots, boot recovery loads the schedule, sees `rolloverIntervalMinutes: 10`, and uses it when (a) computing the next run time for reschedule and (b) any future rollover after the next fire.
|
||||||
|
|
||||||
|
**Example (normal production, no param):**
|
||||||
|
|
||||||
|
- App calls: `scheduleDailyNotification({ id, time, title, body })` (no `rolloverIntervalMinutes`).
|
||||||
|
- Plugin stores the schedule with no interval (or default 24h).
|
||||||
|
- Rollover and boot recovery behave as today: next occurrence 24 hours later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Persistence requirement (critical for device restart)
|
||||||
|
|
||||||
|
The rollover interval must be **stored with the schedule** in the plugin’s persistent storage (e.g. Room on Android, UserDefaults/DB on iOS), not only kept in memory. Concretely:
|
||||||
|
|
||||||
|
1. **When the app calls `scheduleDailyNotification` with `rolloverIntervalMinutes`:**
|
||||||
|
- Persist that value in the same place you persist the rest of the schedule (e.g. Schedule entity, or equivalent table/row that is read on boot and on rollover).
|
||||||
|
|
||||||
|
2. **When computing the next occurrence (rollover path, after a notification fires):**
|
||||||
|
- Read the stored `rolloverIntervalMinutes` for that schedule.
|
||||||
|
- If present and > 0: next trigger = current trigger + `rolloverIntervalMinutes` minutes.
|
||||||
|
- If absent or 0: next trigger = current trigger + 24 hours (existing behavior).
|
||||||
|
|
||||||
|
3. **When boot recovery runs (after device restart):**
|
||||||
|
- Load schedules from persistent storage (including the stored `rolloverIntervalMinutes`).
|
||||||
|
- When rescheduling each alarm, use the stored interval to compute the next run time (same logic as rollover: if interval is set, use it; otherwise 24 hours).
|
||||||
|
- When the next notification fires after reboot, the rollover path will again read the same stored value, so the 10-minute (or whatever) interval continues to apply.
|
||||||
|
|
||||||
|
4. **When the app calls `scheduleDailyNotification` without `rolloverIntervalMinutes` (or to turn off fast rollover):**
|
||||||
|
- Overwrite the stored schedule so that the interval is cleared or set to default (24h). Subsequent rollovers and boot recovery then use 24 hours again.
|
||||||
|
|
||||||
|
**Why this matters:** Without persisting the interval, a device restart would lose the “10 minutes” setting; rollover and boot recovery would have no way to know to use 10 minutes and would default to 24 hours. Rapid testing after reboot would not work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform-specific notes
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
- **Storage:** Add `rollover_interval_minutes` (or equivalent) to the Schedule entity (or wherever the app’s reminder schedule is stored) and persist it when handling `scheduleDailyNotification`. Use it in:
|
||||||
|
- Rollover path (e.g. when scheduling next alarm after notification fires).
|
||||||
|
- Boot recovery path (when rebuilding alarms from DB after `BOOT_COMPLETED`).
|
||||||
|
- **Next trigger:** Current trigger time + `rolloverIntervalMinutes` minutes (using `Calendar` or equivalent so DST/timezone is handled correctly; same care as for 24h rollover).
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
- **Storage:** Add the same field to whatever persistent structure holds the schedule (e.g. the same place that stores time, title, body, id). Persist it when the app calls the schedule method.
|
||||||
|
- **Rollover:** In `scheduleNextNotification()` (or equivalent), read the stored interval; if set, use `Calendar.date(byAdding: .minute, value: rolloverIntervalMinutes, to: currentDate)` (or equivalent) instead of adding 24 hours.
|
||||||
|
- **App launch / recovery:** If the plugin has any path that restores or reschedules after app launch or system events, use the stored interval there as well so behavior is consistent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge cases and defaults
|
||||||
|
|
||||||
|
- **Parameter absent:** Do not change current behavior. Next occurrence = 24 hours later.
|
||||||
|
- **Parameter = 0 or negative:** Treat as “use default”; same as absent (24 hours).
|
||||||
|
- **Parameter > 0 (e.g. 10):** Next occurrence = current trigger + that many minutes.
|
||||||
|
- **Existing schedules (created before this feature):** No stored interval → treat as 24 hours. No migration required beyond “missing field = default”.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App-side behavior (for context)
|
||||||
|
|
||||||
|
- The app will only pass `rolloverIntervalMinutes` when a **dev-only** setting is enabled (e.g. “Use 10-minute rollover for testing” in the Reminder Notifications section). Production users will not set it.
|
||||||
|
- The app will pass it on every `scheduleDailyNotification` call when the user has that setting on (first-time enable and edit). When the user turns the setting off, the app will call `scheduleDailyNotification` without the parameter (so the plugin can persist “no interval” / 24h).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification (plugin repo)
|
||||||
|
|
||||||
|
1. **Rollover with interval:** Schedule with `rolloverIntervalMinutes: 10`. Trigger the notification (or wait). Confirm the **next** scheduled time is ~10 minutes after the current trigger (not 24 hours). Let it fire again; confirm the following occurrence is again ~10 minutes later.
|
||||||
|
2. **Persistence:** Schedule with `rolloverIntervalMinutes: 10`, then **restart the device** (do not open the app). After boot, confirm (via logs or next fire) that the rescheduled alarm uses the 10-minute interval (e.g. next fire is 10 minutes after the last stored trigger, not 24 hours). After that notification fires, confirm the **next** rollover is still 10 minutes later.
|
||||||
|
3. **Default:** Schedule without `rolloverIntervalMinutes`. Confirm next occurrence is 24 hours later. Reboot; confirm boot recovery still uses 24 hours.
|
||||||
|
4. **Turn off:** Schedule with 10 minutes, then have the app call `scheduleDailyNotification` again with the same id/time but **no** `rolloverIntervalMinutes`. Confirm stored interval is cleared and next rollover is 24 hours.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Short summary for plugin maintainers
|
||||||
|
|
||||||
|
- **New optional parameter:** `rolloverIntervalMinutes?: number` on the schedule method used for the app’s daily reminder.
|
||||||
|
- **When set (e.g. 10):** After a notification fires, schedule the next occurrence in that many **minutes** instead of 24 hours.
|
||||||
|
- **Must persist:** Store the value with the schedule in the plugin’s DB/storage. Use it in **rollover** and in **boot recovery** so that after a device restart the same interval is used. Without persistence, the feature would not work after reboot.
|
||||||
|
- **When absent:** Behavior unchanged (24-hour rollover). No migration needed for existing schedules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For Cursor (plugin repo) — actionable handoff
|
||||||
|
|
||||||
|
Use this section when implementing this feature in the **daily-notification-plugin** repo (e.g. with Cursor). You can paste or @-mention this doc as context.
|
||||||
|
|
||||||
|
**Goal:** Support an optional `rolloverIntervalMinutes` (or equivalent) on the daily reminder schedule API. When provided (e.g. `10`), schedule the next occurrence that many minutes after the current trigger instead of 24 hours. **Persist this value** with the schedule so that rollover and boot recovery both use it; after a device restart, the same interval must still apply.
|
||||||
|
|
||||||
|
**Concrete tasks:**
|
||||||
|
|
||||||
|
1. **API:** In the plugin interface used by the app (e.g. `scheduleDailyNotification`), add an optional parameter `rolloverIntervalMinutes?: number`. Document that when absent, next occurrence is 24 hours (current behavior).
|
||||||
|
|
||||||
|
2. **Storage (Android):** In the Schedule entity (or equivalent), add a column/field for the rollover interval (e.g. `rollover_interval_minutes` nullable Int). When handling `scheduleDailyNotification`, persist the value if present; if absent, store null or 0 to mean “24 hours”.
|
||||||
|
|
||||||
|
3. **Storage (iOS):** Add the same field to the persistent structure that holds the reminder schedule. Persist it when the app calls the schedule method.
|
||||||
|
|
||||||
|
4. **Rollover (both platforms):** In the code that runs when a scheduled notification fires and schedules the next occurrence:
|
||||||
|
- Read the stored `rolloverIntervalMinutes` for that schedule.
|
||||||
|
- If present and > 0: next trigger = current trigger + that many minutes (using Calendar/date APIs that respect timezone/DST).
|
||||||
|
- Else: next trigger = current trigger + 24 hours (existing behavior).
|
||||||
|
- Persist the same interval on the new schedule record so the next rollover still uses it.
|
||||||
|
|
||||||
|
5. **Boot recovery (both platforms):** In the path that runs after device reboot and reschedules from stored data:
|
||||||
|
- Load the stored `rolloverIntervalMinutes` with each schedule.
|
||||||
|
- When computing “next run time” for reschedule, use the same logic: if interval set, current trigger + that many minutes; else + 24 hours.
|
||||||
|
- Do not rely on in-memory state; always read from persisted storage so behavior is correct after restart.
|
||||||
|
|
||||||
|
6. **Clearing the interval:** When the app calls `scheduleDailyNotification` without `rolloverIntervalMinutes` (e.g. user turned off “fast rollover” in the app), overwrite the stored schedule so the interval field is null/0. Subsequent rollovers and boot recovery then use 24 hours.
|
||||||
|
|
||||||
|
7. **Tests:** Add or extend tests for: (a) rollover with 10 minutes, (b) boot recovery with stored 10-minute interval, (c) default 24h when parameter absent or cleared.
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -38,7 +38,7 @@
|
|||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@simplewebauthn/server": "^10.0.0",
|
"@simplewebauthn/server": "^10.0.0",
|
||||||
"@timesafari/daily-notification-plugin": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
|
"@timesafari/daily-notification-plugin": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#rollover-interval",
|
||||||
"@tweenjs/tween.js": "^21.1.1",
|
"@tweenjs/tween.js": "^21.1.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@veramo/core": "^5.6.0",
|
"@veramo/core": "^5.6.0",
|
||||||
@@ -8651,8 +8651,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@timesafari/daily-notification-plugin": {
|
"node_modules/@timesafari/daily-notification-plugin": {
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#aa0eaa5389f67709240b88ad5955b2c89a7abf9e",
|
"resolved": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#27144800706094d292115737de3095a7959e8090",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
"@capacitor/share": "^6.0.3",
|
"@capacitor/share": "^6.0.3",
|
||||||
"@capacitor/status-bar": "^6.0.2",
|
"@capacitor/status-bar": "^6.0.2",
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@timesafari/daily-notification-plugin": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
|
"@timesafari/daily-notification-plugin": "git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#rollover-interval",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
pushType = "";
|
pushType = "";
|
||||||
/** When true, dialog only returns time/message to parent; parent does cancel+schedule (avoids double schedule on edit). */
|
/** When true, dialog only returns time/message to parent; parent does cancel+schedule (avoids double schedule on edit). */
|
||||||
skipScheduleForOpen = false;
|
skipScheduleForOpen = false;
|
||||||
|
/** When set (e.g. 10), passed to plugin for dev/test fast rollover. */
|
||||||
|
rolloverIntervalMinutesForSchedule: number | undefined = undefined;
|
||||||
serviceWorkerReady = false;
|
serviceWorkerReady = false;
|
||||||
vapidKey = "";
|
vapidKey = "";
|
||||||
|
|
||||||
@@ -171,12 +173,13 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
async open(
|
async open(
|
||||||
pushType: string,
|
pushType: string,
|
||||||
callback?: (success: boolean, time: string, message?: string) => void,
|
callback?: (success: boolean, time: string, message?: string) => void,
|
||||||
options?: { skipSchedule?: boolean },
|
options?: { skipSchedule?: boolean; rolloverIntervalMinutes?: number },
|
||||||
) {
|
) {
|
||||||
this.callback = callback || this.callback;
|
this.callback = callback || this.callback;
|
||||||
this.isVisible = true;
|
this.isVisible = true;
|
||||||
this.pushType = pushType;
|
this.pushType = pushType;
|
||||||
this.skipScheduleForOpen = options?.skipSchedule ?? false;
|
this.skipScheduleForOpen = options?.skipSchedule ?? false;
|
||||||
|
this.rolloverIntervalMinutesForSchedule = options?.rolloverIntervalMinutes;
|
||||||
|
|
||||||
// Native platforms: Skip web push initialization
|
// Native platforms: Skip web push initialization
|
||||||
if (this.isNativePlatform) {
|
if (this.isNativePlatform) {
|
||||||
@@ -805,6 +808,10 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
priority: "normal",
|
priority: "normal",
|
||||||
|
...(this.rolloverIntervalMinutesForSchedule != null &&
|
||||||
|
this.rolloverIntervalMinutesForSchedule > 0
|
||||||
|
? { rolloverIntervalMinutes: this.rolloverIntervalMinutesForSchedule }
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ export const DEFAULT_PUSH_SERVER =
|
|||||||
|
|
||||||
export const IMAGE_TYPE_PROFILE = "profile";
|
export const IMAGE_TYPE_PROFILE = "profile";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the current API server is not production (test/local build).
|
||||||
|
* Use this to show dev/test-only UI (e.g. 10-minute rollover toggle, test user mnemonic).
|
||||||
|
*/
|
||||||
|
export function isNotProdServer(apiServer: string): boolean {
|
||||||
|
return apiServer !== AppString.PROD_ENDORSER_API_SERVER;
|
||||||
|
}
|
||||||
|
|
||||||
export const PASSKEYS_ENABLED =
|
export const PASSKEYS_ENABLED =
|
||||||
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,13 @@ const MIGRATIONS = [
|
|||||||
CREATE INDEX idx_contact_labels_did ON contact_labels(did);
|
CREATE INDEX idx_contact_labels_did ON contact_labels(did);
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "007_add_reminderFastRolloverForTesting_to_settings",
|
||||||
|
sql: `
|
||||||
|
-- Dev/test only: 10-minute rollover for daily reminder (plugin rolloverIntervalMinutes)
|
||||||
|
ALTER TABLE settings ADD COLUMN reminderFastRolloverForTesting BOOLEAN DEFAULT FALSE;
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export type Settings = {
|
|||||||
notifyingNewActivityTime?: string; // set to their chosen time if they have turned on daily check for new activity via the push server
|
notifyingNewActivityTime?: string; // set to their chosen time if they have turned on daily check for new activity via the push server
|
||||||
notifyingReminderMessage?: string; // set to their chosen message for a daily reminder
|
notifyingReminderMessage?: string; // set to their chosen message for a daily reminder
|
||||||
notifyingReminderTime?: string; // set to their chosen time for a daily reminder
|
notifyingReminderTime?: string; // set to their chosen time for a daily reminder
|
||||||
|
/** Dev/test only: use 10-minute rollover interval for daily reminder (plugin rolloverIntervalMinutes) */
|
||||||
|
reminderFastRolloverForTesting?: boolean;
|
||||||
|
|
||||||
partnerApiServer?: string; // partner server API URL
|
partnerApiServer?: string; // partner server API URL
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface AccountSettings {
|
|||||||
notifyingNewActivityTime?: string;
|
notifyingNewActivityTime?: string;
|
||||||
notifyingReminderMessage?: string;
|
notifyingReminderMessage?: string;
|
||||||
notifyingReminderTime?: string;
|
notifyingReminderTime?: string;
|
||||||
|
reminderFastRolloverForTesting?: boolean;
|
||||||
partnerApiServer?: string;
|
partnerApiServer?: string;
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string;
|
||||||
showContactGivesInline?: boolean;
|
showContactGivesInline?: boolean;
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
|||||||
sound: boolean;
|
sound: boolean;
|
||||||
priority: "low" | "default" | "high";
|
priority: "low" | "default" | "high";
|
||||||
id: string;
|
id: string;
|
||||||
|
rolloverIntervalMinutes?: number;
|
||||||
} = {
|
} = {
|
||||||
time: options.time,
|
time: options.time,
|
||||||
title: options.title,
|
title: options.title,
|
||||||
@@ -367,6 +368,10 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
|||||||
sound: true,
|
sound: true,
|
||||||
priority: (options.priority || "normal") as "low" | "default" | "high",
|
priority: (options.priority || "normal") as "low" | "default" | "high",
|
||||||
id: this.reminderId,
|
id: this.reminderId,
|
||||||
|
...(options.rolloverIntervalMinutes != null &&
|
||||||
|
options.rolloverIntervalMinutes > 0
|
||||||
|
? { rolloverIntervalMinutes: options.rolloverIntervalMinutes }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[NativeNotificationService] Calling scheduleDailyNotification with options:",
|
"[NativeNotificationService] Calling scheduleDailyNotification with options:",
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export interface DailyNotificationOptions {
|
|||||||
* @default 'normal'
|
* @default 'normal'
|
||||||
*/
|
*/
|
||||||
priority?: "low" | "normal" | "high";
|
priority?: "low" | "normal" | "high";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional rollover interval in minutes (e.g. 10 for testing). When set, next occurrence
|
||||||
|
* is scheduled this many minutes after the current trigger instead of 24 hours.
|
||||||
|
* Plugin must support and persist this for it to take effect after reboot.
|
||||||
|
*/
|
||||||
|
rolloverIntervalMinutes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ export const PlatformServiceMixin = {
|
|||||||
column === "showShortcutBvc" ||
|
column === "showShortcutBvc" ||
|
||||||
column === "warnIfProdServer" ||
|
column === "warnIfProdServer" ||
|
||||||
column === "warnIfTestServer" ||
|
column === "warnIfTestServer" ||
|
||||||
|
column === "reminderFastRolloverForTesting" ||
|
||||||
// contacts
|
// contacts
|
||||||
column === "iViewContent" ||
|
column === "iViewContent" ||
|
||||||
column === "registered" ||
|
column === "registered" ||
|
||||||
|
|||||||
@@ -143,6 +143,29 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Dev/test only: 10-minute rollover for rapid testing (Reminder Notifications) -->
|
||||||
|
<label
|
||||||
|
v-if="isNotProdServer"
|
||||||
|
class="flex items-center justify-between cursor-pointer mt-3 py-2"
|
||||||
|
@click="toggleReminderFastRollover"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-slate-600">
|
||||||
|
Use 10-minute rollover (testing)
|
||||||
|
</span>
|
||||||
|
<div class="relative ml-2">
|
||||||
|
<input
|
||||||
|
:checked="reminderFastRolloverForTesting"
|
||||||
|
type="checkbox"
|
||||||
|
class="sr-only"
|
||||||
|
readonly
|
||||||
|
@click.stop.prevent
|
||||||
|
/>
|
||||||
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||||
|
<div
|
||||||
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<div v-if="false" class="mt-4 flex items-center justify-between">
|
<div v-if="false" class="mt-4 flex items-center justify-between">
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<div>
|
<div>
|
||||||
@@ -780,6 +803,7 @@ import {
|
|||||||
DEFAULT_PARTNER_API_SERVER,
|
DEFAULT_PARTNER_API_SERVER,
|
||||||
DEFAULT_PUSH_SERVER,
|
DEFAULT_PUSH_SERVER,
|
||||||
IMAGE_TYPE_PROFILE,
|
IMAGE_TYPE_PROFILE,
|
||||||
|
isNotProdServer as isNotProdServerUtil,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
PASSKEYS_ENABLED,
|
PASSKEYS_ENABLED,
|
||||||
} from "../constants/app";
|
} from "../constants/app";
|
||||||
@@ -824,7 +848,7 @@ interface PushNotificationPermissionRef {
|
|||||||
open: (
|
open: (
|
||||||
title: string,
|
title: string,
|
||||||
callback: (success: boolean, timeText: string, message?: string) => void,
|
callback: (success: boolean, timeText: string, message?: string) => void,
|
||||||
options?: { skipSchedule?: boolean },
|
options?: { skipSchedule?: boolean; rolloverIntervalMinutes?: number },
|
||||||
) => void;
|
) => void;
|
||||||
hourInput?: string;
|
hourInput?: string;
|
||||||
minuteInput?: string;
|
minuteInput?: string;
|
||||||
@@ -899,6 +923,8 @@ export default class AccountViewView extends Vue {
|
|||||||
notifyingReminderTime: string = "";
|
notifyingReminderTime: string = "";
|
||||||
/** Guard: only one edit-reminder schedule per user action (avoids double schedule on Android). */
|
/** Guard: only one edit-reminder schedule per user action (avoids double schedule on Android). */
|
||||||
editReminderScheduleInProgress: boolean = false;
|
editReminderScheduleInProgress: boolean = false;
|
||||||
|
/** Dev/test only: use 10-minute rollover for daily reminder (plugin rolloverIntervalMinutes). */
|
||||||
|
reminderFastRolloverForTesting: boolean = false;
|
||||||
subscription: PushSubscription | null = null;
|
subscription: PushSubscription | null = null;
|
||||||
|
|
||||||
// UI state properties
|
// UI state properties
|
||||||
@@ -934,6 +960,14 @@ export default class AccountViewView extends Vue {
|
|||||||
return Capacitor.isNativePlatform();
|
return Capacitor.isNativePlatform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when not on prod API server (test/local), so dev-only UI (e.g. 10-minute rollover toggle) is shown.
|
||||||
|
* Matches logic used in ImportAccountView and TestView.
|
||||||
|
*/
|
||||||
|
private get isNotProdServer(): boolean {
|
||||||
|
return isNotProdServerUtil(this.apiServer);
|
||||||
|
}
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
|
|
||||||
@@ -1081,6 +1115,8 @@ export default class AccountViewView extends Vue {
|
|||||||
this.notifyingReminder = !!settings.notifyingReminderTime;
|
this.notifyingReminder = !!settings.notifyingReminderTime;
|
||||||
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
|
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
|
||||||
this.notifyingReminderTime = settings.notifyingReminderTime || "";
|
this.notifyingReminderTime = settings.notifyingReminderTime || "";
|
||||||
|
this.reminderFastRolloverForTesting =
|
||||||
|
!!settings.reminderFastRolloverForTesting;
|
||||||
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
||||||
this.partnerApiServerInput =
|
this.partnerApiServerInput =
|
||||||
settings.partnerApiServer || this.partnerApiServerInput;
|
settings.partnerApiServer || this.partnerApiServerInput;
|
||||||
@@ -1232,6 +1268,11 @@ export default class AccountViewView extends Vue {
|
|||||||
this.notifyingReminderTime = timeText;
|
this.notifyingReminderTime = timeText;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rolloverIntervalMinutes: this.reminderFastRolloverForTesting
|
||||||
|
? 10
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// On native platforms, handle notification cancellation directly
|
// On native platforms, handle notification cancellation directly
|
||||||
@@ -1323,6 +1364,9 @@ export default class AccountViewView extends Vue {
|
|||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
priority: "normal",
|
priority: "normal",
|
||||||
|
...(this.reminderFastRolloverForTesting
|
||||||
|
? { rolloverIntervalMinutes: 10 }
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scheduleSuccess) {
|
if (scheduleSuccess) {
|
||||||
@@ -1349,7 +1393,12 @@ export default class AccountViewView extends Vue {
|
|||||||
this.editReminderScheduleInProgress = false;
|
this.editReminderScheduleInProgress = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ skipSchedule: true },
|
{
|
||||||
|
skipSchedule: true,
|
||||||
|
rolloverIntervalMinutes: this.reminderFastRolloverForTesting
|
||||||
|
? 10
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pre-populate the form with current values after dialog opens
|
// Pre-populate the form with current values after dialog opens
|
||||||
@@ -1388,6 +1437,54 @@ export default class AccountViewView extends Vue {
|
|||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle dev-only 10-minute rollover for daily reminder. Saves the setting and,
|
||||||
|
* if reminder is already on, reschedules so the plugin uses the new interval.
|
||||||
|
*/
|
||||||
|
async toggleReminderFastRollover(): Promise<void> {
|
||||||
|
const next = !this.reminderFastRolloverForTesting;
|
||||||
|
await this.$saveSettings({
|
||||||
|
reminderFastRolloverForTesting: next,
|
||||||
|
});
|
||||||
|
this.reminderFastRolloverForTesting = next;
|
||||||
|
|
||||||
|
if (this.notifyingReminder) {
|
||||||
|
try {
|
||||||
|
const service = NotificationService.getInstance();
|
||||||
|
if (Capacitor.getPlatform() !== "android") {
|
||||||
|
await service.cancelDailyNotification();
|
||||||
|
}
|
||||||
|
const time24h = this.parseTimeTo24Hour(this.notifyingReminderTime);
|
||||||
|
const title = "Daily Reminder";
|
||||||
|
const body =
|
||||||
|
this.notifyingReminderMessage ||
|
||||||
|
"Click to share some gratitude with the world -- even if they're unnamed.";
|
||||||
|
await service.scheduleDailyNotification({
|
||||||
|
time: time24h,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
priority: "normal",
|
||||||
|
...(next ? { rolloverIntervalMinutes: 10 } : {}),
|
||||||
|
});
|
||||||
|
this.notify.success(
|
||||||
|
next
|
||||||
|
? "Reminder will repeat every 10 minutes (testing)."
|
||||||
|
: "Reminder will repeat daily (24h).",
|
||||||
|
TIMEOUTS.STANDARD,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
"[AccountViewView] Reschedule after fast-rollover toggle failed:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
this.notify.error(
|
||||||
|
"Failed to update reminder interval. Please try again.",
|
||||||
|
TIMEOUTS.STANDARD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse time string (e.g., "5:22 PM") to 24-hour format (e.g., "17:22")
|
* Parse time string (e.g., "5:22 PM") to 24-hour format (e.g., "17:22")
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -95,7 +95,11 @@
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import {
|
||||||
|
AppString,
|
||||||
|
isNotProdServer as isNotProdServerUtil,
|
||||||
|
NotificationIface,
|
||||||
|
} from "../constants/app";
|
||||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
||||||
import {
|
import {
|
||||||
retrieveAccountCount,
|
retrieveAccountCount,
|
||||||
@@ -194,7 +198,7 @@ export default class ImportAccountView extends Vue {
|
|||||||
* @returns True if not on production server (enables test utilities)
|
* @returns True if not on production server (enables test utilities)
|
||||||
*/
|
*/
|
||||||
public isNotProdServer() {
|
public isNotProdServer() {
|
||||||
return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER;
|
return isNotProdServerUtil(this.apiServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -320,7 +320,11 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import {
|
||||||
|
AppString,
|
||||||
|
isNotProdServer as isNotProdServerUtil,
|
||||||
|
NotificationIface,
|
||||||
|
} from "../constants/app";
|
||||||
import {
|
import {
|
||||||
NOTIFY_SQL_ERROR,
|
NOTIFY_SQL_ERROR,
|
||||||
createSqlErrorMessage,
|
createSqlErrorMessage,
|
||||||
@@ -741,7 +745,7 @@ export default class Help extends Vue {
|
|||||||
* @returns True if not on production server (enables test utilities)
|
* @returns True if not on production server (enables test utilities)
|
||||||
*/
|
*/
|
||||||
public isNotProdServer() {
|
public isNotProdServer() {
|
||||||
return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER;
|
return isNotProdServerUtil(this.apiServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerMe() {
|
async registerMe() {
|
||||||
|
|||||||
Reference in New Issue
Block a user