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)