Intercom React Native does not compile for Expo during build when combined with other packages. | Community
Skip to main content
Answered

Intercom React Native does not compile for Expo during build when combined with other packages.

  • October 16, 2025
  • 3 replies
  • 123 views

Since the upgrade to 9.*.* of version for React Native Intercom (https://github.com/intercom/intercom-react-native), we can no longer build and compile our Expo React Native app for iOS.

Somehow the Intercom upgrade introduces a failure where C++ files are not correctly mapped during compiling. This is only when combined with other packages (in our case UserCentrics for RN).

It is important to highlight that although other third party apps are involved, Intercom’s upgrade breaks the building process. Compiling fails with:

    13 |
14 | #ifndef __cplusplus
> 15 | #error This file must be compiled as Obj-C++. If you are importing it, you must change your file extension to .mm.
| ^ This file must be compiled as Obj-C++. If you are importing it, you must change your file extension to .mm.
16 | #endif
17 |
18 | // Avoid multiple includes of IntercomReactNativeSpec symbols

and

'optional' file not found
'tuple' file not found
'utility' file not found



I’ve added a minimal reproducible example in the following Repo: https://github.com/damiaanh/intercomExample . This repo contains a clean expo project with above two packages.

Note that this behaviour is for Expo apps that do not have the new RN Architecture enabled. Intercom should support this.

 

Hoping to find out more and to create a fix for this issue. Please reach out and engage with this question when you (I) also have this issue or (II) know more.

Best answer by Damiaan Houtschild

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:

 

3 replies

  • New Participant
  • 1 reply
  • October 28, 2025

Hey Intercom team, is there an update on this issue?


Forum|alt.badge.img+4
  • Intercom Team
  • 31 replies
  • October 28, 2025

Hey ​@Damiaan Houtschild  and ​@Vada Reynolds ! Sean from Customer Support Engineering here 🔧
It sounds like the feature is not working as intended here. We may need to have our team dig into this.

I have forwarded your issue to the Customer Support team on your behalf using the forum ticket escalation feature, the team will get back to you as soon as they can. Thank you! 


  • Author
  • New Participant
  • 1 reply
  • Answer
  • October 31, 2025

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: