diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 1823ac903f6..d6413f2cc05 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.", ); } @@ -151,6 +156,8 @@ const config: ExpoConfig = { "expo-asset", "expo-font", "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", [ @@ -194,6 +201,7 @@ const config: ExpoConfig = { ], extra: { appVariant: APP_VARIANT, + iosPersonalTeamBuild: isIosPersonalTeamBuild, relay: { url: repoEnv.T3CODE_RELAY_URL ?? null, }, diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 865ba85e6e2..98141252bcc 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -106,7 +106,7 @@ "react-native": "0.85.3", "react-native-gesture-handler": "~2.31.1", "react-native-image-viewing": "^0.2.2", - "react-native-keyboard-controller": "1.21.6", + "react-native-keyboard-controller": "1.21.7", "react-native-nitro-markdown": "^0.5.0", "react-native-nitro-modules": "0.35.9", "react-native-reanimated": "4.3.1", 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 a4e6fc3d6db..7f3b48a471e 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"; export function makeRelayDeviceRegistrationRequest(input: { readonly deviceId: string; @@ -12,7 +13,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, @@ -23,7 +25,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 7f97d7c718c..db001f7d86c 100644 --- a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts +++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts @@ -213,6 +213,26 @@ describe("makeRelayDeviceRegistrationRequest", () => { }); }); + 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/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts index 3281381e0e1..f7e9ac9a463 100644 --- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts +++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts @@ -26,6 +26,7 @@ import { } from "../../lib/storage"; import AgentActivity, { type AgentActivityProps } from "../../widgets/AgentActivity"; import { resolveCloudPublicConfig } from "../cloud/publicConfig"; +import { supportsAgentAwarenessPush } from "./capabilities"; import { makeRelayDeviceRegistrationRequest } from "./registrationPayload"; const REMOTE_ACTIVITY_REGISTRATION_RETRY_MS = 15_000; @@ -157,7 +158,7 @@ function iosMajorVersion(): number { function nativePushTokenRegistration(observedPushToken?: string) { return Effect.gen(function* () { - if (!canRegisterRemoteLiveActivities()) { + if (!canRegisterRemoteLiveActivities() || !supportsAgentAwarenessPush()) { return { notificationsEnabled: false, pushToken: null }; } if (observedPushToken) { diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 11d363dbf69..e1a8d5d84e8 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -16,6 +16,7 @@ import { squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; 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 { refreshAgentAwarenessRegistration } from "../agent-awareness/remoteRegistration"; @@ -103,6 +104,7 @@ function LocalSettingsRouteScreen() { } function ConfiguredSettingsRouteScreen() { + const agentAwarenessPushAvailable = supportsAgentAwarenessPush(); const insets = useSafeAreaInsets(); const navigation = useNavigation(); const { expand: expandClerkSheet } = useClerkSettingsSheetDetent(); @@ -392,17 +394,27 @@ function ConfiguredSettingsRouteScreen() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ed19fd7903..69c1dd7c4c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -380,8 +380,8 @@ importers: specifier: ^0.2.2 version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-keyboard-controller: - specifier: 1.21.6 - version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: 1.21.7 + version: 1.21.7(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-markdown: specifier: ^0.5.0 version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -8831,8 +8831,8 @@ packages: react: '*' react-native: '*' - react-native-keyboard-controller@1.21.6: - resolution: {integrity: sha512-nAXCmar/W8Gn4iQV7O5fAVuTh57JszCsqTS+cfR95WFOLR/AfbwfPz/+sWyz/q2SOIe2VpyQzq6hzYiwErhqqw==} + react-native-keyboard-controller@1.21.7: + resolution: {integrity: sha512-gs+8nI8HYnRdDt4NWbk1iVuS6kDLf2taJvp+h/TjM1FBdtnQmlYLJ6buNiUqSnkIH4OFEAxdNr3/GOOYdLfkUQ==} peerDependencies: react: '*' react-native: '*' @@ -18977,7 +18977,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-keyboard-controller@1.21.7(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)