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:
+2
-5
@@ -1,7 +1,5 @@
|
|||||||
package app.timesafari.notifications;
|
package app.timesafari.notifications;
|
||||||
|
|
||||||
import com.getcapacitor.JSArray;
|
|
||||||
import com.getcapacitor.JSObject;
|
|
||||||
import com.getcapacitor.Plugin;
|
import com.getcapacitor.Plugin;
|
||||||
import com.getcapacitor.PluginCall;
|
import com.getcapacitor.PluginCall;
|
||||||
import com.getcapacitor.PluginMethod;
|
import com.getcapacitor.PluginMethod;
|
||||||
@@ -11,9 +9,8 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
|||||||
public class NotificationInspectorPlugin extends Plugin {
|
public class NotificationInspectorPlugin extends Plugin {
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
public void getPendingNotifications(PluginCall call) {
|
public void getPendingNotifications(PluginCall call) {
|
||||||
JSObject result = new JSObject();
|
call.unimplemented(
|
||||||
result.put("pending", new JSArray());
|
"Pending notification inspection is currently implemented on iOS only");
|
||||||
call.resolve(result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import Foundation
|
|||||||
import Capacitor
|
import Capacitor
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
|
// DEV-only diagnostic plugin.
|
||||||
|
// Kept separate from DailyNotificationPlugin intentionally
|
||||||
|
// to avoid altering production notification scheduling behavior.
|
||||||
|
|
||||||
@objc(NotificationInspector)
|
@objc(NotificationInspector)
|
||||||
public class NotificationInspectorPlugin: CAPPlugin, CAPBridgedPlugin {
|
public class NotificationInspectorPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||||
public var identifier: String { "NotificationInspector" }
|
public var identifier: String { "NotificationInspector" }
|
||||||
|
|||||||
@@ -75,7 +75,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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"
|
class="text-sm text-slate-500 bg-white rounded px-3 py-2 border border-slate-200"
|
||||||
>
|
>
|
||||||
(none)
|
(none)
|
||||||
@@ -152,6 +159,7 @@ const presets = [
|
|||||||
const intervalMs = ref<number>(60_000);
|
const intervalMs = ref<number>(60_000);
|
||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
const pending = ref<PendingInfo[]>([]);
|
const pending = ref<PendingInfo[]>([]);
|
||||||
|
const pendingInspectorMessage = ref<string | null>(null);
|
||||||
|
|
||||||
const eventLog = computed(() => NotificationDebugService.eventLog.value);
|
const eventLog = computed(() => NotificationDebugService.eventLog.value);
|
||||||
|
|
||||||
@@ -172,7 +180,9 @@ async function withBusy(fn: () => Promise<void>): Promise<void> {
|
|||||||
|
|
||||||
async function refreshPending(): Promise<void> {
|
async function refreshPending(): Promise<void> {
|
||||||
await withBusy(async () => {
|
await withBusy(async () => {
|
||||||
pending.value = await NotificationDebugService.getPendingNotifications();
|
const result = await NotificationDebugService.getPendingNotifications();
|
||||||
|
pending.value = result.pending;
|
||||||
|
pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -584,6 +584,10 @@ export type NotificationRefreshPayload = {
|
|||||||
nextNotifications?: Array<{ timestamp?: number }>;
|
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.
|
* Apply a "refresh notifications" payload by clearing and scheduling timestamps via the native plugin.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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 { Capacitor } from "@capacitor/core";
|
||||||
import { ref, readonly, type Ref } from "vue";
|
import { ref, readonly, type Ref } from "vue";
|
||||||
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
||||||
@@ -16,6 +25,21 @@ type PendingNotificationInfo = {
|
|||||||
triggerType?: string | null;
|
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]";
|
const LOG = "[NotificationDebugService]";
|
||||||
|
|
||||||
function formatTime(d: Date): string {
|
function formatTime(d: Date): string {
|
||||||
@@ -131,23 +155,30 @@ export const NotificationDebugService = {
|
|||||||
append("Cleared notifications");
|
append("Cleared notifications");
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPendingNotifications(): Promise<PendingNotificationInfo[]> {
|
async getPendingNotifications(): Promise<PendingNotificationsResult> {
|
||||||
append("Fetching pending notifications");
|
append("Fetching pending notifications");
|
||||||
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
append("Skipped: not running on native platform");
|
append("Skipped: not running on native platform");
|
||||||
return [];
|
return { pending: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await NotificationInspector.getPendingNotifications();
|
const res = await NotificationInspector.getPendingNotifications();
|
||||||
const items = (res?.pending ?? []) as PendingNotificationInfo[];
|
const items = (res?.pending ?? []) as PendingNotificationInfo[];
|
||||||
append(`Pending fetched (${items.length})`);
|
append(`Pending fetched (${items.length})`);
|
||||||
return items;
|
return { pending: items };
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
|
if (isUnimplementedError(e)) {
|
||||||
|
return {
|
||||||
|
pending: [],
|
||||||
|
inspectorUnavailableMessage:
|
||||||
|
"Pending notification inspection is currently supported on iOS only.",
|
||||||
|
};
|
||||||
|
}
|
||||||
append("Pending fetch failed");
|
append("Pending fetch failed");
|
||||||
logger.warn(`${LOG} getPendingNotifications failed`, e);
|
logger.warn(`${LOG} getPendingNotifications failed`, e);
|
||||||
return [];
|
return { pending: [] };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -742,8 +742,9 @@
|
|||||||
>
|
>
|
||||||
Logs
|
Logs
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<!-- DEV-only: never render in production (`import.meta.env.DEV` is false there). -->
|
||||||
<router-link
|
<router-link
|
||||||
v-if="isDev"
|
v-if="import.meta.env.DEV"
|
||||||
:to="{ name: 'dev-notifications' }"
|
: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"
|
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_IMAGE_API_SERVER: string = DEFAULT_IMAGE_API_SERVER;
|
||||||
readonly DEFAULT_PARTNER_API_SERVER: string = DEFAULT_PARTNER_API_SERVER;
|
readonly DEFAULT_PARTNER_API_SERVER: string = DEFAULT_PARTNER_API_SERVER;
|
||||||
readonly PASSKEYS_ENABLED: boolean = PASSKEYS_ENABLED;
|
readonly PASSKEYS_ENABLED: boolean = PASSKEYS_ENABLED;
|
||||||
readonly isDev: boolean = import.meta.env.DEV === true;
|
|
||||||
|
|
||||||
// Identity and settings properties
|
// Identity and settings properties
|
||||||
activeDid: string = "";
|
activeDid: string = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user