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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ To run this project, you will need to add the following environment variables to

`appKey`

For iOS CI builds that generate the `CheckInTimerWidget` target, set `IOS_APPLE_TEAM_ID` (or `EXPO_APPLE_TEAM_ID` / `APPLE_TEAM_ID`) so the widget extension inherits the correct signing team during prebuild.



## :toolbox: Getting Started
Expand Down Expand Up @@ -116,4 +118,4 @@ npm run start

## :warning: License

Distributed under the Apache License 2.0. See LICENSE.txt for more information.
Distributed under the Apache License 2.0. See LICENSE.txt for more information.
8 changes: 7 additions & 1 deletion app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
buildNumber: packageJSON.version,
supportsTablet: true,
bundleIdentifier: Env.BUNDLE_ID,
...(Env.IOS_APPLE_TEAM_ID ? { appleTeamId: Env.IOS_APPLE_TEAM_ID } : {}),
requireFullScreen: true,
infoPlist: {
UIBackgroundModes: ['remote-notification', 'audio', 'bluetooth-central', 'voip'],
Expand Down Expand Up @@ -289,7 +290,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'./customGradle.plugin.js',
'./customManifest.plugin.js',
'./plugins/withInCallAudioModule.js',
'./plugins/withLiveActivities.js',
[
'./plugins/withLiveActivities.js',
{
appGroupId: Env.IOS_APP_GROUP,
},
],
['app-icon-badge', appIconBadgeConfig],
],
extra: {
Expand Down
9 changes: 9 additions & 0 deletions env.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const NAME = 'Resgrid Responder'; // app name
const EXPO_ACCOUNT_OWNER = 'resgrid'; // expo account owner
const EAS_PROJECT_ID = '026d4a74-f01d-41db-ae57-67f8c65a5f79'; // eas project id
const SCHEME = 'ResgridRespond'; // app scheme
const IOS_APP_GROUP_SHARED = 'group.com.wavetech.resgrid.shared';

/**
* We declare a function withEnvSuffix that will add a suffix to the variable name based on the APP_ENV
Expand All @@ -52,6 +53,10 @@ const withEnvSuffix = (name) => {
return APP_ENV === 'production' || APP_ENV === 'internal' ? name : `${name}.${APP_ENV}`;
};

const getIosAppGroup = () => {
return APP_ENV === 'production' || APP_ENV === 'internal' ? IOS_APP_GROUP_SHARED : `group.${withEnvSuffix(BUNDLE_ID)}`;
};

/**
* 2nd part: Define your env variables schema
* we use zod to define our env variables schema
Expand Down Expand Up @@ -100,6 +105,8 @@ const client = z.object({
const buildTime = z.object({
EXPO_ACCOUNT_OWNER: z.string(),
EAS_PROJECT_ID: z.string(),
IOS_APP_GROUP: z.string(),
IOS_APPLE_TEAM_ID: z.string().optional(),
// ADD YOUR BUILD TIME ENV VARS HERE
});

Expand Down Expand Up @@ -139,6 +146,8 @@ const _clientEnv = {
const _buildTimeEnv = {
EXPO_ACCOUNT_OWNER,
EAS_PROJECT_ID,
IOS_APP_GROUP: getIosAppGroup(),
IOS_APPLE_TEAM_ID: process.env.IOS_APPLE_TEAM_ID || process.env.EXPO_APPLE_TEAM_ID || process.env.APPLE_TEAM_ID,
// ADD YOUR ENV VARS HERE TOO
};

Expand Down
84 changes: 68 additions & 16 deletions plugins/withLiveActivities.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,46 @@ function entitlementsXml(appGroupId) {
`;
}

function resolveAppGroupId(cfg, appGroupId) {
if (appGroupId) {
return appGroupId;
}

const bundleId = cfg.ios?.bundleIdentifier ?? 'com.example.app';
return `group.${bundleId}`;
}

function trimDoubleQuotes(value) {
return value.replace(/^"(.*)"$/, '$1');
}

function ensureDoubleQuotes(value) {
return value.startsWith('"') ? value : `"${value}"`;
}

function getTargetBuildConfigurations(project, target) {
const configurationListId = target?.target?.buildConfigurationList ?? target?.pbxNativeTarget?.buildConfigurationList;
if (!configurationListId) {
return [];
}

return IOSConfig.XcodeUtils.getBuildConfigurationsForListId(project, configurationListId);
}

function getBuildSettingValue(buildConfigurations, key) {
for (const [, buildConfiguration] of buildConfigurations) {
const value = buildConfiguration?.buildSettings?.[key];
if (typeof value === 'string' || typeof value === 'number') {
const normalizedValue = trimDoubleQuotes(String(value));
if (normalizedValue) {
return normalizedValue;
}
}
}

return null;
}

/**
* Returns the Info.plist XML for the widget extension target.
*/
Expand Down Expand Up @@ -92,7 +132,7 @@ const withLiveActivitiesInfoPlist = (config) => {
* WidgetKit extension sandbox. CallCheckInAttributes.swift is duplicated
* so that both the extension and the main app each have the type in scope.
*/
const withLiveActivitiesFiles = (config) => {
const withLiveActivitiesFiles = (config, appGroupId) => {
return withDangerousMod(config, [
'ios',
async (cfg) => {
Expand All @@ -101,8 +141,7 @@ const withLiveActivitiesFiles = (config) => {
const widgetDir = path.join(iosRoot, WIDGET_EXTENSION_NAME);
const appName = IOSConfig.XcodeUtils.getHackyProjectName(projectRoot, cfg) || 'ResgridResponder';
const appDir = path.join(iosRoot, appName);
const bundleId = cfg.ios?.bundleIdentifier ?? 'com.example.app';
const appGroupId = `group.${bundleId}`;
const resolvedAppGroupId = resolveAppGroupId(cfg, appGroupId);

// ── 1. Widget extension directory ──────────────────────────────────────
if (!fs.existsSync(widgetDir)) {
Expand Down Expand Up @@ -138,7 +177,7 @@ struct CheckInTimerWidgetBundle: WidgetBundle {
fs.writeFileSync(path.join(widgetDir, 'Info.plist'), widgetInfoPlistXml());

// ── 5. Widget extension entitlements (App Group) ───────────────────────
fs.writeFileSync(path.join(widgetDir, `${WIDGET_EXTENSION_NAME}.entitlements`), entitlementsXml(appGroupId));
fs.writeFileSync(path.join(widgetDir, `${WIDGET_EXTENSION_NAME}.entitlements`), entitlementsXml(resolvedAppGroupId));

// ── 6. LiveActivityModule.swift → main app dir ─────────────────────────
// This file imports React and uses RCTPromiseResolveBlock; it must be
Expand Down Expand Up @@ -173,13 +212,12 @@ struct CheckInTimerWidgetBundle: WidgetBundle {
* This uses the managed withEntitlementsPlist modifier so that the change
* is written back through Expo's plist serialiser (safe, idempotent).
*/
const withLiveActivitiesAppEntitlements = (config) => {
const withLiveActivitiesAppEntitlements = (config, appGroupId) => {
return withEntitlementsPlist(config, (cfg) => {
const bundleId = cfg.ios?.bundleIdentifier ?? 'com.example.app';
const appGroupId = `group.${bundleId}`;
const resolvedAppGroupId = resolveAppGroupId(cfg, appGroupId);
const existing = cfg.modResults['com.apple.security.application-groups'];
if (!Array.isArray(existing) || !existing.includes(appGroupId)) {
cfg.modResults['com.apple.security.application-groups'] = [...(Array.isArray(existing) ? existing : []), appGroupId];
if (!Array.isArray(existing) || !existing.includes(resolvedAppGroupId)) {
cfg.modResults['com.apple.security.application-groups'] = [...(Array.isArray(existing) ? existing : []), resolvedAppGroupId];
}
return cfg;
});
Expand All @@ -205,6 +243,12 @@ const withLiveActivitiesXcodeProject = (config) => {
const widgetBundleId = `${bundleId}.${WIDGET_EXTENSION_NAME}`;
const deploymentTarget = '16.2';
const projectName = IOSConfig.XcodeUtils.getProductName(xcodeProject) || 'ResgridResponder';
const appTarget = xcodeProject.getTarget('com.apple.product-type.application');
const appBuildConfigurations = appTarget ? getTargetBuildConfigurations(xcodeProject, appTarget) : [];
const developmentTeam = cfg.ios?.appleTeamId ?? getBuildSettingValue(appBuildConfigurations, 'DEVELOPMENT_TEAM');
const currentProjectVersion = getBuildSettingValue(appBuildConfigurations, 'CURRENT_PROJECT_VERSION') ?? '1';
const marketingVersion = getBuildSettingValue(appBuildConfigurations, 'MARKETING_VERSION') ?? '1.0';
const targetedDeviceFamily = getBuildSettingValue(appBuildConfigurations, 'TARGETED_DEVICE_FAMILY') ?? '1,2';

// ── Idempotency guard ────────────────────────────────────────────────────
// addTarget() stores target names with surrounding quotes in the comment
Expand Down Expand Up @@ -244,7 +288,6 @@ const withLiveActivitiesXcodeProject = (config) => {

// ── 5. Add LiveActivityModule + shared attributes to the main app target ──
// These files have React imports and must be compiled in the app target.
const appTarget = xcodeProject.getTarget('com.apple.product-type.application');
if (appTarget) {
for (const filename of APP_SWIFT_FILES) {
IOSConfig.XcodeUtils.addBuildSourceFileToGroup({
Expand Down Expand Up @@ -288,13 +331,22 @@ const withLiveActivitiesXcodeProject = (config) => {
// Minimum deployment target required for Live Activities
s.IPHONEOS_DEPLOYMENT_TARGET = deploymentTarget;
// Mirror the main app's device family and versioning
s.TARGETED_DEVICE_FAMILY = '"1,2"';
s.CURRENT_PROJECT_VERSION = 1;
s.MARKETING_VERSION = '1.0';
s.TARGETED_DEVICE_FAMILY = ensureDoubleQuotes(targetedDeviceFamily);
s.CURRENT_PROJECT_VERSION = currentProjectVersion;
s.MARKETING_VERSION = marketingVersion;
s.SKIP_INSTALL = 'YES';
if (developmentTeam) {
s.DEVELOPMENT_TEAM = developmentTeam;
s.CODE_SIGN_STYLE = 'Automatic';
}
}
}

if (developmentTeam) {
xcodeProject.addTargetAttribute('DevelopmentTeam', ensureDoubleQuotes(developmentTeam), target);
xcodeProject.addTargetAttribute('ProvisioningStyle', 'Automatic', target);
}

return cfg;
});
};
Expand All @@ -306,10 +358,10 @@ const withLiveActivitiesXcodeProject = (config) => {
* 3. Entitlements for the main app (withEntitlementsPlist)
* 4. Xcode project registration last (needs the files to already exist)
*/
module.exports = (config) => {
module.exports = (config, { appGroupId } = {}) => {
config = withLiveActivitiesInfoPlist(config);
config = withLiveActivitiesFiles(config);
config = withLiveActivitiesAppEntitlements(config);
config = withLiveActivitiesFiles(config, appGroupId);
config = withLiveActivitiesAppEntitlements(config, appGroupId);
config = withLiveActivitiesXcodeProject(config);
return config;
};
Loading