From b503b09056c277ef330b208b91975c9f34f8207b Mon Sep 17 00:00:00 2001 From: Ben Davis Date: Fri, 26 Jun 2026 15:43:57 -0700 Subject: [PATCH 1/2] fix(mobile): disable unsupported push registration --- .../features/agent-awareness/capabilities.ts | 5 +++++ .../agent-awareness/registrationPayload.ts | 6 ++++-- .../remoteRegistration.test.ts | 20 +++++++++++++++++++ .../features/settings/SettingsRouteScreen.tsx | 18 ++++++++++++++--- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 apps/mobile/src/features/agent-awareness/capabilities.ts diff --git a/apps/mobile/src/features/agent-awareness/capabilities.ts b/apps/mobile/src/features/agent-awareness/capabilities.ts new file mode 100644 index 00000000000..d627f365754 --- /dev/null +++ b/apps/mobile/src/features/agent-awareness/capabilities.ts @@ -0,0 +1,5 @@ +import Constants from "expo-constants"; + +export function supportsAgentAwarenessPush() { + return Constants.expoConfig?.extra?.iosPersonalTeamBuild !== true; +} diff --git a/apps/mobile/src/features/agent-awareness/registrationPayload.ts b/apps/mobile/src/features/agent-awareness/registrationPayload.ts index cd2e36a403c..5a9d71470a1 100644 --- a/apps/mobile/src/features/agent-awareness/registrationPayload.ts +++ b/apps/mobile/src/features/agent-awareness/registrationPayload.ts @@ -1,6 +1,7 @@ import type { RelayDeviceRegistrationRequest } from "@t3tools/contracts/relay"; import type { Preferences } from "../../lib/storage"; +import { supportsAgentAwarenessPush } from "./capabilities"; // Development builds are Xcode-signed and receive sandbox APNs tokens; // preview and production builds are distribution-signed and use production @@ -21,7 +22,8 @@ export function makeRelayDeviceRegistrationRequest(input: { readonly notificationsEnabled: boolean; readonly preferences: Preferences; }): RelayDeviceRegistrationRequest { - const liveActivitiesEnabled = input.preferences.liveActivitiesEnabled !== false; + const pushAvailable = supportsAgentAwarenessPush(); + const liveActivitiesEnabled = pushAvailable && input.preferences.liveActivitiesEnabled !== false; return { deviceId: input.deviceId, label: input.label, @@ -34,7 +36,7 @@ export function makeRelayDeviceRegistrationRequest(input: { ...(input.pushToStartToken ? { pushToStartToken: input.pushToStartToken } : {}), preferences: { liveActivitiesEnabled, - notificationsEnabled: input.notificationsEnabled, + notificationsEnabled: pushAvailable && input.notificationsEnabled, notifyOnApproval: true, notifyOnInput: true, notifyOnCompletion: true, diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts index e296825c7ee..0dbf07497c0 100644 --- a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts +++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts @@ -286,6 +286,26 @@ describe("makeRelayDeviceRegistrationRequest", () => { expect(resolveApsEnvironment(undefined)).toBe("production"); }); + it("disables push features in Personal Team relay registrations", () => { + Constants.expoConfig!.extra = { iosPersonalTeamBuild: true }; + + expect( + makeRelayDeviceRegistrationRequest({ + deviceId: "device-1", + label: "Julius's iPhone", + iosMajorVersion: 18, + appVersion: "1.0.0", + pushToken: "apns-token", + pushToStartToken: "push-to-start-token", + notificationsEnabled: true, + preferences: {}, + }).preferences, + ).toMatchObject({ + liveActivitiesEnabled: false, + notificationsEnabled: false, + }); + }); + it("marks notification delivery disabled when APNs permission is unavailable", () => { expect( makeRelayDeviceRegistrationRequest({ diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index f56b197c14d..1c24a15f384 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -19,6 +19,7 @@ import { } from "@t3tools/client-runtime/state/runtime"; import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; import { AppText as Text } from "../../components/AppText"; +import { supportsAgentAwarenessPush } from "../agent-awareness/capabilities"; import { setLiveActivityUpdatesEnabled } from "../agent-awareness/liveActivityPreferences"; import { requestAgentNotificationPermission } from "../agent-awareness/notificationPermissions"; import { @@ -131,6 +132,7 @@ function LocalSettingsRouteScreen() { } function ConfiguredSettingsRouteScreen() { + const agentAwarenessPushAvailable = supportsAgentAwarenessPush(); const insets = useSafeAreaInsets(); const navigation = useNavigation(); const { expand: expandClerkSheet } = useClerkSettingsSheetDetent(); @@ -440,22 +442,32 @@ function ConfiguredSettingsRouteScreen() { Date: Wed, 1 Jul 2026 20:13:08 -0700 Subject: [PATCH 2/2] fix(mobile): harden personal-team config and restore keyboard-controller - Validate T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID as a reverse-DNS identifier - Expose extra.iosPersonalTeamBuild so runtime capability gating works - Gate native APNs token acquisition on supportsAgentAwarenessPush() so Personal Team builds never call getDevicePushTokenAsync without the aps-environment entitlement - Document why appleSignIn gating on the Clerk plugin is load-bearing - Restore react-native-keyboard-controller 1.21.7 (main bumped it in #3545; 1.21.6 here was a stale pin) Co-Authored-By: Claude Fable 5 --- apps/mobile/app.config.ts | 12 ++++++++++-- .../features/agent-awareness/remoteRegistration.ts | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 664659c44c8..81879fde9e0 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -11,10 +11,15 @@ const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT); const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1"; const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim(); +const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; -if (isIosPersonalTeamBuild && !personalTeamBundleIdentifier) { +if ( + isIosPersonalTeamBuild && + (!personalTeamBundleIdentifier || + !IOS_BUNDLE_IDENTIFIER_PATTERN.test(personalTeamBundleIdentifier)) +) { throw new Error( - "T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID is required when T3CODE_IOS_PERSONAL_TEAM=1.", + "T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID must be a reverse-DNS identifier such as com.example.t3code when T3CODE_IOS_PERSONAL_TEAM=1.", ); } @@ -191,6 +196,8 @@ const config: ExpoConfig = { }, ], "expo-secure-store", + // appleSignIn must be gated here: withoutIosPersonalTeamCapabilities.cjs runs before + // plugins earlier in this array, so it cannot strip the entitlement Clerk would add. ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }], "expo-web-browser", [ @@ -242,6 +249,7 @@ const config: ExpoConfig = { ], extra: { appVariant: APP_VARIANT, + iosPersonalTeamBuild: isIosPersonalTeamBuild, relay: { url: repoEnv.T3CODE_RELAY_URL ?? null, }, diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts index e9de93c1dcb..e2e2c3b9768 100644 --- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts +++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts @@ -30,6 +30,7 @@ import { } from "../../lib/storage"; import AgentActivity, { type AgentActivityProps } from "../../widgets/AgentActivity"; import { resolveCloudPublicConfig } from "../cloud/publicConfig"; +import { supportsAgentAwarenessPush } from "./capabilities"; import { makeRelayDeviceRegistrationRequest, resolveApsEnvironment } from "./registrationPayload"; const REMOTE_ACTIVITY_REGISTRATION_RETRY_MS = 15_000; @@ -237,7 +238,7 @@ function iosMajorVersion(): number { function nativePushTokenRegistration(observedPushToken?: string) { return Effect.gen(function* () { - if (!canRegisterRemoteLiveActivities()) { + if (!canRegisterRemoteLiveActivities() || !supportsAgentAwarenessPush()) { return { notificationsEnabled: false, pushToken: null }; } if (observedPushToken) {