Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import AgentActivity, { type AgentActivityProps } from "../../widgets/AgentActivity";

export function dismissEndedAgentLiveActivities() {
AgentActivity.dismissEnded();
}

export function getAgentLiveActivities() {
return AgentActivity.getInstances();
}
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/agent-awareness/agentLiveActivity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { LiveActivity } from "expo-widgets";
import type { AgentActivityProps } from "../../widgets/AgentActivity";

export function dismissEndedAgentLiveActivities(): void {}

export function getAgentLiveActivities(): Array<LiveActivity<AgentActivityProps>> {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ vi.mock("./androidNotifications", () => ({

const secureStore = vi.hoisted(() => new Map<string, string>());
const widgetMocks = vi.hoisted(() => ({
dismissEnded: vi.fn(),
getInstances: vi.fn(() => []),
start: vi.fn(() => ({})),
}));
Expand Down Expand Up @@ -97,6 +98,7 @@ vi.mock("expo-widgets", () => ({
}));

vi.mock("./agentLiveActivity", () => ({
dismissEndedAgentLiveActivities: widgetMocks.dismissEnded,
getAgentLiveActivities: widgetMocks.getInstances,
startAgentLiveActivity: widgetMocks.start,
}));
Expand Down Expand Up @@ -270,6 +272,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
widgetMocks.getInstances.mockReset();
widgetMocks.getInstances.mockReturnValue([]);
widgetMocks.start.mockClear();
widgetMocks.dismissEnded.mockReset();
environmentConfigsMock.configs.clear();
});

Expand Down Expand Up @@ -499,6 +502,43 @@ describe("makeRelayDeviceRegistrationRequest", () => {
},
);

it.effect(
"dismisses ended Live Activities before foreground discovery with no work running",
() => {
setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));

return Effect.gen(function* () {
yield* refreshActiveLiveActivityRemoteRegistration();

expect(widgetMocks.dismissEnded).toHaveBeenCalledOnce();
expect(widgetMocks.dismissEnded.mock.invocationCallOrder[0]).toBeLessThan(
widgetMocks.getInstances.mock.invocationCallOrder[0]!,
);
expect(widgetMocks.start).not.toHaveBeenCalled();
}).pipe(Effect.provide(layerRelayTest));
},
);

it.effect("still registers the active card when ended-card cleanup throws", () => {
widgetMocks.dismissEnded.mockImplementation(() => {
throw new Error("Cannot find function dismissEnded");
});
const activity = {
getPushToken: vi.fn(() => Promise.resolve("activity-token")),
addPushTokenListener: vi.fn(),
end: vi.fn(),
};
widgetMocks.getInstances.mockReturnValue([activity] as never);
setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));

return Effect.gen(function* () {
yield* refreshActiveLiveActivityRemoteRegistration();

expect(activity.getPushToken).toHaveBeenCalled();
expect(activity.end).not.toHaveBeenCalled();
}).pipe(Effect.provide(layerRelayTest));
});

it("ends local Live Activities and stops foreground reconciliation on cloud sign-out", () => {
const end = vi.fn(() => Promise.resolve());
const activity = {
Expand All @@ -513,6 +553,7 @@ describe("makeRelayDeviceRegistrationRequest", () => {
setAgentAwarenessRelayTokenProvider(null);

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

Expand Down
17 changes: 16 additions & 1 deletion apps/mobile/src/features/agent-awareness/remoteRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
saveAgentAwarenessRegistrationRecord,
} from "../../persistence/imperative";
import type { AgentActivityProps } from "../../widgets/AgentActivity";
import { getAgentLiveActivities, startAgentLiveActivity } from "./agentLiveActivity";
import {
dismissEndedAgentLiveActivities,
getAgentLiveActivities,
startAgentLiveActivity,
} from "./agentLiveActivity";
import { resolveCloudPublicConfig } from "../cloud/publicConfig";
import { supportsAgentAwarenessPush } from "./capabilities";
import { makeRelayDeviceRegistrationRequest, resolveApsEnvironment } from "./registrationPayload";
Expand Down Expand Up @@ -804,10 +808,20 @@
});
}

// Binaries built before the native dismissEnded method throw here.
function dismissEndedLocalLiveActivities(): void {
try {
dismissEndedAgentLiveActivities();
} catch (error) {
logRegistrationError("ended live activity cleanup failed", error);
}
}

function endLocalLiveActivities(context: string): void {
if (!canRegisterRemoteLiveActivities()) {
return;
}
dismissEndedLocalLiveActivities();
try {
for (const activity of getAgentLiveActivities()) {
activity.end("immediate").catch((error: unknown) => {
Expand Down Expand Up @@ -966,7 +980,7 @@
"live activity token listener registration failed",
);
}
});

Check failure on line 983 in apps/mobile/src/features/agent-awareness/remoteRegistration.ts

View workflow job for this annotation

GitHub Actions / Test

src/features/agent-awareness/remoteRegistration.test.ts > makeRelayDeviceRegistrationRequest > still registers the active card when ended-card cleanup throws

TypeError: activity.getId is not a function ❯ observeLiveActivityPushTokens src/features/agent-awareness/remoteRegistration.ts:983:31 ❯ src/features/agent-awareness/remoteRegistration.ts:952:18 ❯ IteratorImpl.~effect/Effect/successCont ../../node_modules/.pnpm/effect@4.0.5/node_modules/effect/src/internal/effect.ts:1422:28 ❯ IteratorImpl.~effect/Effect/evaluate ../../node_modules/.pnpm/effect@4.0.5/node_modules/effect/src/internal/effect.ts:1434:25 ❯ FiberImpl.runLoop ../../node_modules/.pnpm/effect@4.0.5/node_modules/effect/src/internal/effect.ts:654:41 ❯ FiberImpl.evaluate ../../node_modules/.pnpm/effect@4.0.5/node_modules/effect/src/internal/effect.ts:595:23 ❯ FiberImpl.evaluate ../../node_modules/.pnpm/effect@4.0.5/node_modules/effect/src/internal/effect.ts:589:27 ❯ ../../node_modules/.pnpm/effect@4.0.5/node_modules/effect/src/internal/effect.ts:1159:17
return false;
}

Expand Down Expand Up @@ -1033,6 +1047,7 @@
return;
}

dismissEndedLocalLiveActivities();
let activities = yield* Effect.try({
try: () => getAgentLiveActivities(),
catch: (cause) =>
Expand Down
65 changes: 58 additions & 7 deletions patches/expo-widgets@58.0.20.patch
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
diff --git a/build/Widgets.d.ts b/build/Widgets.d.ts
index fb37d5d..47a97af 100644
--- a/build/Widgets.d.ts
+++ b/build/Widgets.d.ts
@@ -100,6 +100,8 @@ export declare class LiveActivityFactory<T extends object = object> {
* @platform ios
*/
getInstances(): LiveActivity<T>[];
+ /** Dismisses ended instances that iOS still shows on the Lock Screen. */
+ dismissEnded(): void;
}
/**
* Creates a dismissal policy that removes the Live Activity at the specified time within a four-hour window.
diff --git a/build/Widgets.js b/build/Widgets.js
index cc1efce..3ce8897 100644
--- a/build/Widgets.js
+++ b/build/Widgets.js
@@ -134,6 +134,9 @@ import ExpoWidgetsModule from './ExpoWidgets';
*/ getInstances() {
return this.nativeLiveActivityFactory.getInstances().map((instance)=>new LiveActivity(instance));
}
+ dismissEnded() {
+ this.nativeLiveActivityFactory.dismissEnded();
+ }
}
/**
* Creates a dismissal policy that removes the Live Activity at the specified time within a four-hour window.
diff --git a/build/Widgets.types.d.ts b/build/Widgets.types.d.ts
index 4c474bb..b0173df 100644
--- a/build/Widgets.types.d.ts
Expand All @@ -14,20 +41,29 @@ index 4c474bb..b0173df 100644
* Whether the activity is displayed in a context with reduced luminance.
* @platform iOS 16+
diff --git a/ios/LiveActivityFactory.swift b/ios/LiveActivityFactory.swift
index 29e4f15..7143f9a 100644
index 29e4f15..629e547 100644
--- a/ios/LiveActivityFactory.swift
+++ b/ios/LiveActivityFactory.swift
@@ -19,6 +19,14 @@ final class LiveActivityFactory: SharedObject {
throw LiveActivitiesNotSupportedException()
}
@@ -14,11 +14,23 @@ final class LiveActivityFactory: SharedObject {
WidgetsStorage.set(layout, forKey: "__expo_widgets_live_activity_\(name)_layout")
}

+ // iOS ends an activity after 8 hours but leaves it frozen on the Lock Screen for up to
+ // 4 more. getInstances() can't see it, so a caller starting a replacement would leave
+ // both cards visible. Dismiss the ended ones now.
+ // iOS ends an activity after 8 hours but leaves it frozen on the Lock Screen for up to
+ // 4 more. getInstances() can't see it, so dismiss the ended ones directly.
+ func dismissEnded() {
+ for activity in Activity<LiveActivityAttributes>.activities
+ where activity.content.state.name == name && activity.activityState == .ended {
+ Task { await activity.end(nil, dismissalPolicy: .immediate) }
+ }
+ }
+
func start(props: String?, url: URL?, staleDate: Date?) throws -> LiveActivity {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
throw LiveActivitiesNotSupportedException()
}

+ // A replacement started beside an ended card would leave both visible.
+ dismissEnded()
+
do {
let initialState = LiveActivityAttributes.ContentState(name: name, props: props)
Expand Down Expand Up @@ -150,6 +186,21 @@ index 4d135c4..dae4744 100644
if #available(iOS 18.0, *) {
LiveActivityBanner(context: context, nodes: nodes)
} else if let node = nodes["banner"] as? [String: Any] {
diff --git a/ios/WidgetsModule.swift b/ios/WidgetsModule.swift
index 285136b..d36eac4 100644
--- a/ios/WidgetsModule.swift
+++ b/ios/WidgetsModule.swift
@@ -97,6 +97,10 @@ public final class WidgetsModule: Module {
Function("getInstances") { (liveActivity: LiveActivityFactory) in
try liveActivity.getInstances()
}
+
+ Function("dismissEnded") { (liveActivity: LiveActivityFactory) in
+ liveActivity.dismissEnded()
+ }
}

Class("LiveActivity", LiveActivity.self) {
diff --git a/src/Widgets.types.ts b/src/Widgets.types.ts
index 690d069..ffab6f3 100644
--- a/src/Widgets.types.ts
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading