-
Notifications
You must be signed in to change notification settings - Fork 111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make changes required to support Expo config plugin #101
Comments
UpdateI cloned the project into a monorepo alongside a demo expo app and swapped out the react-native-carplay dependency from npm for my local version. By renaming all variables and method parameters previously called
This contradicts what is stated in this repo's readme. Could it be that this has changed since the package was authored? Or do I still have a ways to go with my integration? Here's a link to my demo repo if anyone's interested: https://github.com/valtyr/expo-carplay-demo/tree/main/apps/carplay-test |
@valtyr did you make any progress on this? I'm looking to do the same! |
I'm also very interested in this |
The template variable name has been resolved, so older configuration should work. But there is no way this is going to work going forward, unless Expo starts supporting custom AppScenes, we will see. |
I don't see any references to AppScenes in Expo code. I understand the basic idea behind UI Application Delegate (being able to control a mobile app from the car screen running CarPlay), even though I'm not an iOS developer by definition. I'm trying to understand whether this is a feature that we can request / contribute to Expo, as I'm also interested in it, but not being a subject matter expert, I would like to gather some requirements before opening a feature request / RFC with them. What would it take to interface this library usage of AppScenes to Expo build/runtime environment? When you say custom, is there a default implementation of this feature in their framework? Would it require tweaking or actual enablement? Thank you, Angelo. |
Expo has a custom app delegate called So, switching to AppScene is a hefty rewrite of how the Expo framework is initiated. So to put it into words: It's not about "supporting" app scenes in Expo, its about switching technology altogether, and you will lose legacy appdelegate support by doing so. |
Would it be possible to get this working by injecting From testing, this is able to build successfully - however the Car play delegate functions can be set up in the same scene delegate file as an extension. Expo is already looking to fix the issue of not being able to find the key window and can be tracked on this thread: expo/expo#23536 |
Looks like that issue has been resolved...someone could give this a try now... |
@caustin24345 Have you made any progress? Can you share your code that builds? Thanks! |
@janwiebe-jump I believe expo has addressed this issue. A PR was merged to their main branch on 22nd Sept with the fix I mentioned to allow for multiple scenes - expo/expo#24565. Im not sure how often expo releases, but you could always test pointing your expo version to the main branch to see if you can get CarPlay up and running. My guess is that this will be officially supported by expo for bare and managed workflows in their next release. |
Tnanks @caustin24345 I got the new expo-dev-client version, and the error has been fixed. However, the carplay app crashes on launch. I don't have a scene delegate, I am using the updated AppDelegate of this issue. I've also tried to change the method declarations to this:
Since that seems to match the protocol |
@janwiebe-jump Were you able to fix this? |
Actually yes. I have created my own expo plugin, to use with react-native-carplay. I don't have the time right now to create a github repo, but here are my files: carplay.zip Be sure to set the correct entitlements as well. The config plugin adds the carplay-audio entitlement. |
Thanks for your work. Is the CarPlay app (without phone app launched) working for you ? |
@janwiebe-jump Thanks for sharing that zip file. I have added the contents to a
Is there anything else I need to do? Is there a missing compile step for the plugin or something? |
@nzhenry I haven't tried expo run:ios. |
did you try to use ./plugins/carplay/withCarPlay.ts" instead?
…On Wed, Feb 28, 2024 at 1:25 AM Henry Johnson ***@***.***> wrote:
@janwiebe-jump <https://github.com/janwiebe-jump> Thanks for sharing that
zip file. I have added the contents to a plugins/carplay directory, added
the entry "./plugins/carplay/withCarPlay" to the plugins array in
app.config.ts, and set the correct entitlements, but when I run expo
run:ios I get this error:
PluginError: Failed to resolve plugin for module "./plugins/carplay/withCarPlay" relative to "{project_root}/ios/Pods/../.."
Is there anything else I need to do? Is there a missing compile step for
the plugin or something?
—
Reply to this email directly, view it on GitHub
<#101 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAOANE4IMHRDLIPOHDYH7TYVZ2OBAVCNFSM555XHRVKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOJWG44TMMBXGA2A>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
I'm having trouble getting the withCarPlay.ts working as a plugin. I put it in a separate directory in the root of my project
I get back an error "CommandError: Cannot use import statement outside a module". I've been trying to find some straight forward docs on the plugins within expo, but they are a bit over my head at this point. Has anyone gotten this to work properly and could share their steps? Thanks, |
@markmccoid I use the Ignite boilerplate and its app.config.ts makes it possible to use typescript.
|
Thanks @janwiebe-jump, this was really helpful! I managed to get it working. However, after upgrading to Expo v51, I get errors in my AppDelegate on Has anyone managed to get this working on the latest expo? |
@tommynordli I see the same. It looks like bridgeless support has been introduced in Expo PR 27601, and support for the bridge has been removed. I did not find a solution yet. Maybe @DanielKuhn has ideas, because config plugin was based on his work as well. |
Sorry, I'm not using expo... But it looks like you need to adjust your AppDelegate code to the new Expo |
Hi, I'm also trying to get things to work in Expo 51. Did some of you find a solution yet? |
I'm also trying to get things to work in Expo 51, but I haven't succeeded yet @casperolesen. |
Hi @janwiebe-jump ! Your code is working correctly, but it's breaking the Linking API. I'm unable to open the app using a scheme like this: myapp://help I have tried numerous solutions, but I haven't been successful in resolving this issue. |
I got my app up and running using expo 51. I made some changes to the plugin from @janwiebe-jump You can see my changes here casperolesen/carplay-plugin@80a5fb3 |
Thank you @casperolesen, your code works well on Expo 51. When trying to open a specific page: This does not work, I still haven't managed to fix this issue. |
I'm not using expo, but the entry point for linking in iOS is the SceneDelegate's
|
Thank you @DanielKuhn 🎉 I've managed to get Linking to work when the app is in background. #import "SceneDelegate.h"
#import "AppDelegate.h"
#import <EXSplashScreen/EXSplashScreenService.h>
@implementation SceneDelegate
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions
{
if ([scene isKindOfClass:[UIWindowScene class]])
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
BOOL hasCreatedBridge = [appDelegate initAppFromScene:connectionOptions];
// Create rootViewController
UIViewController * rootViewController = appDelegate.createRootViewController;
[appDelegate setRootView:appDelegate.rootView toRootViewController:rootViewController];
UIWindow* window = [[UIWindow alloc] initWithWindowScene: scene];
window.rootViewController = rootViewController;
self.window = window;
appDelegate.window = window;
[self.window makeKeyAndVisible];
EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
[appDelegate finishedLaunchingWithOptions:connectionOptions];
if(!hasCreatedBridge) {
[splashScreenService hideSplashScreenFor:rootViewController options:EXSplashScreenDefault successCallback:^(BOOL hasEffect){}
failureCallback:^(NSString * _Nonnull message) {
EXLogWarn(@"Hiding splash screen from root view controller did not succeed: %@", message);
}];
}
}
}
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[appDelegate application:[UIApplication sharedApplication] continueUserActivity:userActivity restorationHandler:^(NSArray<id<UIUserActivityRestoring>> * _Nullable restorableObjects) {
// Handle restoration here if needed
}];
}
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
UIOpenURLContext *context = [URLContexts anyObject];
if (context) {
NSURL *url = context.URL;
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[appDelegate application:[UIApplication sharedApplication] openURL:url options:@{}];
}
}
@end My AppDelegate looks like this : #import <Expo/Expo.h>
#import <ExpoModulesCore/EXReactDelegateWrapper+Private.h>
#import <ExpoModulesCore/Swift.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import "RCTAppSetupUtils.h"
#import "AppDelegate.h"
#import <Firebase/Firebase.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>
@interface RCTAppDelegate () <RCTTurboModuleManagerDelegate>
@end
@interface AppDelegate()
@property (nonatomic, strong) EXReactDelegateWrapper *reactDelegate;
@end
@implementation AppDelegate {
EXExpoAppDelegate *_expoAppDelegate;
}
// Synthesize window, so the AppDelegate can synthesize it too.
@synthesize window = _window;
- (instancetype)init
{
if (self = [super init]) {
_expoAppDelegate = [[EXExpoAppDelegate alloc] init];
_reactDelegate = [[EXReactDelegateWrapper alloc] initWithExpoReactDelegate:_expoAppDelegate.reactDelegate];
}
return self;
}
// This needs to be implemented, otherwise forwarding won't be called.
// When the app starts, `UIApplication` uses it to check beforehand
// which `UIApplicationDelegate` selectors are implemented.
- (BOOL)respondsToSelector:(SEL)selector
{
return [super respondsToSelector:selector]
|| [_expoAppDelegate respondsToSelector:selector];
}
// Forwards all invocations to `ExpoAppDelegate` object.
- (id)forwardingTargetForSelector:(SEL)selector
{
return _expoAppDelegate;
}
- (UIViewController *)createRootViewController
{
return [self.reactDelegate createRootViewController];
}
- (RCTRootViewFactory *)createRCTRootViewFactory
{
RCTRootViewFactoryConfiguration *configuration =
[[RCTRootViewFactoryConfiguration alloc] initWithBundleURL:self.bundleURL
newArchEnabled:self.fabricEnabled
turboModuleEnabled:self.turboModuleEnabled
bridgelessEnabled:self.bridgelessEnabled];
__weak __typeof(self) weakSelf = self;
configuration.createRootViewWithBridge = ^UIView *(RCTBridge *bridge, NSString *moduleName, NSDictionary *initProps)
{
return [weakSelf createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
};
configuration.createBridgeWithDelegate = ^RCTBridge *(id<RCTBridgeDelegate> delegate, NSDictionary *launchOptions)
{
return [weakSelf createBridgeWithDelegate:delegate launchOptions:launchOptions];
};
return [[EXReactRootViewFactory alloc] initWithReactDelegate:self.reactDelegate configuration:configuration turboModuleManagerDelegate:self];
}
- (void)finishedLaunchingWithOptions:(UISceneConnectionOptions *)connectionOptions
{
[_expoAppDelegate application:[UIApplication sharedApplication] didFinishLaunchingWithOptions:[self connectionOptionsToLaunchOptions:connectionOptions]];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
[FIRApp configure];
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
self.moduleName = @"main";
// You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native.
self.initialProps = @{};
return YES;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
}
// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
- (BOOL)initAppFromScene:(UISceneConnectionOptions *)connectionOptions {
// If bridge has already been initiated by another scene, there's nothing to do here
if (self.bridge != nil) {
return NO;
}
if (self.bridge == nil) {
RCTAppSetupPrepareApp([UIApplication sharedApplication], self.turboModuleEnabled);
self.rootViewFactory = [self createRCTRootViewFactory];
}
NSDictionary * initProps = [self prepareInitialProps];
self.rootView = [self.rootViewFactory viewWithModuleName:self.moduleName initialProperties:initProps launchOptions:[self connectionOptionsToLaunchOptions:connectionOptions]];
self.rootView.backgroundColor = [UIColor blackColor];
return YES;
}
- (NSDictionary<NSString *, id> *)prepareInitialProps {
NSMutableDictionary<NSString *, id> *initProps = [self.initialProps mutableCopy] ?: [NSMutableDictionary dictionary];
#if RCT_NEW_ARCH_ENABLED
initProps[@"kRNConcurrentRoot"] = [self concurrentRootEnabled];
#endif
return [initProps copy];
}
- (NSDictionary<UIApplicationLaunchOptionsKey, id> *)connectionOptionsToLaunchOptions:(UISceneConnectionOptions *)connectionOptions {
NSMutableDictionary<UIApplicationLaunchOptionsKey, id> *launchOptions = [NSMutableDictionary dictionary];
if (connectionOptions) {
if (connectionOptions.notificationResponse) {
launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] = connectionOptions.notificationResponse.notification.request.content.userInfo;
}
if ([connectionOptions.userActivities count] > 0) {
NSUserActivity* userActivity = [connectionOptions.userActivities anyObject];
NSDictionary *userActivityDictionary = @{
@"UIApplicationLaunchOptionsUserActivityTypeKey": [userActivity activityType] ? [userActivity activityType] : [NSNull null],
@"UIApplicationLaunchOptionsUserActivityKey": userActivity ? userActivity : [NSNull null]
};
launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey] = userActivityDictionary;
}
}
return launchOptions;
}
@end |
When the app is cold-starting from a link, you need to pick the url from the
|
Kudos to @alex-vasylchenko btw - I learned all this linking stuff from him :) |
That's awesome. But when I tried to integrate into my own expo project, i got error that ExpoModulesCore/EXReactDelegateWrapper+Private.h and React_RCTAppDelegate/RCTAppDelegate.h when trying to build the app to simulator. Do you have any ideas to fix this? |
I was able to fix those same errors. I had to change inside
with
But now the app starts with a black screen and the react native code doesn't start (no loading from the dev server, nothing). Any idea? |
Sorry, forgot that... I also had to define my own continuation interface for the object by copying the private header file to the top of @interface EXReactDelegateWrapper(Private)
- (instancetype)initWithExpoReactDelegate:(EXReactDelegate *)expoReactDelegate;
@end Also, I had some issues where the - xcodeProjectName = config.name;
+ withDangerousMod(config, [
+ 'ios',
+ (expConfig) => {
+ xcodeProjectName = IOSConfig.XcodeUtils.getProjectName(
+ expConfig.modRequest.projectRoot,
+ );
+ return expConfig;
+ },
+ ]); This should work with all kind of project names. |
Wow it works like a charm :D And for black screen, i added |
@casperolesen, @thomas-rx I'm wondering if these changes also allow for Headless mode for RN CarPlay. I can see that with these adjustments, CarPlay is loading and rendering while the RN app is in the foreground or background, however, if the app IS NOT launched on the mobile device, and IS launched through the CarPlay interface - it will just show a blank screen on CarPlay. My understanding is that RNCarPlay requires the mobile app to be initialised in order to populate UI - we rely on the RN code to push templates to CarPlay. In the instance where the mobile app has not been launched, there is no way that we can populate the UI with these templates. With Expo 49 - a bridge was created in the instance where the CarPlay scene was initialised before the mobile app scene (i.e when the mobile app was not launched, and the user launched the CarPlay app). This bridge was then used to initialise the mobile app by using it to create the root view. With these changes, it seems like the same thing is trying to be achieved by using the createRTCRootViewFactory method, and setting the rootView and bridge there. I'm wondering if it is necessary to enable the newArchitecture in the app config to allow for Headless mode to work correctly? I understand that with Expo 50 + the concept of the asynchronous bridge has been removed and replaced by the JSI to remove unnecessary serialising between the RN and native layers, however, is there any other way to force the mobile app to launch when the CarPlay scene is recognised. |
Hey everyone, I was wondering whether anyone has tested |
Our app is still using Expo 50, with RN 0.73 I am running into the following error: Caused by the import from AppDelegate.mm Not sure why, because I am using hermes. |
I have written the RCTAppSetupUtils functionality manually in my AppDelegate, since it seems to be caused by an issue in React Native < 0.76 This also implements booting the CarPlay app stand-alone, inspired by the work of @DanielKuhn in #158 |
@janwiebe-jump have you got it working in Expo 52? I can get it build with your fixes (thanks!), but i can't get it to work in standalone mode |
I'm looking at integrating react-native-carplay into an EAS-powered Expo app. For some background, Expo started supporting custom native code for managed projects recently, and the mechanism that allows this to work is something called an Expo Config Plugin. That should theoretically allow this plugin to be used in custom Expo dev clients (which is very exciting).
The setup
Here's a link to a plugin file I've created which performs all the modifications specified by the readme:
https://gist.github.com/valtyr/48eca6a1b5e3d54d865e2352a6127b6a
This file is transpiled into js and referenced in the
app.json
config:Here's my
eas.json
file for good measure (notice thesimulator: true
flag, this is convenient for testing on apps that haven't yet been granted the CarPlay capability):I then run a build using this command:
The build command then spits out details about the working directory near the top which lets us inspect the source files and make sure the correct modifications have been made:
The changes
Expo's app delegate file is a bit different to vanilla React Native app delegate files:
The issue
The build goes fine until it starts compiling the
RNCPStore.h
header file. It seems that the client is being built in an ObjectiveC++ environment instead of just ObjectiveC (this is my uneducated assumption) so the keywordtemplate
is reserved. Here's the error it spits out:My hunch is that this would all work, if another variable name was used.
It would be awesome if a maintainer could help me get this working. I would of course contribute my expo-config-plugin to the codebase. I think
react-native-carplay
and Expo could be an absolutely killer combo!The text was updated successfully, but these errors were encountered: