I’ve created a workaround for the current version of the Intercom SDK with a custom Expo plugin that creates shim files, such that correct C++ files can be found during compiling. THis ensures constant building success in Expo apps.
The issue has to do with build step trying to compile C++ headers through Swift. This plugin removes the initialization of C++ headers in Swift, and adds a shim c++ file that has no other functionality than pointing towards correct headers during compilation.
Would still be nice to see a more permanent fix in the SDK. :)
// withIntercomFix.ts
// Minimal, surgical fix for Intercom + RN codegen headers that require Obj-C++.
//
// What this plugin does:
// 1) Cleans AppDelegate.swift of the generated "import intercom_react_native" code.
// 2) Adds a Obj-C++ (.mm) compilation unit that includes the Intercom RN spec header,
// so Xcode compiles those C++-requiring headers in an Obj-C++ translation unit.
//
import {
ConfigPlugin,
ExportedConfigWithProps,
withAppDelegate,
withDangerousMod,
withXcodeProject,
} from "@expo/config-plugins";
import * as fs from "fs";
import * as path from "path";
const GROUP_NAME = "IntercomShim";
const SHIM_H = "IntercomShim.h";
const SHIM_MM = "IntercomShim.mm";
const APPDELEGATE_BLOCK_BEGIN =
"// @generated begin Intercom header - expo prebuild";
const APPDELEGATE_BLOCK_END = "// @generated end Intercom header";
// A tiny header/impl whose mere compilation under .mm satisfies the Obj-C++ requirement.
const SHIM_H_CONTENT = `#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface IntercomShim : NSObject
+ (void)prepare;
@end
NS_ASSUME_NONNULL_END
`;
const SHIM_MM_CONTENT = `#import "${SHIM_H}"
// Include the RN Intercom spec *only* from Obj-C++ (.mm), never from Swift or .m.
#import <IntercomReactNativeSpec/IntercomReactNativeSpec.h>
@implementation IntercomShim
+ (void)prepare {
// No-op. Compiling this TU ensures the generated header is built as Obj-C++.
}
@end
`;
// Clean AppDelegate.swift from native Intercom code.
const withCleanAppDelegate: ConfigPlugin = (config) => {
return withAppDelegate(config, (cfg) => {
if (cfg.modResults.language !== "swift") return cfg;
let src = cfg.modResults.contents;
// Remove the generated Intercom import block entirely
const blockRe = new RegExp(
`${APPDELEGATE_BLOCK_BEGIN}[\\s\\S]*?${APPDELEGATE_BLOCK_END}`,
"g",
);
if (blockRe.test(src)) {
src = src.replace(
blockRe,
`${APPDELEGATE_BLOCK_BEGIN}\n// (removed by with-intercom-fix)\n${APPDELEGATE_BLOCK_END}`,
);
}
// Remove any stray "import intercom_react_native" lines
src = src.replace(
/^\s*import\s+intercom_react_native\s*$/gm,
"// (removed) intercom_react_native",
);
// Remove the generated Swift initializer line: IntercomModule.initialize("..", withAppId: "..")
src = src.replace(
/^\s*IntercomModule\.initialize\([^)]*\)\s*$/gm,
"// (removed) Intercom init",
);
cfg.modResults.contents = src;
return cfg;
});
};
// Ensure path stays inside iosRoot; throws on traversal.
function safeJoin(iosRoot: string, ...segments: string[]) {
const base = path.resolve(iosRoot);
const target = path.resolve(iosRoot, ...segments);
if (!target.startsWith(base + path.sep) && target !== base) {
throw new Error(`Blocked path traversal: ${target}`);
}
return target;
}
// Write shim files to ios/PROJECT/IntercomShim
const withShimFiles: ConfigPlugin = (config) => {
return withDangerousMod(config, [
"ios",
(cfg: ExportedConfigWithProps) => {
const iosRoot = cfg.modRequest.platformProjectRoot;
const shimDir = safeJoin(iosRoot, GROUP_NAME);
const hPath = safeJoin(iosRoot, GROUP_NAME, SHIM_H);
const mmPath = safeJoin(iosRoot, GROUP_NAME, SHIM_MM);
// Create folder if missing
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path validated by safeJoin
if (!fs.existsSync(shimDir)) {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path validated by safeJoin
fs.mkdirSync(shimDir, { recursive: true });
}
// Only write if missing (idempotent on re-runs)
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path validated by safeJoin
if (!fs.existsSync(hPath)) {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path validated by safeJoin
fs.writeFileSync(hPath, SHIM_H_CONTENT, "utf8");
}
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path validated by safeJoin
if (!fs.existsSync(mmPath)) {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path validated by safeJoin
fs.writeFileSync(
mmPath,
SHIM_MM_CONTENT.replace(/SHIM_H/g, SHIM_H),
"utf8",
);
}
return cfg;
},
]);
};
// Adds shim files to the Xcode project (group + Sources)
const withAddToXcode: ConfigPlugin = (config) => {
return withXcodeProject(config, (cfg) => {
const project = cfg.modResults;
// Ensure (or create) a group to hold the shim files.
const group =
project.pbxGroupByName(GROUP_NAME) ??
project.addPbxGroup([], GROUP_NAME, GROUP_NAME);
const groupUUID = group.uuid;
function addFileOnce(relPath: string) {
if (!project.hasFile(relPath)) {
project.addSourceFile(
relPath,
{ target: project.getFirstTarget().uuid },
groupUUID,
);
}
}
// Add header to group (not compiled) and .mm to Sources (compiled as Obj-C++).
const headerRel = path.join(GROUP_NAME, SHIM_H);
const implRel = path.join(GROUP_NAME, SHIM_MM);
if (!project.hasFile(headerRel)) {
project.addFile(headerRel, groupUUID);
}
addFileOnce(implRel);
return cfg;
});
};
// Export plugin
const withIntercomFix: ConfigPlugin = (config) => {
config = withCleanAppDelegate(config);
config = withShimFiles(config);
config = withAddToXcode(config);
return config;
};
export default withIntercomFix;
And in app.config: