Skip to content
Merged
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
94 changes: 55 additions & 39 deletions plugins/withCheckInLiveActivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,12 @@ const withCheckInLiveActivity = (config, props = {}) => {

fs.writeFileSync(path.join(appDir, 'CheckInTimerActivityManager.swift'), ACTIVITY_MANAGER_SWIFT);
fs.writeFileSync(path.join(appDir, 'CheckInTimerActivityBridge.m'), BRIDGE_OBJC);
// The ActivityAttributes type must also be compiled into the MAIN APP target,
// not just the widget extension: CheckInTimerActivityManager.swift (app target)
// references CheckInTimerAttributes to start/update/end the activity. ActivityKit
// matches activities across app and widget by the attributes type name, so an
// identical copy compiled into each target is the supported pattern.
fs.writeFileSync(path.join(appDir, 'CheckInTimerAttributes.swift'), ATTRIBUTES_SWIFT);

return config;
},
Expand All @@ -376,8 +382,54 @@ const withCheckInLiveActivity = (config, props = {}) => {
const WIDGET_NAME = 'CheckInTimerWidget';
const widgetBundleId = `${appBundleId}.${WIDGET_NAME}`;

// Idempotent: skip if the target was already added in a previous prebuild run.
// addTarget stores names with surrounding quotes in the comment key, so check both forms.
// Host-target handles, resolved up front. The host-target wiring below must run
// on EVERY prebuild — including when the widget target already exists — so an
// incremental (non `--clean`) prebuild over an existing project still gets the
// bridge files + the ActivityAttributes source compiled into the app target.
// (These were previously placed after the widget early-return, which skipped
// them whenever the widget target already existed.)
const targetSection = project.pbxNativeTargetSection();
const hostTarget = project.getFirstTarget();
const appName = resolveIosAppName(config, projectRoot);

// Host-target wiring (idempotent via hasFile / existsSync guards):
// compile the bridge files AND the ActivityAttributes type into the main app
// target so the native module links and CheckInTimerActivityManager can see
// CheckInTimerAttributes (also compiled into the widget via its own copy).
const mainGroupKey = project.findPBXGroupKey({ name: appName });
const BRIDGE_FILES = [
`${appName}/CheckInTimerAttributes.swift`,
`${appName}/CheckInTimerActivityManager.swift`,
`${appName}/CheckInTimerActivityBridge.m`,
];
for (const filePath of BRIDGE_FILES) {
if (!project.hasFile(filePath)) {
project.addSourceFile(filePath, { target: hostTarget.uuid }, mainGroupKey);
}
}

// Ensure the main target has a bridging header so the ObjC bridge module is
// visible to Swift.
const BRIDGING_HEADER_FILE = `${appName}/${appName}-Bridging-Header.h`;
const bridgingHeaderPath = path.join(projectRoot, 'ios', BRIDGING_HEADER_FILE);
if (!fs.existsSync(bridgingHeaderPath)) {
fs.writeFileSync(bridgingHeaderPath, `// Auto-generated bridging header for Live Activity native bridge.\n#import <React/RCTBridgeModule.h>\n`);
}
const mainBuildConfigListId = targetSection[hostTarget.uuid].buildConfigurationList;
const mainBuildConfigList = project.pbxXCConfigurationList()[mainBuildConfigListId];
if (mainBuildConfigList) {
for (const { value: mainConfigUuid } of mainBuildConfigList.buildConfigurations) {
const mainBuildConfig = project.pbxXCBuildConfigurationSection()[mainConfigUuid];
if (mainBuildConfig && !mainBuildConfig.buildSettings.SWIFT_OBJC_BRIDGING_HEADER) {
mainBuildConfig.buildSettings.SWIFT_OBJC_BRIDGING_HEADER = `"${BRIDGING_HEADER_FILE}"`;
}
}
}

// 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;
}
Expand Down Expand Up @@ -431,11 +483,10 @@ const withCheckInLiveActivity = (config, props = {}) => {

// 6. Patch build settings on both Debug and Release configurations so the
// widget compiles as a Swift 5 app-extension targeting iOS 16.1+.
const targetSection = project.pbxNativeTargetSection();
// (targetSection and hostTarget were resolved at the top of this callback.)

// Resolve host app version numbers so the widget extension stays in sync.
// Priority: main target build settings → Expo config → hardcoded fallback.
const hostTarget = project.getFirstTarget();
let hostMarketingVersion = null;
let hostCurrentProjectVersion = null;
const hostConfigListId = targetSection[hostTarget.uuid].buildConfigurationList;
Expand Down Expand Up @@ -514,41 +565,6 @@ const withCheckInLiveActivity = (config, props = {}) => {
}
}

// 7. Resolve the iOS app target name inside this callback scope.
const appName = resolveIosAppName(config, projectRoot);

// 8. Ensure the bridge files are compiled as part of the main app target
// so the native module is linked at runtime.
const mainGroupKey = project.findPBXGroupKey({ name: appName });
const BRIDGE_FILES = [
`${appName}/CheckInTimerActivityManager.swift`,
`${appName}/CheckInTimerActivityBridge.m`,
];
for (const filePath of BRIDGE_FILES) {
if (!project.hasFile(filePath)) {
project.addSourceFile(filePath, { target: hostTarget.uuid }, mainGroupKey);
}
}

// 8. Ensure the main target has a bridging header configured so the ObjC
// bridge module is visible to Swift.
const BRIDGING_HEADER_FILE = `${appName}/${appName}-Bridging-Header.h`;
const bridgingHeaderPath = path.join(projectRoot, 'ios', BRIDGING_HEADER_FILE);
if (!fs.existsSync(bridgingHeaderPath)) {
fs.writeFileSync(bridgingHeaderPath, `// Auto-generated bridging header for Live Activity native bridge.\n#import <React/RCTBridgeModule.h>\n`);
}

const mainBuildConfigListId = targetSection[hostTarget.uuid].buildConfigurationList;
const mainBuildConfigList = project.pbxXCConfigurationList()[mainBuildConfigListId];
if (mainBuildConfigList) {
for (const { value: mainConfigUuid } of mainBuildConfigList.buildConfigurations) {
const mainBuildConfig = project.pbxXCBuildConfigurationSection()[mainConfigUuid];
if (mainBuildConfig && !mainBuildConfig.buildSettings.SWIFT_OBJC_BRIDGING_HEADER) {
mainBuildConfig.buildSettings.SWIFT_OBJC_BRIDGING_HEADER = `"${BRIDGING_HEADER_FILE}"`;
}
}
}

return config;
});

Expand Down
Loading