fix(dev): clarify Android pending inspector and harden debug entry guard

- Report UNIMPLEMENTED from Android NotificationInspector instead of empty pending
- Surface iOS-only inspector message in NotificationDebugPanel without noisy errors
- Gate Account debug link with import.meta.env.DEV and document intent
- Add architecture comments on NotificationDebugService, inspector plugin, and native exports
This commit is contained in:
Jose Olarte III
2026-05-07 20:40:09 +08:00
parent fd0b8ce6d0
commit 1ef3f32b9e
6 changed files with 60 additions and 14 deletions

View File

@@ -1,7 +1,5 @@
package app.timesafari.notifications;
import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
@@ -11,9 +9,8 @@ import com.getcapacitor.annotation.CapacitorPlugin;
public class NotificationInspectorPlugin extends Plugin {
@PluginMethod
public void getPendingNotifications(PluginCall call) {
JSObject result = new JSObject();
result.put("pending", new JSArray());
call.resolve(result);
call.unimplemented(
"Pending notification inspection is currently implemented on iOS only");
}
}

View File

@@ -2,6 +2,10 @@ import Foundation
import Capacitor
import UserNotifications
// DEV-only diagnostic plugin.
// Kept separate from DailyNotificationPlugin intentionally
// to avoid altering production notification scheduling behavior.
@objc(NotificationInspector)
public class NotificationInspectorPlugin: CAPPlugin, CAPBridgedPlugin {
public var identifier: String { "NotificationInspector" }

View File

@@ -75,7 +75,14 @@
</div>
<div
v-if="pending.length === 0"
v-if="pendingInspectorMessage"
class="text-sm text-amber-900 bg-amber-50 rounded px-3 py-2 border border-amber-200"
role="status"
>
{{ pendingInspectorMessage }}
</div>
<div
v-else-if="pending.length === 0"
class="text-sm text-slate-500 bg-white rounded px-3 py-2 border border-slate-200"
>
(none)
@@ -152,6 +159,7 @@ const presets = [
const intervalMs = ref<number>(60_000);
const busy = ref(false);
const pending = ref<PendingInfo[]>([]);
const pendingInspectorMessage = ref<string | null>(null);
const eventLog = computed(() => NotificationDebugService.eventLog.value);
@@ -172,7 +180,9 @@ async function withBusy(fn: () => Promise<void>): Promise<void> {
async function refreshPending(): Promise<void> {
await withBusy(async () => {
pending.value = await NotificationDebugService.getPendingNotifications();
const result = await NotificationDebugService.getPendingNotifications();
pending.value = result.pending;
pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null;
});
}

View File

@@ -584,6 +584,10 @@ export type NotificationRefreshPayload = {
nextNotifications?: Array<{ timestamp?: number }>;
};
// `handleCapacitorPushNotificationReceived` and `applyNotificationRefreshPayload` are used by
// DEV notification simulation tooling; they must stay production-safe because that tooling
// exercises real flows. (`applyNotificationRefreshPayload` is also used by production refresh.)
/**
* Apply a "refresh notifications" payload by clearing and scheduling timestamps via the native plugin.
*

View File

@@ -1,3 +1,12 @@
/**
* DEV-only notification testing utilities.
*
* IMPORTANT:
* This service intentionally routes through the same production notification
* orchestration paths used by refresh flows, wakeup pushes, and replacement.
* Avoid adding duplicate scheduling logic here.
*/
import { Capacitor } from "@capacitor/core";
import { ref, readonly, type Ref } from "vue";
import type { PushNotificationSchema } from "@capacitor/push-notifications";
@@ -16,6 +25,21 @@ type PendingNotificationInfo = {
triggerType?: string | null;
};
export type PendingNotificationsResult = {
pending: PendingNotificationInfo[];
/** Native layer does not implement inspection on this platform (e.g. Android). */
inspectorUnavailableMessage?: string;
};
function isUnimplementedError(e: unknown): boolean {
return (
typeof e === "object" &&
e !== null &&
"code" in e &&
(e as { code?: string }).code === "UNIMPLEMENTED"
);
}
const LOG = "[NotificationDebugService]";
function formatTime(d: Date): string {
@@ -131,23 +155,30 @@ export const NotificationDebugService = {
append("Cleared notifications");
},
async getPendingNotifications(): Promise<PendingNotificationInfo[]> {
async getPendingNotifications(): Promise<PendingNotificationsResult> {
append("Fetching pending notifications");
if (!Capacitor.isNativePlatform()) {
append("Skipped: not running on native platform");
return [];
return { pending: [] };
}
try {
const res = await NotificationInspector.getPendingNotifications();
const items = (res?.pending ?? []) as PendingNotificationInfo[];
append(`Pending fetched (${items.length})`);
return items;
} catch (e) {
return { pending: items };
} catch (e: unknown) {
if (isUnimplementedError(e)) {
return {
pending: [],
inspectorUnavailableMessage:
"Pending notification inspection is currently supported on iOS only.",
};
}
append("Pending fetch failed");
logger.warn(`${LOG} getPendingNotifications failed`, e);
return [];
return { pending: [] };
}
},
};

View File

@@ -742,8 +742,9 @@
>
Logs
</router-link>
<!-- DEV-only: never render in production (`import.meta.env.DEV` is false there). -->
<router-link
v-if="isDev"
v-if="import.meta.env.DEV"
:to="{ name: 'dev-notifications' }"
class="block w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mt-2"
>
@@ -895,7 +896,6 @@ export default class AccountViewView extends Vue {
readonly DEFAULT_IMAGE_API_SERVER: string = DEFAULT_IMAGE_API_SERVER;
readonly DEFAULT_PARTNER_API_SERVER: string = DEFAULT_PARTNER_API_SERVER;
readonly PASSKEYS_ENABLED: boolean = PASSKEYS_ENABLED;
readonly isDev: boolean = import.meta.env.DEV === true;
// Identity and settings properties
activeDid: string = "";