Skip to content
Merged
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
12 changes: 10 additions & 2 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
);
}

Expand Down Expand Up @@ -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",
[
Expand Down Expand Up @@ -242,6 +249,7 @@ const config: ExpoConfig = {
],
extra: {
appVariant: APP_VARIANT,
iosPersonalTeamBuild: isIosPersonalTeamBuild,
relay: {
url: repoEnv.T3CODE_RELAY_URL ?? null,
},
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/features/agent-awareness/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Constants from "expo-constants";

export function supportsAgentAwarenessPush() {
return Constants.expoConfig?.extra?.iosPersonalTeamBuild !== true;
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -34,7 +36,7 @@ export function makeRelayDeviceRegistrationRequest(input: {
...(input.pushToStartToken ? { pushToStartToken: input.pushToStartToken } : {}),
preferences: {
liveActivitiesEnabled,
notificationsEnabled: input.notificationsEnabled,
Comment thread
cursor[bot] marked this conversation as resolved.
notificationsEnabled: pushAvailable && input.notificationsEnabled,
notifyOnApproval: true,
notifyOnInput: true,
notifyOnCompletion: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
18 changes: 15 additions & 3 deletions apps/mobile/src/features/settings/SettingsRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -131,6 +132,7 @@ function LocalSettingsRouteScreen() {
}

function ConfiguredSettingsRouteScreen() {
const agentAwarenessPushAvailable = supportsAgentAwarenessPush();
const insets = useSafeAreaInsets();
const navigation = useNavigation();
const { expand: expandClerkSheet } = useClerkSettingsSheetDetent();
Expand Down Expand Up @@ -440,22 +442,32 @@ function ConfiguredSettingsRouteScreen() {
<SettingsSwitchRow
icon="bell.badge"
label="Device Notifications"
disabled={notificationStatus === "checking" || notificationStatus === "unsupported"}
disabled={
!agentAwarenessPushAvailable ||
notificationStatus === "checking" ||
notificationStatus === "unsupported"
}
// Only reads as on when this device is actually registered with the
// relay; otherwise notifications cannot be delivered regardless of
// the local iOS permission.
value={notificationStatus === "enabled" && deviceRegistered}
value={
agentAwarenessPushAvailable && notificationStatus === "enabled" && deviceRegistered
}
onValueChange={handleDeviceNotificationsChange}
/>
<SettingsSwitchRow
disabled={
!isLoaded || liveActivityStatus === "checking" || liveActivityStatus === "linking"
!agentAwarenessPushAvailable ||
!isLoaded ||
liveActivityStatus === "checking" ||
liveActivityStatus === "linking"
}
icon="bolt.circle"
label="Live Activity Updates"
// Same gate: a saved preference is meaningless until the device
// registration the relay needs to push updates has succeeded.
value={
agentAwarenessPushAvailable &&
(liveActivityStatus === "enabled" || liveActivityStatus === "linking") &&
deviceRegistered
}
Expand Down
Loading