birkir / react-native-carplay

CarPlay with React Native
https://birkir.dev/react-native-carplay/
MIT License
629 stars 104 forks source link

Make changes required to support Expo config plugin #101

Open valtyr opened 1 year ago

valtyr commented 1 year ago

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:

{
  "expo": {
    // ...
    "plugins": ["./plugins/carplay.js"]
  }
}

Here's my eas.json file for good measure (notice the simulator: true flag, this is convenient for testing on apps that haven't yet been granted the CarPlay capability):

{
  "cli": {
    "version": ">= 0.57.0"
  },
  "build": {
    "simulator": {
      "ios": {
        "developmentClient": true,
        "simulator": true
      }
    },
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {}
  },
  "submit": {
    "production": {}
  }
}

I then run a build using this command:

eas build --local --platform=ios --profile=simulator

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:

[SETUP_WORKINGDIR] Preparing workingdir [PATH_HERE]

The changes

Expo's app delegate file is a bit different to vanilla React Native app delegate files:

- @interface AppDelegate : UIResponder <UIApplicationDelegate, CPApplicationDelegate>
+ @interface AppDelegate : EXAppDelegateWrapper <RCTBridgeDelegate, CPApplicationDelegate>

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 keyword template is reserved. Here's the error it spits out:

❌  (ios/Pods/Headers/Public/react-native-carplay/RNCPStore.h:14:71)

  12 | + (id)sharedManager;
  13 | - (CPTemplate*) findTemplateById: (NSString*)templateId;
> 14 | - (NSString*) setTemplate:(NSString*)templateId template:(CPTemplate*)template;
     |                                                                       ^ expected identifier; 'template' is a keyword in Objective-C++
  15 | - (CPTrip*) findTripById: (NSString*)tripId;
  16 | - (NSString*) setTrip:(NSString*)tripId trip:(CPTrip*)trip;
  17 | - (CPNavigationSession*) findNavigationSessionById:(NSString*)navigationSessionId;

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!

valtyr commented 1 year ago

Update

I 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 template in the Objective C source code I've managed to get the app to build through eas build. It seems like everything is working. At least I'm able to call the native methods from JS without anything crashing. The only problem I seem to have now is that the app doesn't appear on the simulator's CarPlay screen. My instinct is that the reason it doesn't work is simply that I don't have the CarPlay entitlement yet. This is confirmed by the documentation:

Simulator can’t display your CarPlay-enabled app unless it has the necessary entitlements.

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

AlexanderCollins commented 1 year ago

@valtyr did you make any progress on this? I'm looking to do the same!

forki commented 1 year ago

I'm also very interested in this

birkir commented 1 year ago

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.

angeloreale commented 10 months ago

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.

birkir commented 10 months ago

Expo has a custom app delegate called EXAppDelegateWrapper and that is the main entry point of their framework.

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.

caustin24345 commented 10 months ago

Would it be possible to get this working by injecting UIApplicationSceneManifest and all necessary children into the info.plist. This would allow for multiple scenes in the iOS application.

From testing, this is able to build successfully - however the ExAppDelegateWrapper is unable to find the key window once the application finishes launching. If its possible to get this delegate to find the application key window, I believe the scene delegate will connect to the session as well - apparently this was possible in Expo versions < 48.

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: https://github.com/expo/expo/issues/23536

GeorgeBellTMH commented 10 months ago

Looks like that issue has been resolved...someone could give this a try now...

janwiebe-jump commented 8 months ago

@caustin24345 Have you made any progress? Can you share your code that builds? Thanks!

caustin24345 commented 8 months ago

@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 - https://github.com/expo/expo/pull/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.

janwiebe-jump commented 8 months ago

Tnanks @caustin24345 I got the new expo-dev-client version, and the error has been fixed.

However, the carplay app crashes on launch. Application does not implement CarPlay template application lifecycle methods in its scene delegate.

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:

- (void)application:(UIApplication *)application didConnectCarInterfaceController:(CPInterfaceController *)interfaceController to:(CPWindow *)window {
    [RNCarPlay connectWithInterfaceController:interfaceController window:window];
  }

  - (void)application:(nonnull UIApplication *)application didDisconnectCarInterfaceController:(nonnull CPInterfaceController *)interfaceController from:(nonnull CPWindow *)window {
    [RNCarPlay disconnect];
  }

Since that seems to match the protocol

KestasVenslauskas commented 6 months ago

@janwiebe-jump Were you able to fix this?

janwiebe-jump commented 6 months ago

Actually yes. I have created my own expo plugin, to use with react-native-carplay. I also incorporated the proposal of #158 to be able to boot the CarPlay app without having the phone app running.

I don't have the time right now to create a github repo, but here are my files: carplay.zip I added a plugins\carplay folder and put the files in there. Added the withCarPlay plugin to my app.json.

Be sure to set the correct entitlements as well. The config plugin adds the carplay-audio entitlement.

thomas-rx commented 5 months ago

I also incorporated the proposal of #158 to be able to boo

Thanks for your work. Is the CarPlay app (without phone app launched) working for you ?

nzhenry commented 5 months ago

@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?

janwiebe-jump commented 5 months ago

@nzhenry I haven't tried expo run:ios. I use the eas cli, with the --local option. does that work for you?

forki commented 5 months ago

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 https://github.com/birkir/react-native-carplay/issues/101#issuecomment-1967960704, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAOANE4IMHRDLIPOHDYH7TYVZ2OBAVCNFSM555XHRVKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOJWG44TMMBXGA2A . You are receiving this because you are subscribed to this thread.Message ID: @.***>

markmccoid commented 4 months ago

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 ./plugins/carplay and then in app.config.json I've added:

export default {
  plugins: ["./plugins/carplay/withCarPlay.ts"],
};

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,

janwiebe-jump commented 4 months ago

@markmccoid I use the Ignite boilerplate and its app.config.ts makes it possible to use typescript. I import the carplay plugin in app.config.ts like this:

plugins: [
require("./plugins/carplay/withCarPlay").withCarPlay,
]
tommynordli commented 2 months ago

Actually yes. I have created my own expo plugin, to use with react-native-carplay. I also incorporated the proposal of #158 to be able to boot the CarPlay app without having the phone app running.

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 createBridgeWithDelegate and createRootViewWithBridge as they're no longer available on the EXReactDelegateWrapper.

Has anyone managed to get this working on the latest expo?

Screenshot 2024-05-22 at 14 27 46
janwiebe-jump commented 2 months ago

@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.

DanielKuhn commented 2 months ago

Sorry, I'm not using expo... But it looks like you need to adjust your AppDelegate code to the new Expo EXAppDelegateWrapper-code - just like I need to adjust mine to the new RCTAppDelegate-code on every react native upgrade.

casperolesen commented 1 month ago

Hi,

I'm also trying to get things to work in Expo 51.

Did some of you find a solution yet?

thomas-rx commented 1 month ago

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.

thomas-rx commented 1 month ago

Actually yes. I have created my own expo plugin, to use with react-native-carplay. I also incorporated the proposal of #158 to be able to boot the CarPlay app without having the phone app running.

I don't have the time right now to create a github repo, but here are my files: carplay.zip I added a plugins\carplay folder and put the files in there. Added the withCarPlay plugin to my app.json.

Be sure to set the correct entitlements as well. The config plugin adds the carplay-audio entitlement.

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. There is an open issue related to this problem: https://github.com/facebook/react-native/issues/35191

casperolesen commented 1 month ago

I got my app up and running using expo 51. This has not been tested in production yet 🚧

I made some changes to the plugin from @janwiebe-jump

You can see my changes here https://github.com/casperolesen/carplay-plugin/commit/80a5fb3713f7958e295e762677939caaca9774b4

thomas-rx commented 1 month ago

I got my app up and running using expo 51. This has not been tested in production yet 🚧

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. However, the linking is also broken.

When trying to open a specific page: ❯ npx uri-scheme open exp+app://_sitemap --ios › iOS: Opening URI "exp+app://_sitemap" in simulator

This does not work, I still haven't managed to fix this issue.

DanielKuhn commented 1 month ago

I'm not using expo, but the entry point for linking in iOS is the SceneDelegate's continue userActivity and open url methods, depending on the use case. Have you implemented these in your Phone Scene Delegate? Mine look like this:

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
  AppDelegate.shared.application(UIApplication.shared, continue: userActivity) { restorationHandler in
    // Handle restoration here if needed
  }
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
  guard let url = URLContexts.first?.url else {
    return
  }

  AppDelegate.shared.application(UIApplication.shared, open: url, options: [:])
}
thomas-rx commented 1 month ago

I'm not using expo, but the entry point for linking in iOS is the SceneDelegate's continue userActivity and open url methods, depending on the use case. Have you implemented these in your Phone Scene Delegate? Mine look like this:

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
  AppDelegate.shared.application(UIApplication.shared, continue: userActivity) { restorationHandler in
    // Handle restoration here if needed
  }
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
  guard let url = URLContexts.first?.url else {
    return
  }

  AppDelegate.shared.application(UIApplication.shared, open: url, options: [:])
}

Thank you @DanielKuhn 🎉

I've managed to get Linking to work when the app is in background. However, when it's completely closed, it doesn't work. The opening URL is null.

#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
DanielKuhn commented 1 month ago

When the app is cold-starting from a link, you need to pick the url from the connectionOptions. Add this as a third if in your connectionOptionsToLaunchOptions:

if let url = connectionOptions?.urlContexts.first?.url {
    launchOptions[.url] = url
}
DanielKuhn commented 1 month ago

Kudos to @alex-vasylchenko btw - I learned all this linking stuff from him :)

namluu25 commented 3 weeks ago

I got my app up and running using expo 51. This has not been tested in production yet 🚧

I made some changes to the plugin from @janwiebe-jump

You can see my changes here casperolesen/carplay-plugin@80a5fb3

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?

ibobo commented 3 weeks ago

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 AppDelegate.imports.mm:

#import <ExpoModulesCore/EXReactDelegateWrapper+Private.h>
#import <ExpoModulesCore/Swift.h>

with

#import <ExpoModulesCore/EXReactDelegateWrapper.h>
#import <ExpoModulesCore-Swift.h>

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?

namluu25 commented 3 weeks ago

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 AppDelegate.imports.mm:

#import <ExpoModulesCore/EXReactDelegateWrapper+Private.h>
#import <ExpoModulesCore/Swift.h>

with

#import <ExpoModulesCore/EXReactDelegateWrapper.h>
#import <ExpoModulesCore-Swift.h>

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?

Hi thanks for the solution. But since we were importing EXReactDelegateWrapper it did not exporting initWithExpoReactDelegate function to use in AppDelegate. Error image. image

_reactDelegate = [[EXReactDelegateWrapper alloc] initWithExpoReactDelegate:_expoAppDelegate.reactDelegate];

Do you know how to use this function from EXReactDelegateWrapper.h

ibobo commented 3 weeks ago
_reactDelegate = [[EXReactDelegateWrapper alloc] initWithExpoReactDelegate:_expoAppDelegate.reactDelegate];

Do you know how to use this function from EXReactDelegateWrapper.h

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 AppDelegate.topMethods.mm:

@interface EXReactDelegateWrapper(Private)

- (instancetype)initWithExpoReactDelegate:(EXReactDelegate *)expoReactDelegate;

@end

Also, I had some issues where the SceneDelegate.{h,mm} and CarSceneDelegate.{h,mm} were not added to the Xcode project, because my app name contained spaces, so I needed to fix withCarPlay.ts like this:

-  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.

namluu25 commented 3 weeks ago
_reactDelegate = [[EXReactDelegateWrapper alloc] initWithExpoReactDelegate:_expoAppDelegate.reactDelegate];

Do you know how to use this function from EXReactDelegateWrapper.h

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 AppDelegate.topMethods.mm:

@interface EXReactDelegateWrapper(Private)

- (instancetype)initWithExpoReactDelegate:(EXReactDelegate *)expoReactDelegate;

@end

Also, I had some issues where the SceneDelegate.{h,mm} and CarSceneDelegate.{h,mm} were not added to the Xcode project, because my app name contained spaces, so I needed to fix withCarPlay.ts like this:

-  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 @end in the end of AppDelegate.endMethods.mm and it seems like to fix the black screen error

caustin24345 commented 2 weeks ago

@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.