Skip to content
Open
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
6 changes: 6 additions & 0 deletions apps/mobile/app.config.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

infoPlist: {
NSAppTransportSecurity: {

The frequentUpdates: true option under the expo-widgets plugin does not add the required NSSupportsLiveActivitiesFrequentUpdates key to Info.plist, so iOS keeps the standard Live Activity update budget and frequent updates are never enabled. Set NSSupportsLiveActivitiesFrequentUpdates: true in the ios.infoPlist block to actually grant the entitlement.

    infoPlist: {
+     NSSupportsLiveActivitiesFrequentUpdates: true,
      NSAppTransportSecurity: {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/app.config.ts around lines 96-97:

The `frequentUpdates: true` option under the `expo-widgets` plugin does not add the required `NSSupportsLiveActivitiesFrequentUpdates` key to `Info.plist`, so iOS keeps the standard Live Activity update budget and frequent updates are never enabled. Set `NSSupportsLiveActivitiesFrequentUpdates: true` in the `ios.infoPlist` block to actually grant the entitlement.

Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ const config: ExpoConfig = {
bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${variant.iosBundleIdentifier}`,
enablePushNotifications: true,
// Agent activity can update many times an hour; without the
// frequent-updates entitlement iOS throttles the update budget sooner.
frequentUpdates: true,
widgets: [
{
name: "AgentActivity",
Expand All @@ -171,6 +174,9 @@ const config: ExpoConfig = {
],
},
],
// Must run after expo-widgets so the widget target exists when it wires in
// the branded logo asset catalog.
"./plugins/withWidgetLogoAsset.cjs",
"./plugins/withIosSceneLifecycle.cjs",
"./plugins/withAndroidCleartextTraffic.cjs",
],
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/assets/widget/T3Mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions apps/mobile/plugins/lib/addWidgetAssetCatalog.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use strict";

// Bundle the widget asset catalog into an app-extension (widget) target.
//
// expo-widgets generates the widget target without a Resources build phase, and
// hand-adding a PBXResourcesBuildPhase does NOT get picked up — xcodebuild's
// planner never schedules `actool` for it (verified: even a full re-plan skips
// it). So instead we add a shell-script phase that runs `actool` directly and
// drops the compiled Assets.car into the extension bundle. Marked
// alwaysOutOfDate so the build system always runs it.
//
// Idempotent across re-runs. Returns true when it added the phase, false when it
// was already present or the target was not found.

const PHASE_NAME = "Compile Widget Assets";

// Compiles ExpoWidgetsTarget/Assets.xcassets into the extension's resources dir.
// Uses only Xcode-provided build settings so it works for device + simulator.
const ACTOOL_SCRIPT = [
"set -e",
'CATALOG="${SRCROOT}/ExpoWidgetsTarget/Assets.xcassets"',
'if [ ! -d "$CATALOG" ]; then',
' echo "warning: widget asset catalog not found at $CATALOG"',
" exit 0",
"fi",
'DEST="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"',
'mkdir -p "$DEST"',
'xcrun actool "$CATALOG" --compile "$DEST" --platform "${PLATFORM_NAME}" --minimum-deployment-target "${IPHONEOS_DEPLOYMENT_TARGET:-16.0}" --output-format human-readable-text',
].join("\n");

function stripComments(map) {
const out = {};
for (const key of Object.keys(map || {})) {
if (key.endsWith("_comment")) continue;
out[key] = map[key];
}
return out;
}

function findByName(map, name) {
for (const [uuid, value] of Object.entries(stripComments(map))) {
if (value && value.name === name) return { uuid, value };
}
return null;
}

/**
* @param {import('xcode').XcodeProject} proj
* @param {{ targetName: string }} opts
*/
function addWidgetAssetCatalog(proj, opts) {
const objects = proj.hash.project.objects;
const target = findByName(objects.PBXNativeTarget, opts.targetName);
if (!target) return false;

const phases = target.value.buildPhases || [];
const existing = objects.PBXShellScriptBuildPhase || {};
const already = Object.entries(stripComments(existing)).some(
([uuid, value]) =>
value && value.name === `"${PHASE_NAME}"` && phases.some((p) => p.value === uuid),
);
if (already) return false;

const { uuid } = proj.addBuildPhase([], "PBXShellScriptBuildPhase", PHASE_NAME, target.uuid, {
shellPath: "/bin/sh",
shellScript: ACTOOL_SCRIPT,
});
// Always run: input-analysis is exactly what skipped the Resources phase.
objects.PBXShellScriptBuildPhase[uuid].alwaysOutOfDate = 1;
return true;
}

module.exports = { addWidgetAssetCatalog };
62 changes: 62 additions & 0 deletions apps/mobile/plugins/withWidgetLogoAsset.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use strict";

// Ships the branded T3 mark to the Live Activity / widget extension.
//
// expo-widgets generates ExpoWidgetsTarget without a Resources build phase and
// has no asset support, so this plugin (a) writes an SVG template image set into
// the generated widget asset catalog and (b) wires that catalog into the widget
// target with a dedicated Resources build phase. Both steps are idempotent and
// survive `expo prebuild --clean`. Must be listed AFTER "expo-widgets" in the
// plugins array so the widget target exists when this runs.

const path = require("path");
const fs = require("fs");
const { withDangerousMod, withXcodeProject } = require("expo/config-plugins");
const { addWidgetAssetCatalog } = require("./lib/addWidgetAssetCatalog.cjs");

const TARGET_NAME = "ExpoWidgetsTarget";
const CATALOG_NAME = "Assets.xcassets";
const IMAGE_SET = "T3Mark.imageset";
const SVG_NAME = "T3Mark.svg";

const CATALOG_CONTENTS = JSON.stringify({ info: { author: "expo", version: 1 } }, null, 2) + "\n";
const IMAGE_SET_CONTENTS =
JSON.stringify(
{
images: [{ idiom: "universal", filename: SVG_NAME }],
info: { author: "expo", version: 1 },
properties: {
"preserves-vector-representation": true,
"template-rendering-intent": "template",
},
},
null,
2,
) + "\n";

function withAssetFiles(config) {
return withDangerousMod(config, [
"ios",
(cfg) => {
const source = path.join(cfg.modRequest.projectRoot, "assets", "widget", SVG_NAME);
const catalogDir = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, CATALOG_NAME);
const imageSetDir = path.join(catalogDir, IMAGE_SET);
fs.mkdirSync(imageSetDir, { recursive: true });
fs.writeFileSync(path.join(catalogDir, "Contents.json"), CATALOG_CONTENTS);
fs.writeFileSync(path.join(imageSetDir, "Contents.json"), IMAGE_SET_CONTENTS);
fs.copyFileSync(source, path.join(imageSetDir, SVG_NAME));
return cfg;
},
]);
}

function withAssetWiring(config) {
return withXcodeProject(config, (cfg) => {
addWidgetAssetCatalog(cfg.modResults, { targetName: TARGET_NAME });
return cfg;
});
}

module.exports = function withWidgetLogoAsset(config) {
return withAssetWiring(withAssetFiles(config));
};
30 changes: 30 additions & 0 deletions apps/mobile/scripts/wire-widget-asset-catalog.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use strict";

// One-off: apply the widget asset-catalog wiring to the already-generated
// ios/ project so the current build compiles ExpoWidgetsTarget/Assets.xcassets
// without a full `expo prebuild`. The durable equivalent lives in
// plugins/withWidgetLogoAsset.cjs and runs on prebuild.

const path = require("path");
const fs = require("fs");

const xcodePath = require.resolve("xcode", {
paths: [
require.resolve("@expo/config-plugins", { paths: [require.resolve("expo/package.json")] }),
],
});
const xcode = require(xcodePath);
const { addWidgetAssetCatalog } = require("../plugins/lib/addWidgetAssetCatalog.cjs");

const pbxprojPath = path.join(__dirname, "..", "ios", "T3CodeDev.xcodeproj", "project.pbxproj");
const proj = xcode.project(pbxprojPath);
proj.parseSync();

const added = addWidgetAssetCatalog(proj, { targetName: "ExpoWidgetsTarget" });

if (added) {
fs.writeFileSync(pbxprojPath, proj.writeSync());
console.log("Added widget asset-compile phase to ExpoWidgetsTarget.");
} else {
console.log("No change: phase already present or target not found.");
}
11 changes: 11 additions & 0 deletions apps/mobile/src/features/agent-awareness/registrationPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import type { RelayDeviceRegistrationRequest } from "@t3tools/contracts/relay";

import type { Preferences } from "../../lib/storage";

// Development builds are Xcode-signed and receive sandbox APNs tokens;
// preview and production builds are distribution-signed and use production
// APNs. The relay routes each device's pushes accordingly.
export function resolveApsEnvironment(appVariant: unknown): "sandbox" | "production" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agent-awareness/registrationPayload.ts:8

resolveApsEnvironment returns "production" for every value except the literal string "development". Locally run ios:preview and ios:prod builds launched via expo run:ios are development-signed and receive sandbox APNs tokens, but this function classifies them as "production". The relay then routes pushes to the production APNs gateway for a sandbox token, so notifications and Live Activities fail to deliver for those builds. Consider keying off the signing/provisioning environment (e.g. EAS build profile or Configuration/ entitlements) rather than the variant name, or document why locally-run preview/prod builds are expected to use production APNs.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/agent-awareness/registrationPayload.ts around line 8:

`resolveApsEnvironment` returns `"production"` for every value except the literal string `"development"`. Locally run `ios:preview` and `ios:prod` builds launched via `expo run:ios` are development-signed and receive sandbox APNs tokens, but this function classifies them as `"production"`. The relay then routes pushes to the production APNs gateway for a sandbox token, so notifications and Live Activities fail to deliver for those builds. Consider keying off the signing/provisioning environment (e.g. EAS build profile or `Configuration`/ entitlements) rather than the variant name, or document why locally-run `preview`/`prod` builds are expected to use production APNs.

return appVariant === "development" ? "sandbox" : "production";
Comment thread
cursor[bot] marked this conversation as resolved.
}

export function makeRelayDeviceRegistrationRequest(input: {
readonly deviceId: string;
readonly label: string;
readonly iosMajorVersion: number;
readonly appVersion?: string;
readonly bundleId?: string;
readonly apsEnvironment?: "sandbox" | "production";
readonly pushToken?: string;
readonly pushToStartToken?: string;
readonly notificationsEnabled: boolean;
Expand All @@ -19,6 +28,8 @@ export function makeRelayDeviceRegistrationRequest(input: {
platform: "ios",
iosMajorVersion: input.iosMajorVersion,
appVersion: input.appVersion,
...(input.bundleId ? { bundleId: input.bundleId } : {}),
...(input.apsEnvironment ? { apsEnvironment: input.apsEnvironment } : {}),
...(input.pushToken ? { pushToken: input.pushToken } : {}),
...(input.pushToStartToken ? { pushToStartToken: input.pushToStartToken } : {}),
preferences: {
Expand Down
Loading
Loading