{"id":19084106,"url":"https://github.com/shaneosullivan/reactnativeexamplebrowserextension","last_synced_at":"2025-04-30T08:57:31.646Z","repository":{"id":67176758,"uuid":"102689745","full_name":"shaneosullivan/ReactNativeExampleBrowserExtension","owner":"shaneosullivan","description":"Example React Native app that implements a browser extension","archived":false,"fork":false,"pushed_at":"2018-05-07T18:30:48.000Z","size":6260,"stargazers_count":15,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-30T08:57:27.856Z","etag":null,"topics":["ios","react","react-native","safari"],"latest_commit_sha":null,"homepage":null,"language":"Objective-C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shaneosullivan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-09-07T04:03:50.000Z","updated_at":"2024-12-03T02:05:14.000Z","dependencies_parsed_at":"2023-03-11T01:04:59.521Z","dependency_job_id":null,"html_url":"https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shaneosullivan%2FReactNativeExampleBrowserExtension","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shaneosullivan%2FReactNativeExampleBrowserExtension/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shaneosullivan%2FReactNativeExampleBrowserExtension/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shaneosullivan%2FReactNativeExampleBrowserExtension/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shaneosullivan","download_url":"https://codeload.github.com/shaneosullivan/ReactNativeExampleBrowserExtension/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251674576,"owners_count":21625644,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","react","react-native","safari"],"created_at":"2024-11-09T02:50:06.472Z","updated_at":"2025-04-30T08:57:31.639Z","avatar_url":"https://github.com/shaneosullivan.png","language":"Objective-C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ReactNativeExampleBrowserExtension\n\nExample React Native app that implements a browser extension.\nThis example was created with React Native version 0.48.1. The instructions\nmay require updating for later versions. This project builds on the great\ninstructions published by PromptWorks at https://www.promptworks.com/blog/building-ios-app-extensions-with-react-native\n, which did a bit less than we're aiming for here, and were for an earlier version\nof React Native so could be a bit out of date.\n\n## Instructions for iOS (work in progress)\n\n1.  Create a React Native app, e.g.\n    `react-native init MyReactNativeExampleBrowserExtension`\n    This should create a folder called MyReactNativeExampleBrowserExtension.\n2.  Open the iOS app in XCode, using the file `MyReactNativeExampleBrowserExtension/ios/MyReactNativeExampleBrowserExtension.xcodeproj`\n\n### Creating the Extension\n\n3.  Select your project in the left bar, click the `General` tab, and click the\n    plus icon to add a new Target\n    ![Add Target](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/1a%20-%20Add%20New%20Target.png?raw=true)\n4.  Choose `Action Extension` to create the extension\n    ![Choose Action Extensions](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/1b%20-%20Choose%20Action%20Extension.png)\n    Make sure to choose Objective-C as the language, and leave the default selection\n    for `Action Type` to be `Presents User Interface`. For this example we will\n    be showing a UI built with React Native when the extension is activated.\n    ![Choose Objective C](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/1c%20-%20Choose%20Objective-C.png)\n    If you see a prompt to `Activate \"My Example Extension\" scheme`, click the `Activate` button.\n    ![Activate Scheme](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/1d%20-%20Activate%20Scheme.png)\n5.  At this point you have an Action Extension in your app. Note that a new folder named after the name of the extension you chose, in this example `My Example Extension`, has been created in the `ios` folder, and contains three files, `ActionViewController.h`, `ActionViewController.m` and `Info.plist`. We'll be\n    rewriting the code in `ActionViewController.m` pretty soon.\n    ![Folder Layout](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/1e%20-%20Folder%20Layout.png)\n\n### Configure the Action Extension to use React Native\n\nThe goal of this section is to configure the Action Extension you've just created\nto be the same as the main application. This example was created with React Native\nversion 0.48.1, so if you're using a later version and your config is different, use the config in your app instead.\n\n6.  In XCode, click your project name in the left bar. You may see a vertical list appear saying `Projects` and `Targets`, but if now, click the icon circled in the screenshot below to expand that UI control. Click the first target, in this example `ReactNativeExampleBrowserExtension` and expand the `Link Binary With Libraries` section. Note all the libraries in there, perhaps take a screenshot to avoid jumping back and forth. You'll need to replicate them elsewhere.\n    ![Note Libraries](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2a%20-%20Select%20Build%20Phases.png)\n7.  In the `Targets` list, select your extension, in this example `My Example Extension`. Click the `+` icon in the `Link Binary With Libraries` section and add\n    all of the libraries you saw in the main app config.\n    ![Create Libraries](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2b%20-%20Add%20New%20Libraries.png)\n    It should look like this when done\n    ![After Creating Libraries](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2c%20-%20After%20Adding%20New%20Libraries.png)\n8.  In order for the project to compile properly (compilation, remember that headache from the 90's? Yeah, well welcome back to it :-( ), we'll add some \"linker flags\". Select your extension's target in the project settings again, in this case `My Example Extension`. Click the `Build Settings` tab, and make sure to click the `All` button just below the tabs, otherwise XCode will helpfully hide the property you need to set.  \n    Scroll down to find the `Other Linker Flags` property, and double click on the row (do not expand it). This pops up a dialog, where you should click the `+` icon.\n    ![Add Linker Flags](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2d%20-%20Add%20Linker%20Flags.png)\n    Add `-ObjC` and `-lc++` to this dialog.\n    ![After Linker Flags](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2e%20-%20After%20Linker%20Flags.png)\n9.  To enable the app to load JavaScript we need to add an exception to the `App Transport Security Settings` to allow local files to be loaded. The easiest way to do this is to copy the required text from the main app. To do so, expand the main project section in the left bar, in this example `ReactNativeExampleBrowserExtension`, and choose the `Info.plist` file. Right click on the `App Transport Security Settings` property and copy it.\n    ![Copy Security Settings](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2f%20-%20Copy%20Security%20Settings.png)\n    Now expand the Extension section in the left bar, in this example `My Example Extension` and select the `Info.plist` file. Right click and paste. you should see the setting for `App Transport Security Settings` be replicated.\n    ![Paste Security Settings](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2g%20-%20Paste%20Security%20Settings.png)\n10. Now let's test out the app and make sure everything is working! In your terminal, make sure that you are in the root folder of your React Native app, in this example a folder called `ReactNativeExampleBrowserExtension`, and not in the `ios` subfolder. Run the command `react-native run-ios`, which should successfully build and run your app in the simulator.\n11. Once your app is visible, close it using the Home button (Cmd-Shift-H), and open Safari. Tap the share button on the bottom, and an action sheet should appear. Your shiny new extension is not visible here yet, so we have to activate it. (Sadly, end users will need to do the same when they install from the App Store. Talk to your nearest Apple executive when you run into them hanging around cafes in Cupertino about this...) Scroll the bottom row of icons all the way to the right and tap the `More` icon. This should pop up a list containing the name of your extension, in this example `My Example Extension`. Tap the toggle control to enable it and hit `Done`. You should now see the empty icon for your extension in the action sheet. Tap it, and you'll see the empty UI pop up with a `Done` button on the top. Congrats, your app works!\n    ![Paste Security Settings](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/2h%20-%20App%20Extension%20iOS.png)\n\n### Show React Native as the UI in the extension\n\nThe previous steps created the file `ActionViewController.m`, which defines what happens when the UI needs to load. Take a look at it's contents, you will see the `viewDidLoad`\nfunction containing the default code generated by XCode to render the UI. We will replace this with a `loadView` function that creates a RCTRootView with the JavaScript code.\n\n12. Add the following code to the top of `ActionViewController.m`\n    `#import \u003cReact/RCTBundleURLProvider.h\u003e #import \u003cReact/RCTRootView.h\u003e`\n13. Add the following code below the line `@implementation ActionViewController` in `ActionViewController.m`\n\n```Objective-C\n- (void)loadView {\n    NSURL *jsCodeLocation;\n\n    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\"index.ios\" fallbackResource:nil];\n\n    RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation\n                                                        moduleName:@\"ReactNativeExampleBrowserExtension\"\n                                                 initialProperties:nil\n                                                     launchOptions:nil];\n    rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];\n    self.view = rootView;\n}\n```\n\nNote that this code was mostly copied directly from `AppDelegate.m`, with `nil` replacing `launchOptions`\nsince there are no launch options.\n\n14. Comment out the entire `viewDidLoad` function.\n\n### Send parameter to customize React Native UI\n\nThe previous code showed the same UI in your action extension as appears in the main app.\nThis is unlikely to be what you want to have happen, so let's send a property to React Native to\ntell it to render a different UI.\n\n15. Update the `loadView` method in `ActionViewController.m` to pass in a dictionary with a property\n    called `isActionExtension` set to `true`. We pass this to the `initialProperties` value.\n\n```Objective-C\nNSDictionary *initialProps =\n  [NSDictionary dictionaryWithObject:[NSNumber numberWithBool: TRUE] forKey:@\"isActionExtension\"];\n\nRCTRootView *rootView =\n  [[RCTRootView alloc] initWithBundleURL:jsCodeLocation\n      moduleName:@\"ReactNativeExampleBrowserExtension\"\n      initialProperties:initialProps\n      launchOptions:nil];\n```\n\n16. Create a new React component called 'ActionExtensionScreen' in the file 'app/ActionExtensionScreen.js'\n    (create the `app` folder). Put the following code in there.\n\n```javascript\n// @flow\n\nimport React from 'react';\nimport { Text, View } from 'react-native';\n\nexport default class ActionExtensionScreen extends React.Component {\n  render() {\n    return (\n      \u003cView style={{ paddingTop: 100 }}\u003e\n        \u003cText style={{ fontSize: 30, textAlign: 'center' }}\u003eHello from our Action Extension!\u003c/Text\u003e\n      \u003c/View\u003e\n    );\n  }\n}\n```\n\n17. Update the root component of the app, in the file `index.ios.js`, to add the prop check for\n    isActionExtension.\n\n```javascript\nimport ActionExtensionScreen from './app/ActionExtensionScreen';\n\nexport default class ReactNativeExampleBrowserExtension extends Component {\n  static propTypes = {\n    isActionExtension: PropTypes.bool\n  };\n\n  static defaultProps = {\n    isActionExtension: false\n  };\n\n  render() {\n    if (this.props.isActionExtension) {\n      return \u003cActionExtensionScreen /\u003e;\n    } else {\n      return (\n        \u003cView style={styles.container}\u003e\n          \u003cText style={styles.welcome}\u003eWelcome to ReactNativeExampleBrowserExtension!\u003c/Text\u003e\n          \u003cText style={styles.instructions}\u003e\n            This app shows how to create a browser extension using React Native\n          \u003c/Text\u003e\n          \u003cText style={styles.instructions}\u003e\n            You can mostly ignore this main app code, all the fun is in the extension and the\n            README, which contains the instructions you should follow\n          \u003c/Text\u003e\n        \u003c/View\u003e\n      );\n    }\n  }\n}\n```\n\nNow when you open the action extension, you'll see the `ActionExtensionScreen` component rendered.\n![Custom RN Screen](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/4a%20-%20Custom%20RN%20screen.png)\n\n### Add a Done button to dismiss the extension\n\nTo close the extension, the `done` method on `ActionViewController` must be called. Unfortunately\nthis is in Objective-C, so we can't call it directly from JavaScript. To enable this we\nwill create a Native Module.\n\nThis gets a bit hacky due to the fact that Action Extensions do not have access to the application\ncontext so we need to keep track of the ActionViewController ourselves.\n\n18. Update the `ActionViewController.h` file to expose a pointer to the view controller,\n    and set that pointer when the view is loaded. The new code looks like:\n\n```Objective-C\n#import \u003cUIKit/UIKit.h\u003e\n\n@interface ActionViewController : UIViewController\n\n- (void) done;\n\nextern ActionViewController * actionViewController;\n\n@end\n```\n\n19. Update `ActionViewContoller.m` to also include a pointer to `actionViewController`,\n    and set the value of the pointer at the end of the `loadView` function.\n\n```Objective-C\n@interface ActionViewController ()\n\n@property(strong,nonatomic) IBOutlet UIImageView *imageView;\n\n@end\n\nActionViewController * actionViewController = nil;\n\n@implementation ActionViewController\n\n- (void)loadView {\n  // All other existing code here....\n\n  // New code\n  actionViewController = self;\n}\n```\n\n20. Time to create the NativeModule! Create a new file called `ActionExtension.h` in the same\n    folder as `AppDelegate.m`.\n\n```Objective-C\n#import \u003cReact/RCTBridgeModule.h\u003e\n\n@interface ActionExtension : NSObject \u003cRCTBridgeModule\u003e\n@end\n```\n\nCreate a file called `ActionExtension.m` in the same folder. You will be prompted to choose\nthe targets that the file should be compiled for. Make sure to select both the default one,\n`My Example Extension` and the main target, which should be the first in the list.\n\n```Objective-C\n#import \"ActionExtension.h\"\n#import \"ActionViewController.h\"\n\n@implementation ActionExtension\n\nRCT_EXPORT_MODULE();\n\nRCT_EXPORT_METHOD(done) {\n  [actionViewController done];\n}\n\n@end\n```\n\n![Select target](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/4b%20-%20Add%20targets.png)\n\nTo avoid compilation/linker issues, add `ActionViewController.m` to the `Compile Sources` list of the\nmain project's `Build Phases` tab.\n![Select target](https://github.com/shaneosullivan/ReactNativeExampleBrowserExtension/blob/master/ReadmeMedia/4c%20-%20Add%20compile%20source.png)\n\n21. Update the `ActionExtensionScreen.js` file to contain a `Done` component, which when pressed\n    activates the newly exposed `done()` function on the native module.\n\n```javascript\n// @flow\n\nimport React from 'react';\nimport { NativeModules, TouchableOpacity, Text, View } from 'react-native';\n\nexport default class ActionExtensionScreen extends React.Component {\n  render() {\n    return (\n      \u003cView style={{ paddingTop: 100 }}\u003e\n        \u003cText style={{ fontSize: 30, textAlign: 'center' }}\u003eHello from our Action Extension!\u003c/Text\u003e\n        \u003cView style={{ paddingTop: 100 }}\u003e\n          \u003cTouchableOpacity onPress={this._handleDone}\u003e\n            \u003cText style={{ fontSize: 30, textAlign: 'center' }}\u003eDone\u003c/Text\u003e\n          \u003c/TouchableOpacity\u003e\n        \u003c/View\u003e\n      \u003c/View\u003e\n    );\n  }\n\n  _handleDone = () =\u003e {\n    // Call the function that has been exposed on the native module to close the screen.\n    NativeModules.ActionExtension.done();\n  };\n}\n```\n\nNow run your app again with `react-native run-ios` and try opening the extension from a browser.\nClicking the `Done` component should close the extension, taking you back to the browser!\n\n### Running in Release mode\n\nRunning in Release mode, rather than the Dev mode that we've been using up until now, does not work\nout of the box when using an Action Extension. This is because, to run in Release mode,\nReact Native bundles up all the required JavaScript code and creates a file called `main.jsbundle`,\nand adds that file to the build folder of your application. Of course, the App Extension is\na separate application, so it doesn't have access to this file. The result is that when you try to\nopen the app extension from the browser, nothing happens.\n\nThere is likely a way to enable cross-application bundle access (a bundle is what Apple calls the\nset of assets in your app), and I tried out a few that didn't work, but the approach I found to\nwork was to write a script that runs after React Native finishes building and copies the file into\nthe extension app.\n\n22. Create a folder in the root of the project called `scripts`. In that folder, create a text file\n    called `copyJsBundle.sh`. In that file, add the following:\n\n```\ncp \"$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle\" \"$CONFIGURATION_BUILD_DIR/\u003cYOUR_APP_EXTENSION_NAME\u003e.appex\"\ncp \"$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle.meta\" \"$CONFIGURATION_BUILD_DIR/\u003cYOUR_APP_EXTENSION_NAME\u003e.appex\"\n```\n\nIn this, replace \u003cYOUR_APP_EXTENSION_NAME\u003e with the name of your app extension, in this example\nit is `My Example Extension`.\n\nThe other parameters are passed in by React Native, which I discovered looking at the build file\nfor iOS in `node_modules/react-native/scripts/react-native-xcode.sh`.\n\nYou need to make this file executable, so in a terminal, from the root of your project, type\n\n```\nchmod +x scripts/copyJsBundle.sh\n```\n\n23. Now you have to tell XCode to run this script after the main build has completed. This is done\n    by editing the file `ios/\u003cYOUR_PROJECT\u003e.xcodeproj/project.pbxproj`, so in this case\n    `ios/ReactNativeExampleBrowserExtension/project.pbxproj`. In this file, search for any instances of\n    `react-native-xcode.sh`, then edit that line to add `\u0026\u0026 ../scripts/copyJsBundle.sh`, so the line\n    should look like\n\n```\nshellScript = \"export NODE_BINARY=node\\n../node_modules/react-native/scripts/react-native-xcode.sh \u0026\u0026 ../scripts/copyJsBundle.sh\";\n```\n\n24. Switch both the main project's scheme and the extension project's scheme to Release mode\n    by going to the menu `Product/Scheme/Edit Scheme`, clicking on `Run` in the left side of the\n    dialog that pops up, and choosing `Release` from the dropdown. You can toggle between different\n    project schemes in the top left of the dialog to modify both in one go.\n\n25. Run the app on the simulator, open the browser, go to some website, open the Share dialog and\n    tap your extension's icon. It should open up correctly and render the React Native UI! This\n    also means it should be possible to actually package up your app and send it to Apple for entry\n    in the app store.\n\n### Debugging your extension\n\nYou'll notice that when you open your extension in the Simulator, you do not see logs show up\nin the XCode console. This is because XCode automatically attaches its debug session to the main\napp. The way to fix this is to attach the debug session to the app from which you'll open your\nextension, in this example it's Safari.\n\n26. Open the Scheme for your extension. Click the menu `Product/Scheme/Edit Scheme` and choose\n    your extension's project in the top left of the dialog that pops up. Choose `Run` on the left side\n    of the dialog. Change the `Executable` dropdown to `Ask on Launch`. Click the `Close` button.\n\n27. At the top of XCode, just to the right of the Stop button, choose your extension project to be\n    run. It was probably on your main app all along. Click the `Play` button to run it and you should\n    be prompted for the app to run it in the context of. Choose Safari. Now you should see all your\n    logs in the Console! If you do not see the Console at all, you can show it by going to the menu\n    `View/Debug Area/Activate Console`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshaneosullivan%2Freactnativeexamplebrowserextension","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshaneosullivan%2Freactnativeexamplebrowserextension","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshaneosullivan%2Freactnativeexamplebrowserextension/lists"}