Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ const config: ExpoConfig = {
bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${variant.iosBundleIdentifier}`,
enablePushNotifications: true,
// Agent activity can update many times an hour; without the
// frequent-updates entitlement iOS throttles the update budget sooner.
frequentUpdates: true,
widgets: [
{
name: "AgentActivity",
Expand Down
95 changes: 95 additions & 0 deletions apps/mobile/src/features/agent-awareness/deviceDiagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, it } from "vite-plus/test";
import type { RelayClientDeviceRecord } from "@t3tools/contracts/relay";

import { formatDeviceDiagnosticsRows } from "./deviceDiagnostics";

function makeDevice(diagnostics: RelayClientDeviceRecord["diagnostics"]): RelayClientDeviceRecord {
return {
deviceId: "device-1",
label: "iPhone",
platform: "ios",
iosMajorVersion: 18,
appVersion: "1.0.0",
notifications: {
enabled: true,
notifyOnApproval: true,
notifyOnInput: true,
notifyOnCompletion: true,
notifyOnFailure: true,
},
liveActivities: { enabled: true },
...(diagnostics ? { diagnostics } : {}),
updatedAt: "2026-07-04T00:00:00.000Z",
} as RelayClientDeviceRecord;
}

describe("formatDeviceDiagnosticsRows", () => {
it("flags an unregistered device", () => {
expect(formatDeviceDiagnosticsRows(null)).toEqual([
{ label: "Relay Registration", value: "Not registered", tone: "warn" },
]);
});

it("explains when the relay predates delivery diagnostics", () => {
expect(formatDeviceDiagnosticsRows(makeDevice(undefined))).toEqual([
{ label: "Relay Registration", value: "Registered", tone: "ok" },
{ label: "Delivery Details", value: "Requires a relay update", tone: "muted" },
]);
});

it("warns about missing tokens and surfaces the last delivery failure", () => {
const rows = formatDeviceDiagnosticsRows(
makeDevice({
bundleId: "com.t3tools.t3code.preview",
apsEnvironment: "production",
hasPushToken: false,
hasPushToStartToken: false,
hasLiveActivityToken: false,
lastDeliveryAt: "2026-06-05T01:02:59.566Z",
lastDeliveryKind: "live_activity_end",
lastDeliveryStatus: 400,
lastDeliveryError: "DeviceTokenNotForTopic",
}),
);

expect(rows).toEqual([
{ label: "Notification Token", value: "Missing", tone: "warn" },
{ label: "Live Activity Start Token", value: "Missing", tone: "warn" },
{ label: "Active Live Activity", value: "None", tone: "muted" },
{
label: "APNs Route",
value: "com.t3tools.t3code.preview (production)",
tone: "muted",
},
{
label: "Last Delivery",
value: "DeviceTokenNotForTopic (400)",
tone: "warn",
},
]);
});

it("reports healthy registrations with a successful delivery", () => {
const rows = formatDeviceDiagnosticsRows(
makeDevice({
bundleId: "com.t3tools.t3code",
apsEnvironment: "production",
hasPushToken: true,
hasPushToStartToken: true,
hasLiveActivityToken: true,
lastDeliveryAt: "2026-07-04T00:00:00.000Z",
lastDeliveryKind: "live_activity_update",
lastDeliveryStatus: 200,
lastDeliveryError: null,
}),
);

expect(rows.slice(0, 3)).toEqual([
{ label: "Notification Token", value: "Registered", tone: "ok" },
{ label: "Live Activity Start Token", value: "Registered", tone: "ok" },
{ label: "Active Live Activity", value: "Connected", tone: "ok" },
]);
expect(rows[4]).toMatchObject({ label: "Last Delivery", tone: "ok" });
expect(rows[4]?.value).toContain("Delivered");
});
});
96 changes: 96 additions & 0 deletions apps/mobile/src/features/agent-awareness/deviceDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { RelayClientDeviceRecord } from "@t3tools/contracts/relay";

export interface DeviceDiagnosticsRow {
readonly label: string;
readonly value: string;
readonly tone: "ok" | "warn" | "muted";
}

function formatLastDeliveryTimestamp(iso: string): string {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) {
return "";
}
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}

// Renders the relay's view of this device so "why am I not getting pushes"
// is answerable from the Settings screen instead of the relay database.
export function formatDeviceDiagnosticsRows(
device: RelayClientDeviceRecord | null,
): ReadonlyArray<DeviceDiagnosticsRow> {
if (device === null) {
return [
{
label: "Relay Registration",
value: "Not registered",
tone: "warn",
},
];
}
const diagnostics = device.diagnostics;
if (!diagnostics) {
return [
{ label: "Relay Registration", value: "Registered", tone: "ok" },
{ label: "Delivery Details", value: "Requires a relay update", tone: "muted" },
];
}

const rows: Array<DeviceDiagnosticsRow> = [
{
label: "Notification Token",
value: diagnostics.hasPushToken ? "Registered" : "Missing",
tone: diagnostics.hasPushToken ? "ok" : "warn",
},
{
label: "Live Activity Start Token",
value: diagnostics.hasPushToStartToken ? "Registered" : "Missing",
tone: diagnostics.hasPushToStartToken ? "ok" : "warn",
},
{
label: "Active Live Activity",
value: diagnostics.hasLiveActivityToken ? "Connected" : "None",
tone: diagnostics.hasLiveActivityToken ? "ok" : "muted",
},
];

if (diagnostics.bundleId) {
rows.push({
label: "APNs Route",
value: `${diagnostics.bundleId} (${diagnostics.apsEnvironment ?? "default"})`,
tone: "muted",
});
} else {
rows.push({
label: "APNs Route",
value: "Relay default (update the app to register)",
tone: "warn",
});
}

if (diagnostics.lastDeliveryAt === null) {
rows.push({ label: "Last Delivery", value: "None yet", tone: "muted" });
} else if (diagnostics.lastDeliveryError !== null) {
const status =
diagnostics.lastDeliveryStatus === null ? "" : ` (${diagnostics.lastDeliveryStatus})`;
rows.push({
label: "Last Delivery",
value: `${diagnostics.lastDeliveryError}${status}`,
tone: "warn",
});
} else {
const timestamp = formatLastDeliveryTimestamp(diagnostics.lastDeliveryAt);
rows.push({
label: "Last Delivery",
value: timestamp ? `Delivered ${timestamp}` : "Delivered",
tone: "ok",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last delivery ignores HTTP status

Medium Severity

The "Last Delivery" status in device diagnostics may incorrectly report failed push deliveries as successful. This happens because the display logic checks only lastDeliveryError for failure, but APNs can return non-2xx statuses without populating apnsReason, leaving lastDeliveryError null. This obscures actual delivery issues.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f81c2ca. Configure here.

}

return rows;
}
11 changes: 11 additions & 0 deletions apps/mobile/src/features/agent-awareness/registrationPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import type { RelayDeviceRegistrationRequest } from "@t3tools/contracts/relay";

import type { Preferences } from "../../lib/storage";

// Development builds are Xcode-signed and receive sandbox APNs tokens;
// preview and production builds are distribution-signed and use production
// APNs. The relay routes each device's pushes accordingly.
export function resolveApsEnvironment(appVariant: unknown): "sandbox" | "production" {
return appVariant === "development" ? "sandbox" : "production";
}

export function makeRelayDeviceRegistrationRequest(input: {
readonly deviceId: string;
readonly label: string;
readonly iosMajorVersion: number;
readonly appVersion?: string;
readonly bundleId?: string;
readonly apsEnvironment?: "sandbox" | "production";
readonly pushToken?: string;
readonly pushToStartToken?: string;
readonly notificationsEnabled: boolean;
Expand All @@ -19,6 +28,8 @@ export function makeRelayDeviceRegistrationRequest(input: {
platform: "ios",
iosMajorVersion: input.iosMajorVersion,
appVersion: input.appVersion,
...(input.bundleId ? { bundleId: input.bundleId } : {}),
...(input.apsEnvironment ? { apsEnvironment: input.apsEnvironment } : {}),
...(input.pushToken ? { pushToken: input.pushToken } : {}),
...(input.pushToStartToken ? { pushToStartToken: input.pushToStartToken } : {}),
preferences: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { verifyDpopProof } from "@t3tools/shared/dpop";
import type { SavedRemoteConnection } from "../../lib/connection";
import { cryptoLayer } from "../cloud/dpop";
import { managedRelayClientLayer } from "../cloud/managedRelayLayer";
import { makeRelayDeviceRegistrationRequest } from "./registrationPayload";
import { makeRelayDeviceRegistrationRequest, resolveApsEnvironment } from "./registrationPayload";
import {
AgentAwarenessOperationError,
__resetAgentAwarenessRemoteRegistrationForTest,
Expand All @@ -41,6 +41,9 @@ const backgroundRuntime = vi.hoisted(() => ({
readonly resolve: (exit: Exit.Exit<unknown, unknown>) => void;
}>,
}));
const appStateMock = vi.hoisted(() => ({
listeners: [] as Array<(state: string) => void>,
}));

vi.mock("expo-constants", () => ({
default: {
Expand Down Expand Up @@ -100,6 +103,19 @@ vi.mock("react-native", () => ({
OS: "ios",
Version: "18.0",
},
AppState: {
addEventListener: (_event: string, listener: (state: string) => void) => {
appStateMock.listeners.push(listener);
return {
remove: () => {
const index = appStateMock.listeners.indexOf(listener);
if (index >= 0) {
appStateMock.listeners.splice(index, 1);
}
},
};
},
},
}));

vi.mock("../../lib/runtime", () => ({
Expand Down Expand Up @@ -176,6 +192,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
backgroundRuntime.pending.length = 0;
Constants.expoConfig!.extra = {};
__resetAgentAwarenessRemoteRegistrationForTest();
appStateMock.listeners.length = 0;
widgetMocks.getInstances.mockReset();
widgetMocks.getInstances.mockReturnValue([]);
});
Expand Down Expand Up @@ -213,6 +230,31 @@ describe("makeRelayDeviceRegistrationRequest", () => {
});
});

it("registers the app's APNs routing so the relay targets the right bundle", () => {
expect(
makeRelayDeviceRegistrationRequest({
deviceId: "device-1",
label: "Julius's iPhone",
iosMajorVersion: 18,
appVersion: "1.0.0",
bundleId: "com.t3tools.t3code.preview",
apsEnvironment: resolveApsEnvironment("preview"),
notificationsEnabled: true,
preferences: {},
}),
).toMatchObject({
bundleId: "com.t3tools.t3code.preview",
apsEnvironment: "production",
});
});

it("routes development builds to the APNs sandbox", () => {
expect(resolveApsEnvironment("development")).toBe("sandbox");
expect(resolveApsEnvironment("preview")).toBe("production");
expect(resolveApsEnvironment("production")).toBe("production");
expect(resolveApsEnvironment(undefined)).toBe("production");
});

it("marks notification delivery disabled when APNs permission is unavailable", () => {
expect(
makeRelayDeviceRegistrationRequest({
Expand Down Expand Up @@ -329,6 +371,53 @@ describe("makeRelayDeviceRegistrationRequest", () => {
},
);

it.effect(
"re-registers active Live Activity tokens when the app returns to the foreground",
() => {
const activity = {
getPushToken: vi.fn(() => Promise.resolve("activity-token")),
addPushTokenListener: vi.fn(),
};
widgetMocks.getInstances.mockReturnValue([activity] as never);
setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));

return Effect.gen(function* () {
yield* runBackgroundOperations();
activity.getPushToken.mockClear();

expect(appStateMock.listeners).toHaveLength(1);
for (const listener of appStateMock.listeners) {
listener("background");
}
yield* runBackgroundOperations();
expect(activity.getPushToken).not.toHaveBeenCalled();

for (const listener of appStateMock.listeners) {
listener("active");
}
yield* runBackgroundOperations();
expect(activity.getPushToken).toHaveBeenCalled();
}).pipe(Effect.provide(relayTestLayer));
},
);

it("ends local Live Activities and stops foreground reconciliation on cloud sign-out", () => {
const end = vi.fn(() => Promise.resolve());
const activity = {
getPushToken: vi.fn(() => Promise.resolve("activity-token")),
addPushTokenListener: vi.fn(),
end,
};
widgetMocks.getInstances.mockReturnValue([activity] as never);
setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));
expect(appStateMock.listeners).toHaveLength(1);

setAgentAwarenessRelayTokenProvider(null);

expect(end).toHaveBeenCalledWith("immediate");
expect(appStateMock.listeners).toHaveLength(0);
});

it.effect("refreshes APNs registration for connected environments after settings changes", () => {
registerAgentAwarenessConnection(savedConnection());
return Effect.gen(function* () {
Expand Down
Loading
Loading