Skip to content

Instantly share code, notes, and snippets.

@nixolas1
Created July 26, 2023 08:29
Show Gist options
  • Save nixolas1/62f5ce8473224cc8437211e787489b1d to your computer and use it in GitHub Desktop.
Save nixolas1/62f5ce8473224cc8437211e787489b1d to your computer and use it in GitHub Desktop.
React Native Carplay (2.1.0) plugin for Expo 47 (Dangerous modifications)
const {createRunOncePlugin, IOSConfig, withDangerousMod} = require("@expo/config-plugins")
const fs = require("fs/promises")
const withCarPlay = (config) => {
config = withCarPlayAppDelegate(config)
config = withCarPlayAppDelegateHeader(config)
return config
}
const withCarPlayAppDelegate = (conf) => {
return withDangerousMod(conf, [
"ios",
async (config) => {
const fileInfo = IOSConfig.Paths.getAppDelegate(config.modRequest.projectRoot)
let contents = await fs.readFile(fileInfo.path, "utf-8")
if (fileInfo.language === "objcpp" || fileInfo.language === "objc") {
contents = modifySourceFile(contents)
} else {
throw new Error(`Cannot add CarPlay code to AppDelegate of language "${fileInfo.language}"`)
}
await fs.writeFile(fileInfo.path, contents)
return config
},
])
}
const withCarPlayAppDelegateHeader = (conf) => {
return withDangerousMod(conf, [
"ios",
async (config) => {
const headerFilePath = IOSConfig.Paths.getAppDelegateHeaderFilePath(config.modRequest.projectRoot)
let contents = await fs.readFile(headerFilePath, "utf-8")
contents = modifyHeaderFile(contents)
await fs.writeFile(headerFilePath, contents)
return config
},
])
}
// @interface AppDelegate : EXAppDelegateWrapper <RCTBridgeDelegate>
const newHeaders = `
@property (nonatomic, strong) RCTBridge* bridge;
@end
@interface TemplateSceneDelegate: NSObject <CPTemplateApplicationSceneDelegate>
@end
@interface WindowSceneDelegate: EXAppDelegateWrapper <UIWindowSceneDelegate>
@end
`
const originalSceneRegex = /RCTBridge \*bridge =[\s\S]+?return YES;/ // todo: new arch
const originalSceneReplacement = `
// Carplay removed stuff above, added below
self.bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
self.window = nil; // Carplay added
[super application:application didFinishLaunchingWithOptions:launchOptions];
return YES;
`
const sceneDelegateRegex = /@end[\s\n]+?$/
const sceneDelegateReplacement = `
@end
// Start carplay
#import <RNCarPlay.h>
@implementation TemplateSceneDelegate
- (void)templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene didConnectInterfaceController:(CPInterfaceController *)interfaceController toWindow:(CPWindow *)window {
[RNCarPlay connectWithInterfaceController:interfaceController window:window scene:templateApplicationScene];
}
- (void)templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene didConnectInterfaceController:(CPInterfaceController *)interfaceController {
[RNCarPlay connectWithInterfaceController:interfaceController window:templateApplicationScene.carWindow scene:templateApplicationScene];
}
- (void)templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene didDisconnectInterfaceController:(CPInterfaceController *)interfaceController {
[RNCarPlay disconnect];
}
@end
@implementation WindowSceneDelegate
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
[RCTLinkingManager application:[UIApplication sharedApplication] openURL:userActivity.webpageURL options:@{}];
}
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
AppDelegate* appDelegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
UIWindowScene* windowScene = (UIWindowScene*)scene;
for (NSUserActivity* activity in connectionOptions.userActivities) {
NSURL* url = activity.webpageURL;
if (url != nil) {
// Hack: Open URL after everything is initialized, after booting for 1.5s
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[RCTLinkingManager application:[UIApplication sharedApplication] openURL:url options:@{}];
});
}
}
if ([[RNCPStore sharedManager] isConnected]) {
[appDelegate.bridge invalidate];
appDelegate.bridge = [[RCTBridge alloc] initWithDelegate:appDelegate launchOptions:@{}];
}
UIView *rootView = [self.reactDelegate createRootViewWithBridge:appDelegate.bridge moduleName:@"main" initialProperties:nil]; // updated
rootView.backgroundColor = [UIColor whiteColor];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[UIApplication sharedApplication].delegate.window = self.window;
self.window.windowScene = windowScene;
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
[super application:[UIApplication sharedApplication] didFinishLaunchingWithOptions:nil];
}
-(void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
UIOpenURLContext* context = [URLContexts anyObject];
if (context != nil) {
[RCTLinkingManager application:[UIApplication sharedApplication] openURL:context.URL options:@{}];
}
}
@end
`
const modifyHeaderFile = (contents) => {
contents = "#import <CarPlay/CarPlay.h>\n" + contents // add import carplay
contents = contents.replace(/@end/, newHeaders) // add bridge and scene delegate
return contents
}
const modifySourceFile = (contents) => {
contents = contents.replace(originalSceneRegex, originalSceneReplacement)
contents = contents.replace(sceneDelegateRegex, sceneDelegateReplacement)
return contents
}
const withCarPlayPlugin = (config) => {
config = withCarPlay(config)
// Return the modified config.
return config
}
const pkg = {
// Prevent this plugin from being run more than once.
name: "@valtyr/react-native-carplay",
// Indicates that this plugin is dangerously linked to a module,
// and might not work with the latest version of that module.
version: "UNVERSIONED",
}
module.exports = createRunOncePlugin(withCarPlayPlugin, pkg.name, pkg.version)
@janwiebe-jump
Copy link

janwiebe-jump commented Nov 24, 2023

Hi @nixolas1
have you got this working with Expo 49? In SDK 48 or 49 the EXAppDelegateWrapper has changed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment