feat(notifications): add localStorage debug config for notification API base URL

Introduce NotificationDebugConfig so register/refresh use getNotificationApiBaseUrl()
(APP_SERVER by default, optional LAN/ngrok override) and configurable testMode without rebuilds.
This commit is contained in:
Jose Olarte III
2026-05-18 15:06:52 +08:00
parent 4c97c578bb
commit 794b48f0d7
4 changed files with 118 additions and 4 deletions

View File

@@ -16,6 +16,10 @@ import type { PushNotificationSchema } from "@capacitor/push-notifications";
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds";
import { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
import {
getNotificationApiBaseUrl,
getTestMode,
} from "./NotificationDebugConfig";
/**
* Extended type for DailyNotification that includes the actual Swift implementation
@@ -555,12 +559,13 @@ export async function refreshNotifications(): Promise<void> {
}
try {
const res = await fetch("/notifications/refresh", {
const baseUrl = getNotificationApiBaseUrl();
const res = await fetch(`${baseUrl}/notifications/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
platform: Capacitor.getPlatform(),
testMode: true,
testMode: getTestMode(),
}),
});

View File

@@ -0,0 +1,96 @@
/**
* Lightweight debug configuration for notification backend testing.
* Persists overrides in localStorage; production defaults apply when unset.
*/
import { APP_SERVER } from "@/constants/app";
const LOG = "[NotificationDebug]";
const STORAGE_KEY_BACKEND_URL = "notificationDebug.backendBaseUrl";
const STORAGE_KEY_TEST_MODE = "notificationDebug.testMode";
/** Trim whitespace, drop trailing slash; empty input becomes null. */
export function normalizeNotificationBackendUrl(url: string): string | null {
const trimmed = url.trim();
if (!trimmed) {
return null;
}
return trimmed.replace(/\/$/, "");
}
function readStorage(key: string): string | null {
if (typeof localStorage === "undefined") {
return null;
}
try {
return localStorage.getItem(key);
} catch {
return null;
}
}
function writeStorage(key: string, value: string | null): void {
if (typeof localStorage === "undefined") {
return;
}
try {
if (value === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, value);
}
} catch {
// Quota / privacy mode — ignore
}
}
/** Backend URL override, or null when using the default app server. */
export function getBackendBaseUrl(): string | null {
const raw = readStorage(STORAGE_KEY_BACKEND_URL);
if (raw === null) {
return null;
}
return normalizeNotificationBackendUrl(raw);
}
export function setBackendBaseUrl(url: string): void {
const normalized = normalizeNotificationBackendUrl(url);
if (normalized === null) {
writeStorage(STORAGE_KEY_BACKEND_URL, null);
// eslint-disable-next-line no-console
console.log(`${LOG} backend URL cleared (using default)`);
return;
}
writeStorage(STORAGE_KEY_BACKEND_URL, normalized);
// eslint-disable-next-line no-console
console.log(`${LOG} backend URL set to ${normalized}`);
}
/**
* When never configured via debug UI/console, matches prior hardcoded `testMode: true`.
*/
export function getTestMode(): boolean {
const raw = readStorage(STORAGE_KEY_TEST_MODE);
if (raw === null) {
return true;
}
return raw === "true";
}
export function setTestMode(enabled: boolean): void {
writeStorage(STORAGE_KEY_TEST_MODE, enabled ? "true" : "false");
// eslint-disable-next-line no-console
console.log(`${LOG} test mode ${enabled ? "enabled" : "disabled"}`);
}
/**
* Base URL for `/notifications/*` API calls.
* Uses debug override when set; otherwise the built-in app server (production default).
*/
export function getNotificationApiBaseUrl(): string {
const override = getBackendBaseUrl();
if (override) {
return override;
}
return normalizeNotificationBackendUrl(APP_SERVER) ?? APP_SERVER;
}

View File

@@ -16,6 +16,10 @@
import { Capacitor } from "@capacitor/core";
import { logger } from "@/utils/logger";
import { getOrCreateDeviceId } from "./deviceId";
import {
getNotificationApiBaseUrl,
getTestMode,
} from "./NotificationDebugConfig";
import { NativeNotificationService } from "./NativeNotificationService";
import { WebPushNotificationService } from "./WebPushNotificationService";
@@ -24,14 +28,15 @@ import { WebPushNotificationService } from "./WebPushNotificationService";
*/
export async function registerToken(fcmToken: string): Promise<void> {
const deviceId = await getOrCreateDeviceId();
const res = await fetch("/notifications/register", {
const baseUrl = getNotificationApiBaseUrl();
const res = await fetch(`${baseUrl}/notifications/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
deviceId,
fcmToken,
platform: Capacitor.getPlatform(),
testMode: true,
testMode: getTestMode(),
}),
});
if (!res.ok) {

View File

@@ -13,6 +13,14 @@
* ```
*/
export {
getBackendBaseUrl,
getNotificationApiBaseUrl,
getTestMode,
normalizeNotificationBackendUrl,
setBackendBaseUrl,
setTestMode,
} from "./NotificationDebugConfig";
export { NotificationService, registerToken } from "./NotificationService";
export { NativeNotificationService } from "./NativeNotificationService";
export { WebPushNotificationService } from "./WebPushNotificationService";