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
1 change: 0 additions & 1 deletion app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'./plugins/withCheckInLiveActivity.js',
{
teamId: 'QKQVAJMTCN',
enableLiveActivityEntitlement: Env.APP_ENV === 'production' || Env.APP_ENV === 'internal',
},
],
'./plugins/withInCallAudioModule.js',
Expand Down
143 changes: 81 additions & 62 deletions plugins/withCheckInLiveActivity.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { withDangerousMod, withInfoPlist, withEntitlementsPlist, withXcodeProject } = require('expo/config-plugins');
const { withDangerousMod, withInfoPlist, withXcodeProject } = require('expo/config-plugins');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find stale Live Activity entitlement keys in native entitlement files and confirm plugin cleanup coverage.
# Expected after the fix: no *.entitlements file contains com.apple.developer.live-activity; the plugin may only mention it in comments/deletion logic.

fd -e entitlements -x sh -c 'echo "== $1 =="; rg -n "com\.apple\.developer\.live-activity" "$1" || true' sh {}

fd -i 'withCheckInLiveActivity.js' -x sh -c 'echo "== $1 =="; rg -n -C3 "withEntitlementsPlist|com\.apple\.developer\.live-activity|NSSupportsLiveActivities" "$1"' sh {}

Repository: Resgrid/Unit

Length of output: 961


🏁 Script executed:

head -5 plugins/withCheckInLiveActivity.js

Repository: Resgrid/Unit

Length of output: 211


🏁 Script executed:

find . -name "*.entitlements" -type f 2>/dev/null | head -20

Repository: Resgrid/Unit

Length of output: 38


Add cleanup logic to remove stale invalid entitlements.

The current change prevents writing com.apple.developer.live-activity going forward, but projects that ran earlier plugin versions may still have this key in their native entitlements. Since lines 310–314 document that this key is invalid and archive-breaking, add a mod to actively delete it during prebuild to clean up existing projects:

Proposed cleanup
-const { withDangerousMod, withInfoPlist, withXcodeProject } = require('expo/config-plugins');
+const { withDangerousMod, withEntitlementsPlist, withInfoPlist, withXcodeProject } = require('expo/config-plugins');

@@
   config = withInfoPlist(config, (config) => {
     config.modResults.NSSupportsLiveActivities = true;
     return config;
   });
+
+  config = withEntitlementsPlist(config, (config) => {
+    delete config.modResults['com.apple.developer.live-activity'];
+    return config;
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/withCheckInLiveActivity.js` at line 1, The plugin currently prevents
writing the invalid com.apple.developer.live-activity entitlement going forward,
but existing projects may still have this stale key in their native entitlements
file. Add a cleanup mod using the withInfoPlist function (which is already
imported) that actively removes the com.apple.developer.live-activity key from
the Info.plist during prebuild to ensure old projects get cleaned up when
running the plugin, preventing the archive-breaking issues documented around
lines 310–314. Execute this cleanup before any other modifications to the
entitlements.

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

Expand Down Expand Up @@ -299,21 +299,19 @@ const WIDGET_INFO_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
`;

const withCheckInLiveActivity = (config, props = {}) => {
const { teamId, enableLiveActivityEntitlement = true } = props;
const { teamId } = props;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Step 1: Add NSSupportsLiveActivities to Info.plist
config = withInfoPlist(config, (config) => {
config.modResults.NSSupportsLiveActivities = true;
return config;
});

// Step 2: Add live activity entitlement (only if the provisioning profile supports it)
if (enableLiveActivityEntitlement) {
config = withEntitlementsPlist(config, (config) => {
config.modResults['com.apple.developer.live-activity'] = true;
return config;
});
}
// Step 2: (intentionally none) Live Activities require NO code-signing entitlement —
// only the NSSupportsLiveActivities Info.plist key above. Do NOT add
// `com.apple.developer.live-activity`: it is not a valid Apple entitlement, so the
// provisioning profile cannot include it and the archive fails with "Entitlement
// com.apple.developer.live-activity not found and could not be included in profile."

// Step 3: Write Swift Widget Extension files and native bridge
config = withDangerousMod(config, [
Expand Down Expand Up @@ -426,60 +424,81 @@ const withCheckInLiveActivity = (config, props = {}) => {
}
}

// Widget-target creation is idempotent: skip if it was already added in a
// previous prebuild run. addTarget stores names with surrounding quotes in the
// comment key, so check both forms. (Only widget CREATION is gated here — the
// host-target wiring above already ran.)
if (project.pbxTargetByName(WIDGET_NAME) || project.pbxTargetByName(`"${WIDGET_NAME}"`)) {
return config;
}
// Resolve the widget target: reuse an existing one (created in a previous
// prebuild) or create it now. Only the one-time CREATION steps below are gated by
// this check — the build-settings / signing patches further down run for BOTH the
// new and existing cases, so an updated teamId or version is applied even when the
// widget target already exists (e.g. on an incremental, non `--clean` prebuild).
// addTarget stores names with surrounding quotes in the comment key, so check both
// forms.
const widgetAlreadyExists = !!(project.pbxTargetByName(WIDGET_NAME) || project.pbxTargetByName(`"${WIDGET_NAME}"`));
let widgetTargetUuid = null;

if (widgetAlreadyExists) {
// Find the existing target's uuid in the native-target section (skip the
// companion "<uuid>_comment" string entries).
for (const key of Object.keys(targetSection)) {
if (key.endsWith('_comment')) continue;
const existing = targetSection[key];
if (existing && (existing.name === WIDGET_NAME || existing.name === `"${WIDGET_NAME}"`)) {
widgetTargetUuid = key;
break;
}
}
} else {
// 1. Create the PBXNativeTarget.
// addTarget('app_extension') also:
// - adds an "Embed App Extensions" CopyFiles phase to the main target
// - adds a PBXTargetDependency from main app → widget
// - creates Debug/Release XCBuildConfigurations with basic defaults
const widgetTarget = project.addTarget(WIDGET_NAME, 'app_extension', WIDGET_NAME, widgetBundleId);
widgetTargetUuid = widgetTarget.uuid;

// 2. Add the three build phases the widget target needs.
// These must be added before files/frameworks are wired, because the
// addSourceFile / addFramework helpers find phases by scanning the
// target's buildPhases array.
project.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', widgetTarget.uuid);
project.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', widgetTarget.uuid);
project.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', widgetTarget.uuid);

// 3. Create a PBX group for the widget folder and attach it to the project's
// main group so the files appear in the Xcode file navigator.
const { uuid: widgetGroupUuid } = project.addPbxGroup([], WIDGET_NAME, WIDGET_NAME);
const { firstProject } = project.getFirstProject();
const mainGroup = project.getPBXGroupByKey(firstProject.mainGroup);
if (mainGroup && !mainGroup.children.find((c) => c.comment === WIDGET_NAME)) {
mainGroup.children.push({ value: widgetGroupUuid, comment: WIDGET_NAME });
}

// 1. Create the PBXNativeTarget.
// addTarget('app_extension') also:
// - adds an "Embed App Extensions" CopyFiles phase to the main target
// - adds a PBXTargetDependency from main app → widget
// - creates Debug/Release XCBuildConfigurations with basic defaults
const widgetTarget = project.addTarget(WIDGET_NAME, 'app_extension', WIDGET_NAME, widgetBundleId);

// 2. Add the three build phases the widget target needs.
// These must be added before files/frameworks are wired, because the
// addSourceFile / addFramework helpers find phases by scanning the
// target's buildPhases array.
project.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', widgetTarget.uuid);
project.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', widgetTarget.uuid);
project.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', widgetTarget.uuid);

// 3. Create a PBX group for the widget folder and attach it to the project's
// main group so the files appear in the Xcode file navigator.
const { uuid: widgetGroupUuid } = project.addPbxGroup([], WIDGET_NAME, WIDGET_NAME);
const { firstProject } = project.getFirstProject();
const mainGroup = project.getPBXGroupByKey(firstProject.mainGroup);
if (mainGroup && !mainGroup.children.find((c) => c.comment === WIDGET_NAME)) {
mainGroup.children.push({ value: widgetGroupUuid, comment: WIDGET_NAME });
}
// 4. Add Swift source files to the widget group and to the widget's Sources phase.
// Passing the group key as the third argument to addSourceFile ensures the
// file reference lands in the right PBX group; opt.target routes the build
// file to the widget's PBXSourcesBuildPhase rather than the main app's.
const SWIFT_SOURCES = [
'CheckInTimerAttributes.swift',
'CheckInTimerLiveActivity.swift',
'CheckInTimerWidgetBundle.swift',
];
for (const filename of SWIFT_SOURCES) {
project.addSourceFile(
filename,
{ target: widgetTarget.uuid },
widgetGroupUuid
);
}

// 4. Add Swift source files to the widget group and to the widget's Sources phase.
// Passing the group key as the third argument to addSourceFile ensures the
// file reference lands in the right PBX group; opt.target routes the build
// file to the widget's PBXSourcesBuildPhase rather than the main app's.
const SWIFT_SOURCES = [
'CheckInTimerAttributes.swift',
'CheckInTimerLiveActivity.swift',
'CheckInTimerWidgetBundle.swift',
];
for (const filename of SWIFT_SOURCES) {
project.addSourceFile(
filename,
{ target: widgetTarget.uuid },
widgetGroupUuid
);
// 5. Link WidgetKit and ActivityKit into the widget's Frameworks phase.
// opt.target directs addToPbxFrameworksBuildPhase to use the widget's
// PBXFrameworksBuildPhase (added above) instead of the main app's.
project.addFramework('WidgetKit.framework', { target: widgetTarget.uuid });
project.addFramework('ActivityKit.framework', { target: widgetTarget.uuid });
}

// 5. Link WidgetKit and ActivityKit into the widget's Frameworks phase.
// opt.target directs addToPbxFrameworksBuildPhase to use the widget's
// PBXFrameworksBuildPhase (added above) instead of the main app's.
project.addFramework('WidgetKit.framework', { target: widgetTarget.uuid });
project.addFramework('ActivityKit.framework', { target: widgetTarget.uuid });
// If the widget target can't be resolved, skip the build-settings/signing patches.
if (!widgetTargetUuid) {
return config;
}

// 6. Patch build settings on both Debug and Release configurations so the
// widget compiles as a Swift 5 app-extension targeting iOS 16.1+.
Expand Down Expand Up @@ -533,7 +552,7 @@ const withCheckInLiveActivity = (config, props = {}) => {
hostDevelopmentTeam = process.env.EXPO_APPLE_TEAM_ID || null;
}

const buildConfigListId = targetSection[widgetTarget.uuid].buildConfigurationList;
const buildConfigListId = targetSection[widgetTargetUuid].buildConfigurationList;
const buildConfigList = project.pbxXCConfigurationList()[buildConfigListId];
if (buildConfigList) {
for (const { value: configUuid } of buildConfigList.buildConfigurations) {
Expand Down Expand Up @@ -569,8 +588,8 @@ const withCheckInLiveActivity = (config, props = {}) => {
// the parent app (matches the host target's signing identity). Mirrors the
// Responder app's working Live Activity signing setup.
if (hostDevelopmentTeam) {
project.addTargetAttribute('DevelopmentTeam', hostDevelopmentTeam, widgetTarget);
project.addTargetAttribute('ProvisioningStyle', 'Automatic', widgetTarget);
project.addTargetAttribute('DevelopmentTeam', hostDevelopmentTeam, { uuid: widgetTargetUuid });
project.addTargetAttribute('ProvisioningStyle', 'Automatic', { uuid: widgetTargetUuid });
}

return config;
Expand Down
Loading