├── docs
├── static
│ ├── .nojekyll
│ └── img
│ │ ├── favicon.ico
│ │ ├── safari.png
│ │ ├── docusaurus.png
│ │ ├── docusaurus-social-card.jpg
│ │ ├── logo.svg
│ │ ├── undraw_docusaurus_tree.svg
│ │ ├── undraw_docusaurus_mountain.svg
│ │ └── undraw_docusaurus_react.svg
├── babel.config.js
├── src
│ ├── pages
│ │ ├── markdown-page.md
│ │ └── index.module.css
│ ├── components
│ │ └── HomepageFeatures
│ │ │ ├── styles.module.css
│ │ │ └── index.js
│ └── css
│ │ └── custom.css
├── .gitignore
├── docs
│ ├── messaging.md
│ ├── api.md
│ ├── troubleshooting.md
│ ├── getting-started.md
│ ├── extension-files.md
│ ├── basic.md
│ └── experimental.md
├── sidebars.js
├── README.md
├── package.json
└── docusaurus.config.js
├── examples
├── expo-router-example
│ ├── index.ts
│ ├── MyExtension
│ │ ├── src
│ │ │ ├── popup.js
│ │ │ ├── background.js
│ │ │ ├── content.js
│ │ │ ├── popup.html
│ │ │ └── popup.css
│ │ ├── assets
│ │ │ └── assets
│ │ │ │ ├── favicon.png
│ │ │ │ ├── icon-128.png
│ │ │ │ ├── icon-256.png
│ │ │ │ ├── icon-48.png
│ │ │ │ ├── icon-512.png
│ │ │ │ ├── icon-64.png
│ │ │ │ ├── icon-96.png
│ │ │ │ ├── toolbar-icon-16.png
│ │ │ │ ├── toolbar-icon-19.png
│ │ │ │ ├── toolbar-icon-32.png
│ │ │ │ ├── toolbar-icon-38.png
│ │ │ │ ├── toolbar-icon-48.png
│ │ │ │ └── toolbar-icon-72.png
│ │ ├── SafariWebExtensionHandler.swift
│ │ ├── Info.plist
│ │ └── manifest.json
│ ├── assets
│ │ ├── icon.png
│ │ ├── favicon.png
│ │ ├── splash.png
│ │ └── adaptive-icon.png
│ ├── .prettierrc
│ ├── tsconfig.json
│ ├── babel.config.js
│ ├── app
│ │ ├── [...unmatched].tsx
│ │ ├── details.tsx
│ │ ├── _layout.tsx
│ │ └── index.tsx
│ ├── .gitignore
│ ├── metro.config.js
│ ├── patches
│ │ └── @expo+metro-runtime+2.2.16.patch
│ ├── app.json
│ └── package.json
├── react-navigation-example
│ ├── .env
│ ├── MyExtension
│ │ ├── src
│ │ │ ├── popup.js
│ │ │ ├── background.js
│ │ │ ├── content.js
│ │ │ ├── popup.html
│ │ │ └── popup.css
│ │ ├── assets
│ │ │ └── assets
│ │ │ │ ├── favicon.png
│ │ │ │ ├── icon-128.png
│ │ │ │ ├── icon-256.png
│ │ │ │ ├── icon-48.png
│ │ │ │ ├── icon-512.png
│ │ │ │ ├── icon-64.png
│ │ │ │ ├── icon-96.png
│ │ │ │ ├── toolbar-icon-16.png
│ │ │ │ ├── toolbar-icon-19.png
│ │ │ │ ├── toolbar-icon-32.png
│ │ │ │ ├── toolbar-icon-38.png
│ │ │ │ ├── toolbar-icon-48.png
│ │ │ │ └── toolbar-icon-72.png
│ │ ├── MyExtension.entitlements
│ │ ├── SafariWebExtensionHandler.swift
│ │ ├── Info.plist
│ │ └── manifest.json
│ ├── tsconfig.json
│ ├── assets
│ │ ├── icon.png
│ │ ├── favicon.png
│ │ ├── splash.png
│ │ └── adaptive-icon.png
│ ├── .prettierrc
│ ├── App.tsx
│ ├── babel.config.js
│ ├── .gitignore
│ ├── eas.json
│ ├── src
│ │ ├── screens
│ │ │ ├── details.tsx
│ │ │ └── overview.tsx
│ │ └── navigation
│ │ │ └── index.tsx
│ ├── patches
│ │ └── @expo+metro-runtime+2.2.16.patch
│ ├── package.json
│ └── app.json
└── basic-example
│ ├── MyExtension
│ ├── src
│ │ ├── popup.js
│ │ ├── background.js
│ │ ├── popup.html
│ │ ├── content.js
│ │ └── popup.css
│ ├── assets
│ │ └── assets
│ │ │ ├── favicon.png
│ │ │ ├── icon-48.png
│ │ │ ├── icon-64.png
│ │ │ ├── icon-96.png
│ │ │ ├── icon-128.png
│ │ │ ├── icon-256.png
│ │ │ ├── icon-512.png
│ │ │ ├── toolbar-icon-16.png
│ │ │ ├── toolbar-icon-19.png
│ │ │ ├── toolbar-icon-32.png
│ │ │ ├── toolbar-icon-38.png
│ │ │ ├── toolbar-icon-48.png
│ │ │ └── toolbar-icon-72.png
│ ├── SafariWebExtensionHandler.swift
│ ├── Info.plist
│ └── manifest.json
│ ├── assets
│ ├── icon.png
│ ├── splash.png
│ ├── favicon.png
│ └── adaptive-icon.png
│ ├── tsconfig.json
│ ├── .prettierrc
│ ├── App.tsx
│ ├── babel.config.js
│ ├── .gitignore
│ ├── app.json
│ ├── src
│ ├── screens
│ │ ├── details.tsx
│ │ └── overview.tsx
│ └── navigation
│ │ └── index.tsx
│ └── package.json
├── app.plugin.js
├── .gitignore
├── .eslintrc.js
├── plugin
├── src
│ ├── utils.ts
│ ├── withAppEntitlements.ts
│ ├── xcodeSafariExtension
│ │ ├── addTargetDependency.ts
│ │ ├── addToPbxProjectSection.ts
│ │ ├── addProductFile.ts
│ │ ├── addPbxGroup.ts
│ │ ├── addToPbxNativeTargetSection.ts
│ │ ├── addBuildPhases.ts
│ │ ├── addXCConfigurationList.ts
│ │ └── xcodeSafariExtension.ts
│ ├── withXcodeTarget.ts
│ ├── withExtensionEntitlements.ts
│ ├── withSafariExtension.ts
│ ├── withPodfile.ts
│ ├── withSafariWebExtensionHandler.ts
│ ├── withExtensionInfoPlist.ts
│ └── withExtensionConfig.ts
└── tsconfig.json
├── tsconfig.json
├── src
└── index.ts
├── README.md
└── package.json
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/expo-router-example/index.ts:
--------------------------------------------------------------------------------
1 | import 'expo-router/entry';
2 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/.env:
--------------------------------------------------------------------------------
1 | EXPO_PUBLIC_SAFARI_EXTENSION_PORT=8081
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/src/popup.js:
--------------------------------------------------------------------------------
1 | console.log('Hello World!!');
2 |
--------------------------------------------------------------------------------
/app.plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./plugin/build/withSafariExtension.js");
2 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/src/popup.js:
--------------------------------------------------------------------------------
1 | console.log('Hello World!!');
2 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/src/popup.js:
--------------------------------------------------------------------------------
1 | console.log('Hello World!!');
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | *.tgz
4 | .DS_Store
5 | examples/**/node_modules/
6 | examples/**/ios/
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // @generated by expo-module-scripts
2 | module.exports = require('expo-module-scripts/eslintrc.base.js');
3 |
--------------------------------------------------------------------------------
/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/safari.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/docs/static/img/safari.png
--------------------------------------------------------------------------------
/docs/static/img/docusaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/docs/static/img/docusaurus.png
--------------------------------------------------------------------------------
/examples/basic-example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/assets/icon.png
--------------------------------------------------------------------------------
/examples/basic-example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/assets/splash.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/docs/static/img/docusaurus-social-card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/docs/static/img/docusaurus-social-card.jpg
--------------------------------------------------------------------------------
/examples/basic-example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/assets/favicon.png
--------------------------------------------------------------------------------
/examples/basic-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 |
6 | }
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/examples/expo-router-example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/assets/icon.png
--------------------------------------------------------------------------------
/examples/basic-example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/examples/expo-router-example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/assets/favicon.png
--------------------------------------------------------------------------------
/examples/expo-router-example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/assets/splash.png
--------------------------------------------------------------------------------
/docs/src/pages/markdown-page.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Markdown page example
3 | ---
4 |
5 | # Markdown page example
6 |
7 | You don't need React to write simple standalone pages.
8 |
--------------------------------------------------------------------------------
/examples/basic-example/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "bracketSameLine": true,
6 | "trailingComma": "es5"
7 | }
--------------------------------------------------------------------------------
/examples/react-navigation-example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/assets/icon.png
--------------------------------------------------------------------------------
/examples/expo-router-example/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "bracketSameLine": true,
6 | "trailingComma": "es5"
7 | }
--------------------------------------------------------------------------------
/examples/expo-router-example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/assets/favicon.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/assets/splash.png
--------------------------------------------------------------------------------
/plugin/src/utils.ts:
--------------------------------------------------------------------------------
1 | import * as util from "util";
2 |
3 | export type PBXFile = any;
4 |
5 | export function quoted(str: string) {
6 | return util.format(`"%s"`, str);
7 | }
8 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "bracketSameLine": true,
6 | "trailingComma": "es5"
7 | }
--------------------------------------------------------------------------------
/examples/react-navigation-example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/favicon.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/icon-48.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/icon-64.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/icon-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/icon-96.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/icon-128.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/icon-256.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/icon-512.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/toolbar-icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/toolbar-icon-16.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/toolbar-icon-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/toolbar-icon-19.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/toolbar-icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/toolbar-icon-32.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/toolbar-icon-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/toolbar-icon-38.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/toolbar-icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/toolbar-icon-48.png
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/assets/assets/toolbar-icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/basic-example/MyExtension/assets/assets/toolbar-icon-72.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/favicon.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/icon-128.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/icon-256.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/icon-48.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/icon-512.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/icon-64.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/icon-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/icon-96.png
--------------------------------------------------------------------------------
/examples/basic-example/App.tsx:
--------------------------------------------------------------------------------
1 | import "react-native-gesture-handler";
2 |
3 | import RootStack from "./src/navigation";
4 |
5 | export default function App() {
6 |
7 | return ;
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/favicon.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/icon-128.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/icon-256.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/icon-48.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/icon-512.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/icon-64.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/icon-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/icon-96.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-16.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-19.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-32.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-38.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-48.png
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/expo-router-example/MyExtension/assets/assets/toolbar-icon-72.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo-module-scripts/tsconfig.plugin",
3 | "compilerOptions": {
4 | "outDir": "./build"
5 | },
6 | "include": ["./src"],
7 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
8 | }
9 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-16.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-19.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-32.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-38.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-48.png
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrew-levy/react-native-safari-extension/HEAD/examples/react-navigation-example/MyExtension/assets/assets/toolbar-icon-72.png
--------------------------------------------------------------------------------
/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo-module-scripts/tsconfig.plugin",
3 | "compilerOptions": {
4 | "outDir": "./build"
5 | },
6 | "include": ["./src"],
7 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
8 | }
9 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/styles.module.css:
--------------------------------------------------------------------------------
1 | .features {
2 | display: flex;
3 | align-items: center;
4 | padding: 2rem 0;
5 | width: 100%;
6 | }
7 |
8 | .featureSvg {
9 | height: 200px;
10 | width: 200px;
11 | }
12 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/App.tsx:
--------------------------------------------------------------------------------
1 | import '@expo/metro-runtime';
2 | import 'react-native-gesture-handler';
3 |
4 | import RootStack from './src/navigation';
5 |
6 | export default function App() {
7 | return ;
8 | }
9 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | let plugins = [];
4 |
5 | return {
6 | presets: ['babel-preset-expo'],
7 | plugins: plugins,
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export function isSafariExtension(): boolean {
2 | return (
3 | // @ts-ignore
4 | typeof window !== "undefined" &&
5 | // @ts-ignore
6 | window?.location?.href.startsWith("safari-web-extension://")
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/src/background.js:
--------------------------------------------------------------------------------
1 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
2 | console.log("Received request: ", request);
3 |
4 | if (request.greeting === "hello") sendResponse({ farewell: "goodbye" });
5 | });
6 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/src/background.js:
--------------------------------------------------------------------------------
1 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
2 | console.log("Received request: ", request);
3 |
4 | if (request.greeting === "hello") sendResponse({ farewell: "goodbye" });
5 | });
6 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/src/background.js:
--------------------------------------------------------------------------------
1 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
2 | console.log("Received request: ", request);
3 |
4 | if (request.greeting === "hello") sendResponse({ farewell: "goodbye" });
5 | });
6 |
--------------------------------------------------------------------------------
/examples/basic-example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | let plugins = [];
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | return {
14 | presets: ['babel-preset-expo'],
15 | plugins: plugins,
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/examples/expo-router-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 |
6 | },
7 | "include": [
8 | "**/*.ts",
9 | "**/*.tsx",
10 | ".expo/types/**/*.ts",
11 | "expo-env.d.ts"
12 | ]
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/src/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Hello World!
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/src/content.js:
--------------------------------------------------------------------------------
1 | browser.runtime.sendMessage({ greeting: "hello" }).then((response) => {
2 | console.log("Received response: ", response);
3 | });
4 |
5 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
6 | console.log("Received request: ", request);
7 | });
8 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/src/content.js:
--------------------------------------------------------------------------------
1 | browser.runtime.sendMessage({ greeting: "hello" }).then((response) => {
2 | console.log("Received response: ", response);
3 | });
4 |
5 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
6 | console.log("Received request: ", request);
7 | });
8 |
--------------------------------------------------------------------------------
/examples/basic-example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 |
14 |
15 | # macOS
16 | .DS_Store
17 |
18 | # Temporary files created by Metro to check the health of the file watcher
19 | .metro-health-check*
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/src/content.js:
--------------------------------------------------------------------------------
1 | browser.runtime.sendMessage({ greeting: "hello" }).then((response) => {
2 | console.log("Received response: ", response);
3 | });
4 |
5 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
6 | console.log("Received request: ", request);
7 | });
8 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/examples/expo-router-example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | let plugins = [];
4 |
5 |
6 |
7 |
8 |
9 |
10 | plugins.push('expo-router/babel');
11 |
12 |
13 |
14 |
15 | return {
16 | presets: ['babel-preset-expo'],
17 | plugins: plugins,
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 |
14 |
15 | # macOS
16 | .DS_Store
17 |
18 | # Temporary files created by Metro to check the health of the file watcher
19 | .metro-health-check*
--------------------------------------------------------------------------------
/examples/expo-router-example/app/[...unmatched].tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'expo-router';
2 | import { useEffect } from 'react';
3 |
4 | export default function Unmatched() {
5 | const router = useRouter();
6 | useEffect(() => {
7 | setTimeout(() => {
8 | router.replace('/');
9 | }, 1);
10 | }, []);
11 | return null;
12 | }
13 |
--------------------------------------------------------------------------------
/examples/expo-router-example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 | # expo router
13 | expo-env.d.ts
14 |
15 |
16 | # macOS
17 | .DS_Store
18 |
19 | # Temporary files created by Metro to check the health of the file watcher
20 | .metro-health-check*
--------------------------------------------------------------------------------
/plugin/src/withAppEntitlements.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withEntitlementsPlist } from "@expo/config-plugins";
2 |
3 | export const withAppEntitlements: ConfigPlugin = (config) => {
4 | return withEntitlementsPlist(config, (config) => {
5 | config.modResults["com.apple.security.application-groups"] = [
6 | `group.${config.ios!.bundleIdentifier}`,
7 | ];
8 | return config;
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/examples/expo-router-example/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require('expo/metro-config');
3 |
4 | /** @type {import('expo/metro-config').MetroConfig} */
5 | // eslint-disable-next-line no-undef
6 | const config = getDefaultConfig(__dirname, {
7 | // [Web-only]: Enables CSS support in Metro.
8 | isCSSEnabled: true
9 | });
10 |
11 | module.exports = config;
12 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/MyExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.com.anonymous.react-navigation-example2
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 5.9.1"
4 | },
5 | "build": {
6 | "development": {
7 | "developmentClient": true,
8 | "distribution": "internal",
9 | "ios": {
10 | "simulator": true
11 | }
12 | },
13 | "preview": {
14 | "distribution": "internal"
15 | },
16 | "production": {}
17 | },
18 | "submit": {
19 | "production": {}
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/docs/docs/messaging.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | title: "Extension ⇆ App Messaging"
4 | ---
5 |
6 | You can send messages between your app and extension using [this guide](https://developer.apple.com/documentation/safariservices/safari_web_extensions/messaging_between_the_app_and_javascript_in_a_safari_web_extension). This plugin sets up App Groups automatically for you using the `com.apple.security.application-groups` entitlement with a value of `group.{YOUR_APP_BUNDLE_ID}`,
7 |
--------------------------------------------------------------------------------
/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS files with the .module.css suffix will be treated as CSS modules
3 | * and scoped locally.
4 | */
5 |
6 | .heroBanner {
7 | padding: 4rem 0;
8 | text-align: center;
9 | position: relative;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 996px) {
14 | .heroBanner {
15 | padding: 2rem;
16 | }
17 | }
18 |
19 | .buttons {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | }
24 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/addTargetDependency.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export default function addTargetDependency(proj: XcodeProject, target: any) {
4 | if (!proj.hash.project.objects["PBXTargetDependency"]) {
5 | proj.hash.project.objects["PBXTargetDependency"] = {};
6 | }
7 | if (!proj.hash.project.objects["PBXContainerItemProxy"]) {
8 | proj.hash.project.objects["PBXContainerItemProxy"] = {};
9 | }
10 |
11 | proj.addTargetDependency(proj.getFirstTarget().uuid, [target.uuid]);
12 | }
13 |
--------------------------------------------------------------------------------
/docs/docs/api.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | title: "API"
4 | ---
5 |
6 | ### `isSafariExtension`
7 |
8 | Returns if the app is running in a Safari Extension. Use this to conditionally render components that should only be rendered in the extension.
9 |
10 | ```ts
11 | function isSafariExtension(): boolean;
12 | ```
13 |
14 | Example:
15 |
16 | ```tsx
17 | import { isSafariExtension } from "react-native-safari-extension";
18 |
19 | function App() {
20 | if (isSafariExtension()) {
21 | return ;
22 | }
23 | return ;
24 | }
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #2976eb;
10 | --ifm-code-font-size: 95%;
11 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
12 | }
13 |
14 | /* For readability concerns, you should choose a lighter palette in dark mode. */
15 | [data-theme="dark"] {
16 | --ifm-color-primary: #5093f7;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
18 | }
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.buymeacoffee.com/hugemathguy)
2 |
3 | # react-native-safari-extension
4 |
5 | ## What is it?
6 |
7 | An [Expo Config Plugin](https://docs.expo.dev/config-plugins/introduction/) that allows you to add a Safari Extension to your iOS apps. This plugin allows you to manage your extension
8 | without having to open Xcode.
9 |
10 | > **Note** Not sure what Safari Extensions are? Check out [Apple's Safari Extension documentation](https://developer.apple.com/safari/extensions/) to learn more.
11 |
12 | ## Getting Started
13 |
14 | See the [official documentation](https://react-native-safari-extension.vercel.app) to get started.
15 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/addToPbxProjectSection.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export default function addToPbxProjectSection(
4 | proj: XcodeProject,
5 | target: any
6 | ) {
7 | proj.addToPbxProjectSection(target);
8 |
9 | // Add target attributes to project section
10 | if (
11 | !proj.pbxProjectSection()[proj.getFirstProject().uuid].attributes
12 | .TargetAttributes
13 | ) {
14 | proj.pbxProjectSection()[
15 | proj.getFirstProject().uuid
16 | ].attributes.TargetAttributes = {};
17 | }
18 | proj.pbxProjectSection()[
19 | proj.getFirstProject().uuid
20 | ].attributes.TargetAttributes[target.uuid] = {
21 | CreatedOnToolsVersion: "13.4.1",
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/SafariWebExtensionHandler.swift:
--------------------------------------------------------------------------------
1 |
2 | import SafariServices
3 | import os.log
4 | import Alamofire
5 |
6 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
7 | func beginRequest(with context: NSExtensionContext) {
8 | let item = context.inputItems[0] as! NSExtensionItem
9 | let message = item.userInfo?[SFExtensionMessageKey]
10 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
11 |
12 | let response = NSExtensionItem()
13 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
14 |
15 | context.completeRequest(returningItems: [response], completionHandler: nil)
16 | }
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/src/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/plugin/src/withXcodeTarget.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withXcodeProject } from "@expo/config-plugins";
2 | import { addSafariExtensionXcodeTarget } from "./xcodeSafariExtension/xcodeSafariExtension";
3 |
4 | export const withXcodeTarget: ConfigPlugin<{
5 | folderName: string;
6 | }> = (config, { folderName }) => {
7 | return withXcodeProject(config, (config) => {
8 | addSafariExtensionXcodeTarget(config.modResults, {
9 | extensionName: folderName,
10 | extensionBundleIdentifier: `${config.ios?.bundleIdentifier}.${folderName}`,
11 | currentProjectVersion: config.ios?.buildNumber || "1",
12 | marketingVersion: config.version || "1.0.0",
13 | iosRoot: config.modRequest.platformProjectRoot,
14 | });
15 |
16 | return config;
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/addProductFile.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export default function addProductFile(
4 | proj: XcodeProject,
5 | extensionName: string,
6 | groupName: string
7 | ) {
8 | const productFile = {
9 | basename: `${extensionName}.appex`,
10 | fileRef: proj.generateUuid(),
11 | uuid: proj.generateUuid(),
12 | group: groupName,
13 | explicitFileType: "wrapper.application",
14 | settings: {
15 | ATTRIBUTES: ["RemoveHeadersOnCopy"],
16 | },
17 | includeInIndex: 0,
18 | path: `${extensionName}.appex`,
19 | sourceTree: "BUILT_PRODUCTS_DIR",
20 | };
21 |
22 | proj.addToPbxFileReferenceSection(productFile);
23 | proj.addToPbxBuildFileSection(productFile);
24 |
25 | return productFile;
26 | }
27 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/src/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/SafariWebExtensionHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SafariWebExtensionHandler.swift
3 | // NewTestExtension
4 | //
5 | // Created by {author} on 6/22/22.
6 | //
7 |
8 | import SafariServices
9 | import os.log
10 |
11 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
12 |
13 | func beginRequest(with context: NSExtensionContext) {
14 | let item = context.inputItems[0] as! NSExtensionItem
15 | let message = item.userInfo?[SFExtensionMessageKey]
16 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
17 |
18 | let response = NSExtensionItem()
19 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
20 |
21 | context.completeRequest(returningItems: [response], completionHandler: nil)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/SafariWebExtensionHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SafariWebExtensionHandler.swift
3 | // NewTestExtension
4 | //
5 | // Created by {author} on 6/22/22.
6 | //
7 |
8 | import SafariServices
9 | import os.log
10 |
11 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
12 |
13 | func beginRequest(with context: NSExtensionContext) {
14 | let item = context.inputItems[0] as! NSExtensionItem
15 | let message = item.userInfo?[SFExtensionMessageKey]
16 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
17 |
18 | let response = NSExtensionItem()
19 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
20 |
21 | context.completeRequest(returningItems: [response], completionHandler: nil)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/addPbxGroup.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export default function addPbxGroup(
4 | proj: XcodeProject,
5 | { extensionName }: { extensionName: string }
6 | ) {
7 | // Add PBX group
8 | const { uuid: pbxGroupUuid } = proj.addPbxGroup(
9 | [
10 | "src",
11 | "assets",
12 | "manifest.json",
13 | "Info.plist",
14 | "SafariWebExtensionHandler.swift",
15 | ],
16 | extensionName,
17 | `../${extensionName}`
18 | );
19 |
20 | // Add PBXGroup to top level group
21 | const groups = proj.hash.project.objects["PBXGroup"];
22 | if (pbxGroupUuid) {
23 | Object.keys(groups).forEach(function (key) {
24 | if (groups[key].name === undefined && groups[key].path === undefined) {
25 | proj.addToPbxGroup(pbxGroupUuid, key);
26 | }
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/expo-router-example/app/details.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { StyleSheet, View, Text } from "react-native";
3 |
4 | import { useLocalSearchParams } from "expo-router";
5 |
6 | export default function Details() {
7 | const { name } = useLocalSearchParams();
8 | return (
9 |
10 |
11 |
12 | Details
13 | Showing details for user {name}.
14 |
15 |
16 |
17 | );
18 | }
19 |
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | padding: 24,
25 | },
26 | main: {
27 | flex: 1,
28 | maxWidth: 960,
29 | marginHorizontal: "auto",
30 | },
31 | title: {
32 | fontSize: 64,
33 | fontWeight: "bold",
34 | },
35 | subtitle: {
36 | fontSize: 36,
37 | color: "#38434D",
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | // @ts-check
13 |
14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
15 | const sidebars = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | 'intro',
23 | 'hello',
24 | {
25 | type: 'category',
26 | label: 'Tutorial',
27 | items: ['tutorial-basics/create-a-document'],
28 | },
29 | ],
30 | */
31 | };
32 |
33 | export default sidebars;
34 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/examples/basic-example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "basic-example",
4 | "slug": "basic-example",
5 | "version": "1.0.0",
6 | "web": {
7 | "favicon": "./assets/favicon.png"
8 | },
9 | "orientation": "portrait",
10 | "icon": "./assets/icon.png",
11 | "userInterfaceStyle": "light",
12 | "splash": {
13 | "image": "./assets/splash.png",
14 | "resizeMode": "contain",
15 | "backgroundColor": "#ffffff"
16 | },
17 | "assetBundlePatterns": ["**/*"],
18 | "ios": {
19 | "supportsTablet": true,
20 | "bundleIdentifier": "com.alevy97.basic-example"
21 | },
22 | "android": {
23 | "adaptiveIcon": {
24 | "foregroundImage": "./assets/adaptive-icon.png",
25 | "backgroundColor": "#ffffff"
26 | }
27 | },
28 | "plugins": [
29 | [
30 | "react-native-safari-extension",
31 | {
32 | "folderName": "MyExtension"
33 | }
34 | ]
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/addToPbxNativeTargetSection.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | import { PBXFile, quoted } from "../utils";
4 |
5 | export default function addToPbxNativeTargetSection(
6 | proj: XcodeProject,
7 | {
8 | extensionName,
9 | targetUuid,
10 | productFile,
11 | xCConfigurationList,
12 | }: {
13 | extensionName: string;
14 | targetUuid: string;
15 | productFile: PBXFile;
16 | xCConfigurationList: any;
17 | }
18 | ) {
19 | const target = {
20 | uuid: targetUuid,
21 | pbxNativeTarget: {
22 | isa: "PBXNativeTarget",
23 | name: extensionName,
24 | productName: extensionName,
25 | productReference: productFile.fileRef,
26 | productType: quoted("com.apple.product-type.app-extension"),
27 | buildConfigurationList: xCConfigurationList.uuid,
28 | buildPhases: [],
29 | buildRules: [],
30 | dependencies: [],
31 | },
32 | };
33 |
34 | proj.addToPbxNativeTargetSection(target);
35 |
36 | return target;
37 | }
38 |
--------------------------------------------------------------------------------
/examples/basic-example/src/screens/details.tsx:
--------------------------------------------------------------------------------
1 | import { RouteProp, useRoute } from "@react-navigation/native";
2 |
3 | import { View, StyleSheet, Text } from "react-native";
4 |
5 | import { RootStackParamList } from "../navigation";
6 |
7 | type DetailsSreenRouteProp = RouteProp;
8 |
9 | export default function Details() {
10 | const router = useRoute();
11 |
12 | return (
13 |
14 |
15 | Details
16 | Showing details for user {router.params.name}.
17 |
18 |
19 | );
20 |
21 | }
22 |
23 |
24 | const styles = StyleSheet.create({
25 | container: {
26 | flex: 1,
27 | padding: 24,
28 | },
29 | main: {
30 | flex: 1,
31 | maxWidth: 960,
32 | marginHorizontal: "auto",
33 | },
34 | title: {
35 | fontSize: 64,
36 | fontWeight: "bold",
37 | },
38 | subtitle: {
39 | fontSize: 36,
40 | color: "#38434D",
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/examples/expo-router-example/patches/@expo+metro-runtime+2.2.16.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/@expo/metro-runtime/build/HMRClient.js b/node_modules/@expo/metro-runtime/build/HMRClient.js
2 | index 8ce3c59..b8332cd 100644
3 | --- a/node_modules/@expo/metro-runtime/build/HMRClient.js
4 | +++ b/node_modules/@expo/metro-runtime/build/HMRClient.js
5 | @@ -131,8 +131,8 @@ const HMRClient = {
6 | setup({ isEnabled }) {
7 | assert(!hmrClient, "Cannot initialize hmrClient twice");
8 | const serverScheme = window.location.protocol === "https:" ? "wss" : "ws";
9 | - const client = new MetroHMRClient(`${serverScheme}://${window.location.host}/hot`);
10 | - hmrClient = client;
11 | + const port = process.env.EXPO_PUBLIC_SAFARI_EXTENSION_PORT || "8081"
12 | + const client = new MetroHMRClient(`${serverScheme}://localhost:${port}/hot`); hmrClient = client;
13 | const { fullBundleUrl } = (0, getDevServer_1.default)();
14 | pendingEntryPoints.push(
15 | // HMRServer understands regular bundle URLs, so prefer that in case
16 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/src/screens/details.tsx:
--------------------------------------------------------------------------------
1 | import { RouteProp, useRoute } from "@react-navigation/native";
2 |
3 | import { View, StyleSheet, Text } from "react-native";
4 |
5 | import { RootStackParamList } from "../navigation";
6 |
7 | type DetailsSreenRouteProp = RouteProp;
8 |
9 | export default function Details() {
10 | const router = useRoute();
11 |
12 | return (
13 |
14 |
15 | Details
16 | Showing details for user {router.params.name}.
17 |
18 |
19 | );
20 |
21 | }
22 |
23 |
24 | const styles = StyleSheet.create({
25 | container: {
26 | flex: 1,
27 | padding: 24,
28 | },
29 | main: {
30 | flex: 1,
31 | maxWidth: 960,
32 | marginHorizontal: "auto",
33 | },
34 | title: {
35 | fontSize: 64,
36 | fontWeight: "bold",
37 | },
38 | subtitle: {
39 | fontSize: 36,
40 | color: "#38434D",
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/examples/expo-router-example/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Feather } from '@expo/vector-icons';
2 |
3 | import { Stack, useRouter } from 'expo-router';
4 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
5 |
6 | export default function Layout() {
7 | const router = useRouter();
8 |
9 | const BackButton = () => (
10 |
11 |
12 |
13 | Back
14 |
15 |
16 | );
17 |
18 | return (
19 |
20 |
21 | }}
24 | />
25 |
26 | );
27 | }
28 |
29 | const styles = StyleSheet.create({
30 | backButton: {
31 | flexDirection: 'row',
32 | },
33 | backButtonText: {
34 | color: '#007AFF',
35 | marginLeft: 4,
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.Safari.web-extension
9 | NSExtensionPrincipalClass
10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler
11 |
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundleDisplayName
15 | Extension
16 | CFBundleIdentifier
17 | $(PRODUCT_BUNDLE_IDENTIFIER)
18 | CFBundleVersion
19 | $(CURRENT_PROJECT_VERSION)
20 | CFBundleExecutable
21 | $(EXECUTABLE_NAME)
22 | CFBundlePackageType
23 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
24 | CFBundleShortVersionString
25 | $(MARKETING_VERSION)
26 |
27 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.Safari.web-extension
9 | NSExtensionPrincipalClass
10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler
11 |
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundleDisplayName
15 | Extension
16 | CFBundleIdentifier
17 | $(PRODUCT_BUNDLE_IDENTIFIER)
18 | CFBundleVersion
19 | $(CURRENT_PROJECT_VERSION)
20 | CFBundleExecutable
21 | $(EXECUTABLE_NAME)
22 | CFBundlePackageType
23 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
24 | CFBundleShortVersionString
25 | $(MARKETING_VERSION)
26 |
27 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.Safari.web-extension
9 | NSExtensionPrincipalClass
10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler
11 |
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundleDisplayName
15 | Extension
16 | CFBundleIdentifier
17 | $(PRODUCT_BUNDLE_IDENTIFIER)
18 | CFBundleVersion
19 | $(CURRENT_PROJECT_VERSION)
20 | CFBundleExecutable
21 | $(EXECUTABLE_NAME)
22 | CFBundlePackageType
23 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
24 | CFBundleShortVersionString
25 | $(MARKETING_VERSION)
26 |
27 |
--------------------------------------------------------------------------------
/plugin/src/withExtensionEntitlements.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withInfoPlist } from "@expo/config-plugins";
2 | import plist from "@expo/plist";
3 | import * as fs from "fs";
4 | import * as path from "path";
5 |
6 | export const withExtensionEntitlements: ConfigPlugin<{
7 | folderName: string;
8 | }> = (config, { folderName }) => {
9 | return withInfoPlist(config, (config) => {
10 | const extensionEntitlementsPath = path.join(
11 | config.modRequest.projectRoot,
12 | folderName,
13 | `${folderName}.entitlements`
14 | );
15 | const entitilementsFileExists = fs.existsSync(extensionEntitlementsPath);
16 |
17 | if (entitilementsFileExists) return config;
18 |
19 | const safariExtensionEntitlements: Record = {
20 | "com.apple.security.application-groups": [
21 | `group.${config.ios?.bundleIdentifier}`,
22 | ],
23 | };
24 |
25 | fs.mkdirSync(path.dirname(extensionEntitlementsPath), { recursive: true });
26 | fs.writeFileSync(
27 | extensionEntitlementsPath,
28 | plist.build(safariExtensionEntitlements)
29 | );
30 |
31 | return config;
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/examples/expo-router-example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "expo-router-example",
4 | "slug": "expo-router-example",
5 | "version": "1.0.0",
6 | "scheme": "expo-router-example",
7 | "web": {
8 | "bundler": "metro",
9 | "output": "static",
10 | "favicon": "./assets/favicon.png"
11 | },
12 | "plugins": [
13 | "expo-router",
14 | [
15 | "react-native-safari-extension",
16 | {
17 | "folderName": "MyExtension"
18 | }
19 | ]
20 | ],
21 | "experiments": {
22 | "typedRoutes": true
23 | },
24 | "orientation": "portrait",
25 | "icon": "./assets/icon.png",
26 | "userInterfaceStyle": "light",
27 | "splash": {
28 | "image": "./assets/splash.png",
29 | "resizeMode": "contain",
30 | "backgroundColor": "#ffffff"
31 | },
32 | "assetBundlePatterns": ["**/*"],
33 | "ios": {
34 | "supportsTablet": true,
35 | "bundleIdentifier": "com.alevy97.expo-router-example"
36 | },
37 | "android": {
38 | "adaptiveIcon": {
39 | "foregroundImage": "./assets/adaptive-icon.png",
40 | "backgroundColor": "#ffffff"
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/plugin/src/withSafariExtension.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withPlugins } from "@expo/config-plugins";
2 | import { withAppEntitlements } from "./withAppEntitlements";
3 | import { withExtensionConfig } from "./withExtensionConfig";
4 | import { withExtensionEntitlements } from "./withExtensionEntitlements";
5 | import { withExtensionInfoPlist } from "./withExtensionInfoPlist";
6 | import { withPodfile } from "./withPodfile";
7 | import { withSafariWebExtensionHandler } from "./withSafariWebExtensionHandler";
8 | import { withXcodeTarget } from "./withXcodeTarget";
9 |
10 | type PluginParams = {
11 | folderName: string;
12 | dependencies?: Record[];
13 | };
14 |
15 | const withSafariExtension: ConfigPlugin = (
16 | config,
17 | { folderName, dependencies }
18 | ) => {
19 | return withPlugins(config, [
20 | withAppEntitlements,
21 | [withExtensionEntitlements, { folderName }],
22 | [withExtensionInfoPlist, { folderName }],
23 | [withSafariWebExtensionHandler, { folderName }],
24 | [withPodfile, { folderName, dependencies }],
25 | [withExtensionConfig, { folderName }],
26 | [withXcodeTarget, { folderName }],
27 | ]);
28 | };
29 |
30 | export default withSafariExtension;
31 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids"
15 | },
16 | "dependencies": {
17 | "@docusaurus/core": "3.0.1",
18 | "@docusaurus/preset-classic": "3.0.1",
19 | "@mdx-js/react": "^3.0.0",
20 | "clsx": "^2.0.0",
21 | "prism-react-renderer": "^2.3.0",
22 | "react": "^18.0.0",
23 | "react-dom": "^18.0.0"
24 | },
25 | "devDependencies": {
26 | "@docusaurus/module-type-aliases": "3.0.1",
27 | "@docusaurus/types": "3.0.1"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.5%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 3 chrome version",
37 | "last 3 firefox version",
38 | "last 5 safari version"
39 | ]
40 | },
41 | "engines": {
42 | "node": ">=18.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/patches/@expo+metro-runtime+2.2.16.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/@expo/metro-runtime/.DS_Store b/node_modules/@expo/metro-runtime/.DS_Store
2 | new file mode 100644
3 | index 0000000..8f70268
4 | Binary files /dev/null and b/node_modules/@expo/metro-runtime/.DS_Store differ
5 | diff --git a/node_modules/@expo/metro-runtime/build/HMRClient.js b/node_modules/@expo/metro-runtime/build/HMRClient.js
6 | index 8ce3c59..9a5409e 100644
7 | --- a/node_modules/@expo/metro-runtime/build/HMRClient.js
8 | +++ b/node_modules/@expo/metro-runtime/build/HMRClient.js
9 | @@ -131,7 +131,9 @@ const HMRClient = {
10 | setup({ isEnabled }) {
11 | assert(!hmrClient, "Cannot initialize hmrClient twice");
12 | const serverScheme = window.location.protocol === "https:" ? "wss" : "ws";
13 | - const client = new MetroHMRClient(`${serverScheme}://${window.location.host}/hot`);
14 | + const port = process.env.EXPO_PUBLIC_SAFARI_EXTENSION_PORT || "8081"
15 | + const host = process.env.EXPO_PUBLIC_SAFARI_EXTENSION_HOSTNAME || "localhost"
16 | + const client = new MetroHMRClient(`${serverScheme}://${host}:${port}/hot`);
17 | hmrClient = client;
18 | const { fullBundleUrl } = (0, getDevServer_1.default)();
19 | pendingEntryPoints.push(
20 |
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/src/popup.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Extend the react-native-web reset:
3 | * https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js
4 | */
5 | html,
6 | body,
7 | #root {
8 | width: 100%;
9 | /* To smooth any scrolling behavior */
10 | -webkit-overflow-scrolling: touch;
11 | margin: 0px;
12 | padding: 0px;
13 | /* Allows content to fill the viewport and go beyond the bottom */
14 | min-height: 100%;
15 | }
16 | #root {
17 | flex-shrink: 0;
18 | flex-basis: auto;
19 | flex-grow: 1;
20 | display: flex;
21 | flex: 1;
22 | }
23 |
24 | html {
25 | scroll-behavior: smooth;
26 | /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
27 | -webkit-text-size-adjust: 100%;
28 | height: calc(100% + env(safe-area-inset-top));
29 | }
30 |
31 | body {
32 | display: flex;
33 | /* Allows you to scroll below the viewport; default value is visible */
34 | overflow-y: auto;
35 | overscroll-behavior-y: none;
36 | text-rendering: optimizeLegibility;
37 | -webkit-font-smoothing: antialiased;
38 | -moz-osx-font-smoothing: grayscale;
39 | -ms-overflow-style: scrollbar;
40 | }
41 | /* Enable for apps that support dark-theme */
42 | @media (prefers-color-scheme: dark) {
43 | body {
44 | background-color: black;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/src/popup.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Extend the react-native-web reset:
3 | * https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js
4 | */
5 | html,
6 | body,
7 | #root {
8 | width: 100%;
9 | /* To smooth any scrolling behavior */
10 | -webkit-overflow-scrolling: touch;
11 | margin: 0px;
12 | padding: 0px;
13 | /* Allows content to fill the viewport and go beyond the bottom */
14 | min-height: 100%;
15 | }
16 | #root {
17 | flex-shrink: 0;
18 | flex-basis: auto;
19 | flex-grow: 1;
20 | display: flex;
21 | flex: 1;
22 | }
23 |
24 | html {
25 | scroll-behavior: smooth;
26 | /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
27 | -webkit-text-size-adjust: 100%;
28 | height: calc(100% + env(safe-area-inset-top));
29 | }
30 |
31 | body {
32 | display: flex;
33 | /* Allows you to scroll below the viewport; default value is visible */
34 | overflow-y: auto;
35 | overscroll-behavior-y: none;
36 | text-rendering: optimizeLegibility;
37 | -webkit-font-smoothing: antialiased;
38 | -moz-osx-font-smoothing: grayscale;
39 | -ms-overflow-style: scrollbar;
40 | }
41 | /* Enable for apps that support dark-theme */
42 | @media (prefers-color-scheme: dark) {
43 | body {
44 | background-color: black;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/src/popup.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Extend the react-native-web reset:
3 | * https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js
4 | */
5 | html,
6 | body,
7 | #root {
8 | width: 100%;
9 | /* To smooth any scrolling behavior */
10 | -webkit-overflow-scrolling: touch;
11 | margin: 0px;
12 | padding: 0px;
13 | /* Allows content to fill the viewport and go beyond the bottom */
14 | min-height: 100%;
15 | }
16 | #root {
17 | flex-shrink: 0;
18 | flex-basis: auto;
19 | flex-grow: 1;
20 | display: flex;
21 | flex: 1;
22 | }
23 |
24 | html {
25 | scroll-behavior: smooth;
26 | /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
27 | -webkit-text-size-adjust: 100%;
28 | height: calc(100% + env(safe-area-inset-top));
29 | }
30 |
31 | body {
32 | display: flex;
33 | /* Allows you to scroll below the viewport; default value is visible */
34 | overflow-y: auto;
35 | overscroll-behavior-y: none;
36 | text-rendering: optimizeLegibility;
37 | -webkit-font-smoothing: antialiased;
38 | -moz-osx-font-smoothing: grayscale;
39 | -ms-overflow-style: scrollbar;
40 | }
41 | /* Enable for apps that support dark-theme */
42 | @media (prefers-color-scheme: dark) {
43 | body {
44 | background-color: black;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/basic-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic-example",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "android": "expo run:android",
6 | "format": "eslint '**/*.{js,jsx,ts,tsx}' --fix && prettier '**/*.{js,jsx,ts,tsx,json}' --write",
7 | "ios": "expo run:ios",
8 | "start": "expo start",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@react-navigation/native": "^6.1.7",
13 | "@react-navigation/stack": "^6.3.17",
14 | "expo": "~49.0.11",
15 | "expo-splash-screen": "~0.20.5",
16 | "expo-status-bar": "~1.6.0",
17 | "react": "18.2.0",
18 | "react-native": "0.72.6",
19 | "react-native-gesture-handler": "~2.12.0",
20 | "react-native-safari-extension": "^1.1.0",
21 | "react-native-safe-area-context": "^4.6.3",
22 | "react-native-screens": "~3.22.0"
23 | },
24 | "devDependencies": {
25 | "@babel/core": "^7.20.0",
26 | "@types/react": "~18.2.14",
27 | "@typescript-eslint/eslint-plugin": "^6.7.2",
28 | "@typescript-eslint/parser": "^6.7.2",
29 | "eslint": "^8.50.0",
30 | "eslint-config-universe": "^12.0.0",
31 | "prettier": "^3.0.3",
32 | "typescript": "^5.1.3"
33 | },
34 | "eslintConfig": {
35 | "extends": "universe/native"
36 | },
37 | "main": "node_modules/expo/AppEntry.js",
38 | "expo": {
39 | "install": {
40 | "exclude": [
41 | "react-native-safe-area-context"
42 | ]
43 | }
44 | },
45 | "private": true
46 | }
47 |
--------------------------------------------------------------------------------
/plugin/src/withPodfile.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withDangerousMod } from "@expo/config-plugins";
2 | import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
3 | import * as fs from "fs";
4 | import * as path from "path";
5 |
6 | export const withPodfile: ConfigPlugin<{
7 | folderName: string;
8 | dependencies?: Record[];
9 | }> = (config, { folderName, dependencies }) => {
10 | return withDangerousMod(config, [
11 | "ios",
12 | (config) => {
13 | if (!dependencies) return config;
14 | const podFilePath = path.join(
15 | config.modRequest.platformProjectRoot,
16 | "Podfile"
17 | );
18 | let podfileContent = fs.readFileSync(podFilePath).toString();
19 |
20 | const extensionTargetContents = `target '${folderName}' do
21 | ${dependencies
22 | ?.map(
23 | (dependency) =>
24 | `pod '${dependency.name}'${
25 | dependency.version ? `, '${dependency.version}'` : ""
26 | }`
27 | )
28 | .join("\n")}
29 | end`;
30 |
31 | podfileContent = mergeContents({
32 | tag: "safari-extension-target",
33 | src: podfileContent,
34 | newSrc: extensionTargetContents,
35 | anchor: /target\s+'reactnavigationexample'/,
36 | offset: -1,
37 | comment: "#",
38 | }).contents;
39 |
40 | fs.writeFileSync(podFilePath, podfileContent);
41 |
42 | return config;
43 | },
44 | ]);
45 | };
46 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/addBuildPhases.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 | import { PBXFile, quoted } from "../utils";
3 |
4 | type AddBuildPhaseParams = {
5 | groupName: string;
6 | productFile: PBXFile;
7 | targetUuid: string;
8 | };
9 |
10 | export default function addBuildPhases(
11 | proj: XcodeProject,
12 | { groupName, productFile, targetUuid }: AddBuildPhaseParams
13 | ) {
14 | const buildPath = quoted("");
15 |
16 | // Sources build phase
17 | const { uuid: sourcesBuildPhaseUuid } = proj.addBuildPhase(
18 | ["SafariWebExtensionHandler.swift"],
19 | "PBXSourcesBuildPhase",
20 | groupName,
21 | targetUuid,
22 | "app_extension",
23 | buildPath
24 | );
25 |
26 | // Copy files build phase
27 | const { uuid: copyFilesBuildPhaseUuid } = proj.addBuildPhase(
28 | [productFile.path],
29 | "PBXCopyFilesBuildPhase",
30 | groupName,
31 | proj.getFirstTarget().uuid,
32 | "app_extension",
33 | buildPath
34 | );
35 |
36 | // Frameworks build phase
37 | const { uuid: frameworksBuildPhaseUuid } = proj.addBuildPhase(
38 | [],
39 | "PBXFrameworksBuildPhase",
40 | groupName,
41 | targetUuid,
42 | "app_extension",
43 | buildPath
44 | );
45 |
46 | // Resources build phase
47 | const { uuid: resourcesBuildPhaseUuid } = proj.addBuildPhase(
48 | ["src", "assets", "manifest.json"],
49 | "PBXResourcesBuildPhase",
50 | groupName,
51 | targetUuid,
52 | "app_extension",
53 | buildPath
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/plugin/src/withSafariWebExtensionHandler.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withDangerousMod } from "@expo/config-plugins";
2 | import * as fs from "fs";
3 | import * as path from "path";
4 |
5 | export const withSafariWebExtensionHandler: ConfigPlugin<{
6 | folderName: string;
7 | }> = (config, { folderName }) => {
8 | return withDangerousMod(config, [
9 | "ios",
10 | (config) => {
11 | const extensionHandlerPath = path.join(
12 | config.modRequest.projectRoot,
13 | folderName,
14 | "SafariWebExtensionHandler.swift"
15 | );
16 | const extensionHandlerExists = fs.existsSync(extensionHandlerPath);
17 |
18 | if (!extensionHandlerExists) {
19 | fs.writeFileSync(extensionHandlerPath, extensionHandlerContent);
20 | }
21 |
22 | return config;
23 | },
24 | ]);
25 | };
26 |
27 | const extensionHandlerContent = `
28 | import SafariServices
29 | import os.log
30 |
31 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
32 | func beginRequest(with context: NSExtensionContext) {
33 | let item = context.inputItems[0] as! NSExtensionItem
34 | let message = item.userInfo?[SFExtensionMessageKey]
35 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
36 |
37 | let response = NSExtensionItem()
38 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
39 |
40 | context.completeRequest(returningItems: [response], completionHandler: nil)
41 | }
42 | }
43 |
44 | `;
45 |
--------------------------------------------------------------------------------
/examples/basic-example/src/navigation/index.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { Feather } from "@expo/vector-icons";
3 | import { NavigationContainer } from "@react-navigation/native";
4 | import { createStackNavigator } from "@react-navigation/stack";
5 |
6 | import { Text, View, StyleSheet } from "react-native";
7 |
8 |
9 | import Overview from "../screens/overview";
10 | import Details from "../screens/details";
11 |
12 | export type RootStackParamList = {
13 | Overview: undefined;
14 | Details: { name: string };
15 | };
16 |
17 | const Stack = createStackNavigator();
18 |
19 | export default function RootStack() {
20 | return (
21 |
22 |
23 |
24 | ({
29 | headerLeft: () => (
30 |
31 |
32 | Back
33 |
34 | )
35 | })}
36 |
37 | />
38 |
39 |
40 | );
41 | }
42 |
43 |
44 | const styles = StyleSheet.create({
45 | backButton: {
46 | flexDirection: "row",
47 | paddingLeft: 20,
48 | },
49 | backButtonText: {
50 | color: "#007AFF",
51 | marginLeft: 4
52 | }
53 | });
54 |
55 |
--------------------------------------------------------------------------------
/examples/expo-router-example/app/index.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'expo-router';
2 | import { Pressable, StyleSheet, Text, View } from 'react-native';
3 |
4 | export default function Page() {
5 | return (
6 |
7 |
8 |
9 | Hello World!!
10 |
11 |
12 |
13 | Press Me
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | button: {
23 | alignItems: 'center',
24 | backgroundColor: '#6366F1',
25 | borderRadius: 24,
26 | elevation: 5,
27 | flexDirection: 'row',
28 | justifyContent: 'center',
29 | padding: 16,
30 | shadowColor: '#000',
31 | shadowOffset: {
32 | height: 2,
33 | width: 0,
34 | },
35 | shadowOpacity: 0.25,
36 | shadowRadius: 3.84,
37 | },
38 | buttonText: {
39 | color: '#FFFFFF',
40 | fontSize: 16,
41 | fontWeight: '600',
42 | textAlign: 'center',
43 | },
44 | container: {
45 | flex: 1,
46 | padding: 24,
47 | },
48 | main: {
49 | flex: 1,
50 | maxWidth: 960,
51 | marginHorizontal: 'auto',
52 | justifyContent: 'space-between',
53 | },
54 | title: {
55 | fontSize: 64,
56 | fontWeight: 'bold',
57 | },
58 | subtitle: {
59 | color: '#38434D',
60 | fontSize: 36,
61 | },
62 | });
63 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/src/navigation/index.tsx:
--------------------------------------------------------------------------------
1 | import { Feather } from '@expo/vector-icons';
2 | import { NavigationContainer } from '@react-navigation/native';
3 | import { createStackNavigator } from '@react-navigation/stack';
4 |
5 | import { StyleSheet, Text, View } from 'react-native';
6 |
7 | import Details from '../screens/details';
8 | import Overview from '../screens/overview';
9 |
10 | export type RootStackParamList = {
11 | Overview: undefined;
12 | Details: { name: string };
13 | };
14 |
15 | const Stack = createStackNavigator();
16 |
17 | export default function RootStack() {
18 | return (
19 |
20 |
21 |
22 | ({
26 | headerLeft: () => (
27 |
28 |
29 |
30 | Back
31 |
32 |
33 | ),
34 | })}
35 | />
36 |
37 |
38 | );
39 | }
40 |
41 | const styles = StyleSheet.create({
42 | backButton: {
43 | flexDirection: 'row',
44 | paddingLeft: 20,
45 | },
46 | backButtonText: {
47 | color: '#007AFF',
48 | marginLeft: 4,
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/examples/basic-example/MyExtension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "default_locale": "en",
4 | "name": "My Safari Extension - Basic",
5 | "description": "Extension Description",
6 | "version": "1.0",
7 | "icons": {
8 | "48": "assets/assets/icon-48.png",
9 | "96": "assets/assets/icon-96.png",
10 | "128": "assets/assets/icon-128.png",
11 | "256": "assets/assets/icon-256.png",
12 | "512": "assets/assets/icon-512.png"
13 | },
14 | "host_permissions": ["*://localhost/*", "ws://*"],
15 | "externally_connectable": {
16 | "matches": ["http://localhost/*"]
17 | },
18 | "permissions": [
19 | "alarms",
20 | "clipboardWrite",
21 | "menus",
22 | "nativeMessaging",
23 | "storage",
24 | "cookies",
25 | "tabs",
26 | "webNavigation",
27 | "activeTab",
28 | "webRequest",
29 | "webRequestBlocking"
30 | ],
31 | "background": {
32 | "scripts": ["src/background.js"],
33 | "persistent": false
34 | },
35 | "content_scripts": [
36 | {
37 | "js": ["src/content.js"],
38 | "matches": ["*://*/*"],
39 | "match_about_blank": true,
40 | "all_frames": true,
41 | "run_at": "document_start"
42 | }
43 | ],
44 | "content_security_policy": "script-src 'nonce-e60ed1dc-fe33-11ec-b939-0242ac120002'",
45 | "browser_action": {
46 | "default_popup": "src/popup.html",
47 | "default_icon": {
48 | "16": "assets/assets/toolbar-icon-16.png",
49 | "19": "assets/assets/toolbar-icon-19.png",
50 | "32": "assets/assets/toolbar-icon-32.png",
51 | "38": "assets/assets/toolbar-icon-38.png",
52 | "48": "assets/assets/toolbar-icon-48.png",
53 | "72": "assets/assets/toolbar-icon-72.png"
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/plugin/src/withExtensionInfoPlist.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, InfoPlist, withInfoPlist } from "@expo/config-plugins";
2 | import plist from "@expo/plist";
3 | import * as fs from "fs";
4 | import * as path from "path";
5 |
6 | export const withExtensionInfoPlist: ConfigPlugin<{ folderName: string }> = (
7 | config,
8 | { folderName }
9 | ) => {
10 | return withInfoPlist(config, async (config) => {
11 | const extensionRootPath = path.join(
12 | config.modRequest.projectRoot,
13 | folderName
14 | );
15 | const infoPlistExists = fs.existsSync(
16 | path.join(extensionRootPath, "Info.plist")
17 | );
18 |
19 | if (infoPlistExists) return config;
20 |
21 | const extensionFilePath = path.join(extensionRootPath, "Info.plist");
22 |
23 | const extensionPlist: InfoPlist = {
24 | NSExtension: {
25 | NSExtensionPointIdentifier: "com.apple.Safari.web-extension",
26 | NSExtensionPrincipalClass:
27 | "$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler",
28 | },
29 | };
30 |
31 | extensionPlist.CFBundleName = "$(PRODUCT_NAME)";
32 | extensionPlist.CFBundleDisplayName = "Extension";
33 | extensionPlist.CFBundleIdentifier = "$(PRODUCT_BUNDLE_IDENTIFIER)";
34 | extensionPlist.CFBundleVersion = "$(CURRENT_PROJECT_VERSION)";
35 | extensionPlist.CFBundleExecutable = "$(EXECUTABLE_NAME)";
36 | extensionPlist.CFBundlePackageType = "$(PRODUCT_BUNDLE_PACKAGE_TYPE)";
37 | extensionPlist.CFBundleShortVersionString = "$(MARKETING_VERSION)";
38 |
39 | fs.mkdirSync(path.dirname(extensionFilePath), {
40 | recursive: true,
41 | });
42 |
43 | fs.writeFileSync(extensionFilePath, plist.build(extensionPlist));
44 |
45 | return config;
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-navigation-example",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "android": "expo run:android",
6 | "format": "eslint '**/*.{js,jsx,ts,tsx}' --fix && prettier '**/*.{js,jsx,ts,tsx,json}' --write",
7 | "ios": "expo run:ios",
8 | "start": "expo start --dev-client",
9 | "web": "expo start --web",
10 | "postinstall": "patch-package"
11 | },
12 | "dependencies": {
13 | "@expo/metro-runtime": "^2.2.16",
14 | "@react-navigation/native": "^6.1.7",
15 | "@react-navigation/stack": "^6.3.17",
16 | "expo": "~49.0.11",
17 | "expo-dev-client": "~2.4.12",
18 | "expo-splash-screen": "~0.20.5",
19 | "expo-status-bar": "~1.6.0",
20 | "react": "18.2.0",
21 | "react-dom": "18.2.0",
22 | "react-native": "0.72.6",
23 | "react-native-gesture-handler": "~2.12.0",
24 | "react-native-safari-extension": "^1.1.0",
25 | "react-native-safe-area-context": "^4.6.3",
26 | "react-native-screens": "~3.22.0",
27 | "react-native-web": "~0.19.6",
28 | "expo-notifications": "~0.20.1"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "^7.20.0",
32 | "@types/react": "~18.2.14",
33 | "@typescript-eslint/eslint-plugin": "^6.7.2",
34 | "@typescript-eslint/parser": "^6.7.2",
35 | "eslint": "^8.50.0",
36 | "eslint-config-universe": "^12.0.0",
37 | "patch-package": "^8.0.0",
38 | "prettier": "^3.0.3",
39 | "typescript": "^5.1.3"
40 | },
41 | "eslintConfig": {
42 | "extends": "universe/native"
43 | },
44 | "main": "node_modules/expo/AppEntry.js",
45 | "expo": {
46 | "install": {
47 | "exclude": [
48 | "react-native-safe-area-context"
49 | ]
50 | }
51 | },
52 | "private": true
53 | }
54 |
--------------------------------------------------------------------------------
/examples/expo-router-example/MyExtension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "default_locale": "en",
4 | "name": "𝝠 Expo Safari Extension - Expo Router",
5 | "description": "Extension Description",
6 | "version": "1.0",
7 | "icons": {
8 | "48": "assets/assets/icon-48.png",
9 | "96": "assets/assets/icon-96.png",
10 | "128": "assets/assets/icon-128.png",
11 | "256": "assets/assets/icon-256.png",
12 | "512": "assets/assets/icon-512.png"
13 | },
14 | "host_permissions": ["*://localhost/*", "ws://*"],
15 | "externally_connectable": {
16 | "matches": ["http://localhost/*"]
17 | },
18 | "permissions": [
19 | "alarms",
20 | "clipboardWrite",
21 | "menus",
22 | "nativeMessaging",
23 | "storage",
24 | "cookies",
25 | "tabs",
26 | "webNavigation",
27 | "activeTab",
28 | "webRequest",
29 | "webRequestBlocking"
30 | ],
31 | "background": {
32 | "scripts": ["src/background.js"],
33 | "persistent": false
34 | },
35 | "content_scripts": [
36 | {
37 | "js": ["src/content.js"],
38 | "matches": ["*://*/*"],
39 | "match_about_blank": true,
40 | "all_frames": true,
41 | "run_at": "document_start"
42 | }
43 | ],
44 | "content_security_policy": "script-src 'nonce-e60ed1dc-fe33-11ec-b939-0242ac120002' 'unsafe-eval'",
45 | "browser_action": {
46 | "default_popup": "src/popup.html",
47 | "default_icon": {
48 | "16": "assets/assets/toolbar-icon-16.png",
49 | "19": "assets/assets/toolbar-icon-19.png",
50 | "32": "assets/assets/toolbar-icon-32.png",
51 | "38": "assets/assets/toolbar-icon-38.png",
52 | "48": "assets/assets/toolbar-icon-48.png",
53 | "72": "assets/assets/toolbar-icon-72.png"
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/MyExtension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "default_locale": "en",
4 | "name": "𝝠 Expo Safari Extension - React Navigation",
5 | "description": "Extension Description",
6 | "version": "1.0",
7 | "icons": {
8 | "48": "assets/assets/icon-48.png",
9 | "96": "assets/assets/icon-96.png",
10 | "128": "assets/assets/icon-128.png",
11 | "256": "assets/assets/icon-256.png",
12 | "512": "assets/assets/icon-512.png"
13 | },
14 | "host_permissions": ["*://localhost/*", "ws://*"],
15 | "externally_connectable": {
16 | "matches": ["http://localhost/*"]
17 | },
18 | "permissions": [
19 | "alarms",
20 | "clipboardWrite",
21 | "menus",
22 | "nativeMessaging",
23 | "storage",
24 | "cookies",
25 | "tabs",
26 | "webNavigation",
27 | "activeTab",
28 | "webRequest",
29 | "webRequestBlocking"
30 | ],
31 | "background": {
32 | "scripts": ["src/background.js"],
33 | "persistent": false
34 | },
35 | "content_scripts": [
36 | {
37 | "js": ["src/content.js"],
38 | "matches": ["*://*/*"],
39 | "match_about_blank": true,
40 | "all_frames": true,
41 | "run_at": "document_start"
42 | }
43 | ],
44 | "content_security_policy": "script-src 'nonce-e60ed1dc-fe33-11ec-b939-0242ac120002' 'unsafe-eval'",
45 | "browser_action": {
46 | "default_popup": "src/popup.html",
47 | "default_icon": {
48 | "16": "assets/assets/toolbar-icon-16.png",
49 | "19": "assets/assets/toolbar-icon-19.png",
50 | "32": "assets/assets/toolbar-icon-32.png",
51 | "38": "assets/assets/toolbar-icon-38.png",
52 | "48": "assets/assets/toolbar-icon-48.png",
53 | "72": "assets/assets/toolbar-icon-72.png"
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/addXCConfigurationList.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | import { quoted } from "../utils";
4 |
5 | export default function (
6 | proj: XcodeProject,
7 | {
8 | extensionBundleIdentifier,
9 | currentProjectVersion,
10 | marketingVersion,
11 | extensionName,
12 | }: {
13 | extensionBundleIdentifier: string;
14 | currentProjectVersion: string;
15 | marketingVersion: string;
16 | extensionName: string;
17 | }
18 | ) {
19 | const commonBuildSettings: any = {
20 | ASSETCATALOG_COMPILER_APPICON_NAME: "AppIcon",
21 | CLANG_ENABLE_MODULES: "YES",
22 | CODE_SIGN_ENTITLEMENTS: `../${extensionName}/${extensionName}.entitlements`,
23 | CURRENT_PROJECT_VERSION: quoted(currentProjectVersion),
24 | INFOPLIST_FILE: `../${extensionName}/Info.plist`,
25 | MARKETING_VERSION: quoted(marketingVersion),
26 | PRODUCT_BUNDLE_IDENTIFIER: extensionBundleIdentifier,
27 | PRODUCT_NAME: quoted(extensionName),
28 | TARGETED_DEVICE_FAMILY: quoted("1,2"),
29 | SWIFT_VERSION: "5.0",
30 | IPHONEOS_DEPLOYMENT_TARGET: "15.0",
31 | VERSIONING_SYSTEM: "apple-generic",
32 | };
33 |
34 | const buildConfigurationsList = [
35 | {
36 | name: "Debug",
37 | isa: "XCBuildConfiguration",
38 | buildSettings: {
39 | ...commonBuildSettings,
40 | },
41 | },
42 | {
43 | name: "Release",
44 | isa: "XCBuildConfiguration",
45 | buildSettings: {
46 | ...commonBuildSettings,
47 | },
48 | },
49 | ];
50 |
51 | const xCConfigurationList = proj.addXCConfigurationList(
52 | buildConfigurationsList,
53 | "Release",
54 | `Build configuration list for PBXNativeTarget ${quoted(extensionName)} `
55 | );
56 |
57 | return xCConfigurationList;
58 | }
59 |
--------------------------------------------------------------------------------
/plugin/src/xcodeSafariExtension/xcodeSafariExtension.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 | import addBuildPhases from "./addBuildPhases";
3 | import addPbxGroup from "./addPbxGroup";
4 | import addProductFile from "./addProductFile";
5 | import addTargetDependency from "./addTargetDependency";
6 | import addToPbxNativeTargetSection from "./addToPbxNativeTargetSection";
7 | import addToPbxProjectSection from "./addToPbxProjectSection";
8 | import addXCConfigurationList from "./addXCConfigurationList";
9 |
10 | type AddXCodeTargetParmas = {
11 | extensionName: string;
12 | extensionBundleIdentifier: string;
13 | currentProjectVersion: string;
14 | marketingVersion: string;
15 | iosRoot: string;
16 | };
17 |
18 | export async function addSafariExtensionXcodeTarget(
19 | proj: XcodeProject,
20 | {
21 | extensionName,
22 | extensionBundleIdentifier,
23 | currentProjectVersion,
24 | marketingVersion,
25 | }: AddXCodeTargetParmas
26 | ) {
27 | if (proj.getFirstProject().firstProject.targets?.length > 1) return true;
28 | const targetUuid = proj.generateUuid();
29 | const groupName = "Embed Safari Extensions";
30 |
31 | const xCConfigurationList = addXCConfigurationList(proj, {
32 | extensionBundleIdentifier,
33 | currentProjectVersion,
34 | marketingVersion,
35 | extensionName,
36 | });
37 | const productFile = addProductFile(proj, extensionName, groupName);
38 | const target = addToPbxNativeTargetSection(proj, {
39 | extensionName,
40 | targetUuid,
41 | productFile,
42 | xCConfigurationList,
43 | });
44 | addToPbxProjectSection(proj, target);
45 | addTargetDependency(proj, target);
46 | addBuildPhases(proj, {
47 | groupName,
48 | productFile,
49 | targetUuid,
50 | });
51 | addPbxGroup(proj, { extensionName });
52 |
53 | return true;
54 | }
55 |
--------------------------------------------------------------------------------
/docs/docs/troubleshooting.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 7
3 | title: "Troubleshooting"
4 | ---
5 |
6 | ### Expo Router
7 |
8 | When you load the first screen in your extension, you may see an Unmatched Route error. In this case, redirect to the correct screen within a custom unmatched route screen.
9 |
10 | ### Debugging
11 |
12 | You can view your extension's settings in the iOS Settings app: _Settings > Safari > Extensions_. If you want to debug your extension, you can use Safari's Web Inspector. To enable this, open Safari, go to _Safari > Preferences > Advanced_ and check the box next to _Show Develop menu in menu bar_. Then, in the Safari menu bar, go to _Develop > Your Device Name > popup.html_.
13 |
14 | ### Using a physical device
15 |
16 | When developing on a physical device, you'll need to set the `EXPO_PUBLIC_SAFARI_EXTENSION_HOSTNAME` environment variable to your computer's IP address. `EXPO_PUBLIC_SAFARI_EXTENSION_HOSTNAME` defaults to `localhost`, which won't work on a physical device.
17 |
18 | ```
19 | EXPO_PUBLIC_SAFARI_EXTENSION_HOSTNAME=10.50.131.40
20 | EXPO_PUBLIC_SAFARI_EXTENSION_PORT=8081
21 | ```
22 |
23 | > **Note:** If you're building with EAS, set these env variables in your `eas.json` as well. See more [here](https://docs.expo.dev/build-reference/variables/).
24 |
25 | ### Using a Custom Port
26 |
27 | The default port for the development server is `8081`. If you are using a different port, you can specify the `EXPO_PUBLIC_SAFARI_EXTENSION_PORT` environment variable to use a different port.
28 |
29 | Add this to your `.env` file:
30 |
31 | ```
32 | EXPO_PUBLIC_SAFARI_EXTENSION_PORT=8082
33 | ```
34 |
35 | > **Note:** If you're building with EAS, set this env variables in your `eas.json` as well. See more [here](https://docs.expo.dev/build-reference/variables/).
36 |
37 | ### Limitations
38 |
39 | Can't use `@expo/vector-icons`
40 |
--------------------------------------------------------------------------------
/plugin/src/withExtensionConfig.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin } from "@expo/config-plugins";
2 |
3 | export const withExtensionConfig: ConfigPlugin<{
4 | folderName: string;
5 | }> = (config, { folderName }) => {
6 | if (!config.ios?.bundleIdentifier) {
7 | throw new Error("You need to specify ios.bundleIdentifier in app.json.");
8 | }
9 | const extensionBundleIdentifier = `${config.ios.bundleIdentifier}.${folderName}`;
10 |
11 | const appExtensions =
12 | config.extra?.eas?.build?.experimental?.ios?.appExtensions;
13 |
14 | const safariExtensionConfig = appExtensions?.find(
15 | (extension: any) => extension.targetName === folderName
16 | );
17 |
18 | return {
19 | ...config,
20 | extra: {
21 | ...(config.extra ?? {}),
22 | eas: {
23 | ...(config.extra?.eas ?? {}),
24 | build: {
25 | ...(config.extra?.eas?.build ?? {}),
26 | experimental: {
27 | ...(config.extra?.eas?.build?.experimental ?? {}),
28 | ios: {
29 | ...(config.extra?.eas?.build?.experimental?.ios ?? {}),
30 | appExtensions: [
31 | {
32 | ...(safariExtensionConfig ?? {
33 | targetName: folderName,
34 | bundleIdentifier: extensionBundleIdentifier,
35 | }),
36 | entitlements: {
37 | ...safariExtensionConfig?.entitlements,
38 | "com.apple.security.application-groups": [
39 | `group.${config.ios.bundleIdentifier}`,
40 | ],
41 | },
42 | },
43 | ...(appExtensions?.filter(
44 | (extension: any) => extension.targetName !== folderName
45 | ) ?? []),
46 | ],
47 | },
48 | },
49 | },
50 | },
51 | },
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-navigation-example",
4 | "slug": "react-navigation",
5 | "version": "1.0.0",
6 | "web": {
7 | "bundler": "metro",
8 | "favicon": "./assets/favicon.png"
9 | },
10 | "orientation": "portrait",
11 | "icon": "./assets/icon.png",
12 | "userInterfaceStyle": "light",
13 | "splash": {
14 | "image": "./assets/splash.png",
15 | "resizeMode": "contain",
16 | "backgroundColor": "#ffffff"
17 | },
18 | "assetBundlePatterns": ["**/*"],
19 | "ios": {
20 | "supportsTablet": true,
21 | "bundleIdentifier": "com.anonymous.react-navigation-example2"
22 | },
23 | "android": {
24 | "adaptiveIcon": {
25 | "foregroundImage": "./assets/adaptive-icon.png",
26 | "backgroundColor": "#ffffff"
27 | }
28 | },
29 | "plugins": [
30 | [
31 | "react-native-safari-extension",
32 | {
33 | "folderName": "MyExtension",
34 | "dependencies": [
35 | {
36 | "name": "Alamofire"
37 | }
38 | ]
39 | }
40 | ]
41 | ],
42 | "extra": {
43 | "eas": {
44 | "build": {
45 | "experimental": {
46 | "ios": {
47 | "appExtensions": [
48 | {
49 | "targetName": "MyExtension",
50 | "bundleIdentifier": "com.anonymous.react-navigation-example2.MyExtension",
51 | "entitlements": {
52 | "com.apple.security.application-groups": [
53 | "group.com.anonymous.react-navigation-example2"
54 | ]
55 | }
56 | }
57 | ]
58 | }
59 | }
60 | },
61 | "projectId": "ee24a0e3-f5ce-4c16-b2dd-49789b5c94ed"
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-safari-extension",
3 | "version": "1.1.0",
4 | "description": "Config plugin to add a Safari Extension to your React Native iOS app",
5 | "main": "build/index.js",
6 | "types": "build/index.d.ts",
7 | "files": [
8 | "build",
9 | "plugin/build",
10 | "app.plugin.js",
11 | "README.md"
12 | ],
13 | "scripts": {
14 | "build": "expo-module build",
15 | "build:plugin": "npx tsc --build ./plugin",
16 | "build:src": "npx tsc --build ./",
17 | "build:all": "npm run build:plugin && npm run build:src",
18 | "clean": "expo-module clean",
19 | "expo-module": "expo-module"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/andrew-levy/react-native-safari-extension.git"
24 | },
25 | "author": {
26 | "name": "Andrew Levy",
27 | "url": "https://github.com/andrew-levy"
28 | },
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/andrew-levy/react-native-safari-extension/issues"
32 | },
33 | "homepage": "https://github.com/andrew-levy/react-native-safari-extension#readme",
34 | "keywords": [
35 | "react-native",
36 | "expo",
37 | "safari-extension",
38 | "expo-config-plugin"
39 | ],
40 | "devDependencies": {
41 | "@types/fs-extra": "^9.0.13",
42 | "@types/node": "^17.0.42",
43 | "@types/react": "~18.2.21",
44 | "@types/react-native": "0.72.2",
45 | "copyfiles": "^2.4.1",
46 | "expo-module-scripts": "^3.1.0",
47 | "expo-modules-core": "^1.5.11",
48 | "prettier": "^3.0.3",
49 | "react": "18.2.0",
50 | "react-native": "0.72.4",
51 | "typescript": "^5.2.2"
52 | },
53 | "peerDependencies": {
54 | "expo": "*",
55 | "react": "*",
56 | "react-native": "*"
57 | },
58 | "sideEffects": false,
59 | "dependencies": {
60 | "@expo/config-plugins": "^6.0.1",
61 | "xcode": "^3.0.1"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/examples/basic-example/src/screens/overview.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigation } from "@react-navigation/native";
2 | import { StackNavigationProp } from "@react-navigation/stack";
3 |
4 | import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
5 |
6 | import { RootStackParamList } from "../navigation";
7 |
8 | type OverviewScreenNavigationProps = StackNavigationProp;
9 |
10 | export default function Overview() {
11 | const navigation = useNavigation();
12 |
13 | return (
14 |
15 |
16 |
17 | Hello World
18 | This is the first page of your app.
19 |
20 | navigation.navigate("Details", { name: "Dan" })}>
21 | Show Details
22 |
23 |
24 |
25 | );
26 |
27 | }
28 |
29 |
30 | const styles = StyleSheet.create({
31 | button: {
32 | alignItems: "center",
33 | backgroundColor: "#6366F1",
34 | borderRadius: 24,
35 | elevation: 5,
36 | flexDirection: "row",
37 | justifyContent: "center",
38 | padding: 16,
39 | shadowColor: "#000",
40 | shadowOffset: {
41 | height: 2,
42 | width: 0
43 | },
44 | shadowOpacity: 0.25,
45 | shadowRadius: 3.84
46 | },
47 | buttonText: {
48 | color: "#FFFFFF",
49 | fontSize: 16,
50 | fontWeight: "600",
51 | textAlign: "center",
52 | },
53 | container: {
54 | flex: 1,
55 | padding: 24,
56 | },
57 | main: {
58 | flex: 1,
59 | maxWidth: 960,
60 | marginHorizontal: "auto",
61 | justifyContent: "space-between",
62 | },
63 | title: {
64 | fontSize: 64,
65 | fontWeight: "bold",
66 | },
67 | subtitle: {
68 | color: "#38434D",
69 | fontSize: 36,
70 | }
71 | });
72 |
--------------------------------------------------------------------------------
/docs/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: /
3 | sidebar_position: 1
4 | title: "Getting Started"
5 | ---
6 |
7 | # React Native Safari Extension
8 |
9 | ## What is it?
10 |
11 | An [Expo Config Plugin](https://docs.expo.dev/config-plugins/introduction/) that allows you to add a Safari Extension to your iOS apps. This plugin allows you to manage your extension
12 | without having to open Xcode.
13 |
14 | :::info Note
15 | Not sure what Safari Extensions are? Check out [Apple's Safari Extension documentation](https://developer.apple.com/safari/extensions/) to learn more.
16 | :::
17 |
18 | ## Choose a workflow
19 |
20 | There are two workflows for using this plugin:
21 |
22 | ### 💯 [Basic Workflow](./basic.md)
23 |
24 | Build your own extension using HTML, CSS, and vanilla JavaScript.
25 |
26 | ### 🚀 [Experimental Workflow](./experimental.md)
27 |
28 | Render React Native web inside of your extension. This uses Expo web and Metro to output your React Native compononents inside of the extension popup. You can use Fast Refresh to see your changes in real time.
29 |
30 | ### Which workflow should I use?
31 |
32 | If you are building a simple extension, the Basic Workflow is probably the best option. If it's more complex, you may want to use the Experimental Workflow (it's also more fun).
33 |
34 | | Feature / Workflow | Experimental Workflow | Basic Workflow |
35 | | ------------------------------ | --------------------- | -------------- |
36 | | Manage files outside of `ios/` | ✅ | ✅ |
37 | | Expo Prebuild | ✅ | ✅ |
38 | | Fast Refresh | ✅ | |
39 | | Expo Web | ✅ | |
40 |
41 | ## Support
42 |
43 | If you find this plugin useful, consider buying me a coffee! ☕️
44 |
45 | [](https://www.buymeacoffee.com/hugemathguy)
46 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/index.js:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import Heading from '@theme/Heading';
3 | import styles from './styles.module.css';
4 |
5 | const FeatureList = [
6 | {
7 | title: 'Easy to Use',
8 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
9 | description: (
10 | <>
11 | Docusaurus was designed from the ground up to be easily installed and
12 | used to get your website up and running quickly.
13 | >
14 | ),
15 | },
16 | {
17 | title: 'Focus on What Matters',
18 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
19 | description: (
20 | <>
21 | Docusaurus lets you focus on your docs, and we'll do the chores. Go
22 | ahead and move your docs into the docs directory.
23 | >
24 | ),
25 | },
26 | {
27 | title: 'Powered by React',
28 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
29 | description: (
30 | <>
31 | Extend or customize your website layout by reusing React. Docusaurus can
32 | be extended while reusing the same header and footer.
33 | >
34 | ),
35 | },
36 | ];
37 |
38 | function Feature({Svg, title, description}) {
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
{title}
46 |
{description}
47 |
48 |
49 | );
50 | }
51 |
52 | export default function HomepageFeatures() {
53 | return (
54 |
55 |
56 |
57 | {FeatureList.map((props, idx) => (
58 |
59 | ))}
60 |
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/examples/expo-router-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expo-router-example",
3 | "version": "1.0.0",
4 | "main": "index",
5 | "scripts": {
6 | "android": "expo run:android",
7 | "format": "eslint '**/*.{js,jsx,ts,tsx}' --fix && prettier '**/*.{js,jsx,ts,tsx,json}' --write",
8 | "ios": "expo run:ios",
9 | "start": "expo start",
10 | "web": "expo start --web",
11 | "postinstall": "patch-package"
12 | },
13 | "dependencies": {
14 | "@expo/metro-runtime": "^2.2.16",
15 | "@expo/vector-icons": "^13.0.0",
16 | "@react-navigation/native": "^6.1.7",
17 | "expo": "~49.0.11",
18 | "expo-linking": "~5.0.2",
19 | "expo-router": "^2.0.0",
20 | "expo-splash-screen": "~0.20.5",
21 | "expo-status-bar": "~1.6.0",
22 | "expo-system-ui": "~2.4.0",
23 | "expo-web-browser": "~12.3.2",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "react-native": "0.72.6",
27 | "react-native-gesture-handler": "~2.12.0",
28 | "react-native-safari-extension": "^1.1.0",
29 | "react-native-safe-area-context": "^4.6.3",
30 | "react-native-screens": "~3.22.0",
31 | "react-native-web": "~0.19.6"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.20.0",
35 | "@types/react": "~18.2.14",
36 | "@typescript-eslint/eslint-plugin": "^6.7.2",
37 | "@typescript-eslint/parser": "^6.7.2",
38 | "eslint": "^8.50.0",
39 | "eslint-config-universe": "^12.0.0",
40 | "patch-package": "^8.0.0",
41 | "prettier": "^3.0.3",
42 | "typescript": "^5.1.3"
43 | },
44 | "eslintConfig": {
45 | "extends": "universe/native"
46 | },
47 | "resolutions": {
48 | "metro": "0.76.0",
49 | "metro-resolver": "0.76.0",
50 | "react-refresh": "~0.14.0"
51 | },
52 | "overrides": {
53 | "metro": "0.76.0",
54 | "metro-resolver": "0.76.0",
55 | "react-refresh": "~0.14.0"
56 | },
57 | "expo": {
58 | "install": {
59 | "exclude": [
60 | "react-native-safe-area-context"
61 | ]
62 | }
63 | },
64 | "private": true
65 | }
66 |
--------------------------------------------------------------------------------
/docs/docs/extension-files.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | title: "Extension Files"
4 | ---
5 |
6 | When using this plugin, you get to manage your extension files outside of the `ios` folder. Here's a breakdown of the necessary files:
7 |
8 | ```console
9 | MyApp/
10 | ├── app/
11 | ├── app.json
12 | ├── MyExtension/ # <-- the folder name you provided in the config
13 | │ ├── src/
14 | │ ├── assets/
15 | │ ├── Info.plist
16 | | ├── manifest.json
17 | │ └── SafariExtensionHandler.swift
18 | ├── node_modules/
19 | ├── package.json
20 | └── ...
21 | ```
22 |
23 | ### `src/`
24 |
25 | This folder contains all of your extension resource files. You can add, remove or modify any of these files to customize your extension. These files are linked closely to the `manifest.json` file, where many of the resources are referenced. **Its very important that you don't change the name of this folder. This folder is required.**
26 |
27 | ### `assets/`
28 |
29 | This folder contains all of your extension assets. If you want to use local assets that your app is using, copy your app's `assets` folder and paste it into here. So the end result should be `assets/assets/...`. Not ideal, I know, but it's necessary. **Its very important that you don't change the name of this folder. This folder is required.**
30 |
31 | ### `manifest.json`
32 |
33 | This file contains further configuration for your extension including the name, description, content scripts, entry point, permissions, etc. **This file is required.**
34 |
35 | ### `Info.plist`
36 |
37 | This file contains the configuration for your extension. **If not included, this file will be generated for you during the prebuild step.**
38 |
39 | ### `SafariExtensionHandler.swift`
40 |
41 | This file contains the native code required to run your extension. You likely won't need to modify this file. **If not included, this file will be generated for you during the prebuild step.**
42 |
43 | ### `{ExtensionName}.entitlements`
44 |
45 | This file contains the entitlements for your extension. This plugin sets up the App Groups entitlement by default so that you can message between the app and the extension. **If not included, this file will be generated for you during the prebuild step.**
46 |
--------------------------------------------------------------------------------
/examples/react-navigation-example/src/screens/overview.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigation } from '@react-navigation/native';
2 | import { StackNavigationProp } from '@react-navigation/stack';
3 |
4 | import { Image, StyleSheet, Text, View } from 'react-native';
5 | import { BorderlessButton } from 'react-native-gesture-handler';
6 | import { isSafariExtension } from 'react-native-safari-extension';
7 | import { RootStackParamList } from '../navigation';
8 |
9 | type OverviewScreenNavigationProps = StackNavigationProp;
10 |
11 | export default function Overview() {
12 | const navigation = useNavigation();
13 |
14 | return (
15 |
16 |
17 |
18 | Hello World from the
19 | {isSafariExtension() ? 'Extension' : 'App'}
20 |
21 |
22 | navigation.navigate('Details', { name: 'Dan' })}>
25 | To Details
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | const styles = StyleSheet.create({
33 | button: {
34 | alignItems: 'center',
35 | backgroundColor: '#6366F1',
36 | borderRadius: 24,
37 | elevation: 5,
38 | flexDirection: 'row',
39 | justifyContent: 'center',
40 | padding: 16,
41 | shadowColor: '#000',
42 | shadowOffset: {
43 | height: 2,
44 | width: 0,
45 | },
46 | shadowOpacity: 0.25,
47 | shadowRadius: 3.84,
48 | },
49 | buttonText: {
50 | color: '#FFFFFF',
51 | fontSize: 16,
52 | fontWeight: '600',
53 | textAlign: 'center',
54 | },
55 | container: {
56 | flex: 1,
57 | padding: 24,
58 | },
59 | main: {
60 | flex: 1,
61 | maxWidth: 960,
62 | marginHorizontal: 'auto',
63 | justifyContent: 'space-between',
64 | },
65 | title: {
66 | fontSize: 64,
67 | fontWeight: 'bold',
68 | },
69 | subtitle: {
70 | color: '#38434D',
71 | fontSize: 36,
72 | },
73 | });
74 |
--------------------------------------------------------------------------------
/docs/docs/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | title: "Basic Workflow"
4 | ---
5 |
6 | Follow these steps to get the Basic Workflow up and running.
7 |
8 | ## Install the plugin
9 |
10 | ```console
11 | npx expo install react-native-safari-extension
12 | ```
13 |
14 | ## Configure the plugin
15 |
16 | Configure the plugin in your `app.json`.
17 |
18 | - Specify a `folderName` for where your extension files will live. This folder should be in the root of your project.
19 | - Optionally define any Swift `dependencies` that you need in your extension.
20 |
21 | ```json
22 | {
23 | "expo": {
24 | "name": "myApp",
25 | "plugins": [
26 | [
27 | "react-native-safari-extension",
28 | {
29 | "folderName": "MyExtension",
30 | "dependencies": [{ "name": "SomeSwiftPackage", "version": "5.4.3" }]
31 | }
32 | ]
33 | ]
34 | }
35 | }
36 | ```
37 |
38 | ### Plugin Params
39 |
40 | ```ts
41 | {
42 | // Required: The name of the folder where your extension files live
43 | folderName: string;
44 | // Optional: Any Swift dependencies that you need in your extension
45 | dependencies?: { name: string; version?: string }[];
46 | }
47 | ```
48 |
49 | ## Add your extension files
50 |
51 | Add your extension files to a folder with the name provided above. This folder should be in the root of your project.
52 |
53 | :::important
54 | Your file structure must match the expected [Extension Files](./extension-files). It's recommended to clone this repo and copy the `MyExtension` folder from the examples to get started.
55 | :::
56 |
57 | ## Prebuild + build your app
58 |
59 | If you are using EAS to build your app, run a build using eas-cli.
60 |
61 | ```console
62 | eas build --platform ios
63 | ```
64 |
65 | Or if you're building locally:
66 |
67 | ```console
68 | npx expo prebuild -p ios --clean
69 | npx expo run:ios
70 | ```
71 |
72 | ## Developing your app
73 |
74 | Once the app has successfully run, open the Safari app, navigate to any webpage, and press the `AA` button in the address bar. This will open a context menu. Select `Manage Extensions` and enable your extension by switching the toggle on. You should now see your extension as an option in the context menu below `Manage Extensions`. Click on your extension to open it.
75 |
76 | Whenever you make a change to your extension files, you will need to rebuild your app.
77 |
--------------------------------------------------------------------------------
/docs/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // `@type` JSDoc annotations allow editor autocompletion and type checking
3 | // (when paired with `@ts-check`).
4 | // There are various equivalent ways to declare your Docusaurus config.
5 | // See: https://docusaurus.io/docs/api/docusaurus-config
6 |
7 | import { themes as prismThemes } from "prism-react-renderer";
8 |
9 | /** @type {import('@docusaurus/types').Config} */
10 | const config = {
11 | title: "React Native Safari Extension",
12 | tagline: "Dinosaurs are cool",
13 | favicon: "img/favicon.ico",
14 |
15 | // Set the production url of your site here
16 | url: "https://your-docusaurus-site.example.com",
17 | // Set the // pathname under which your site is served
18 | // For GitHub pages deployment, it is often '//'
19 | baseUrl: "/",
20 |
21 | // GitHub pages deployment config.
22 | // If you aren't using GitHub pages, you don't need these.
23 | organizationName: "andrew-levy", // Usually your GitHub org/user name.
24 | projectName: "react-native-safari-extension", // Usually your repo name.
25 |
26 | onBrokenLinks: "throw",
27 | onBrokenMarkdownLinks: "warn",
28 |
29 | // Even if you don't use internationalization, you can use this field to set
30 | // useful metadata like html lang. For example, if your site is Chinese, you
31 | // may want to replace "en" with "zh-Hans".
32 | i18n: {
33 | defaultLocale: "en",
34 | locales: ["en"],
35 | },
36 |
37 | presets: [
38 | [
39 | "classic",
40 | /** @type {import('@docusaurus/preset-classic').Options} */
41 | ({
42 | docs: {
43 | routeBasePath: "/",
44 | sidebarPath: "./sidebars.js",
45 | },
46 | theme: {
47 | customCss: "./src/css/custom.css",
48 | },
49 | }),
50 | ],
51 | ],
52 |
53 | themeConfig:
54 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
55 | ({
56 | // Replace with your project's social card
57 | image: "img/docusaurus-social-card.jpg",
58 | navbar: {
59 | title: "React Native Safari Extension",
60 | logo: {
61 | alt: "Docs Logo",
62 | src: "img/safari.png",
63 | },
64 | items: [
65 | {
66 | href: "https://github.com/andrew-levy/react-native-safari-extension",
67 | label: "GitHub",
68 | position: "right",
69 | },
70 | ],
71 | },
72 | footer: {
73 | style: "dark",
74 | copyright: `Copyright © ${new Date().getFullYear()} Built with Docusaurus.`,
75 | },
76 | prism: {
77 | theme: prismThemes.github,
78 | darkTheme: prismThemes.dracula,
79 | },
80 | }),
81 | };
82 |
83 | export default config;
84 |
--------------------------------------------------------------------------------
/docs/docs/experimental.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | title: "Experimental Workflow"
4 | ---
5 |
6 | Follow these steps to get the Experimental Workflow up and running. It's called experimental for a reason, so proceed with caution!
7 |
8 | ## Install the plugin and dependencies
9 |
10 | ```console
11 | npx expo install react-native-safari-extension react-native-web@~0.19.6 react-dom@18.2.0
12 | ```
13 |
14 | ## Configure the plugin
15 |
16 | Configure the plugin in your `app.json`.
17 |
18 | - Specify a `folderName` for where your extension files will live. This folder should be in the root of your project.
19 | - Optionally define any Swift `dependencies` that you need in your extension.
20 | - Make sure you have `expo.web.bundler` set to `"metro"`.
21 |
22 | ```json
23 | {
24 | "expo": {
25 | "name": "myApp",
26 | "plugins": [
27 | [
28 | "react-native-safari-extension",
29 | {
30 | "folderName": "MyExtension",
31 | "dependencies": [{ "name": "SomeSwiftPackage", "version": "5.4.3" }]
32 | }
33 | ]
34 | ],
35 | "web": {
36 | "bundler": "metro"
37 | }
38 | }
39 | }
40 | ```
41 |
42 | ### Plugin Params
43 |
44 | ```ts
45 | {
46 | // Required: The name of the folder where your extension files live
47 | folderName: string;
48 | // Optional: Any Swift dependencies that you need in your extension
49 | dependencies?: { name: string; version?: string }[];
50 | }
51 | ```
52 |
53 | ## Add your extension files
54 |
55 | Add your extension files to a folder with the name provided above This folder should be in the root of your project.
56 |
57 | :::important
58 | Your file structure must match the expected [Extension Files](./extension-files). It's recommended to clone this repo and copy the `MyExtension` folder from the examples to get started.
59 | :::
60 |
61 | ## Setup Fast Refresh
62 |
63 | Fast Refresh allows you to see your changes immediately without having to rebuild your app. To enable Fast Refresh, you'll need to patch `@expo/metro-runtime`.
64 |
65 | 1. First, install `@expo/metro-runtime` and `patch-package`:
66 |
67 | ```console
68 | npm install @expo/metro-runtime
69 | npm install patch-package -D
70 | ```
71 |
72 | 2. Then, add this to your `package.json`:
73 |
74 | ```json
75 | "scripts": {
76 | "postinstall": "patch-package"
77 | }
78 | ```
79 |
80 | 3. Next, open `node_modules/@expo/metro-runtime/build/HMRClient.js` and make this change:
81 |
82 | ```diff
83 | - const client = new MetroHMRClient(`${serverScheme}://${window.location.host}/hot`);
84 | + const host = process.env.EXPO_PUBLIC_SAFARI_EXTENSION_HOSTNAME || "localhost"
85 | + const port = process.env.EXPO_PUBLIC_SAFARI_EXTENSION_PORT || "8081"
86 | + const client = new MetroHMRClient(`${serverScheme}://${host}:${port}/hot`);
87 | ```
88 |
89 | > **Note:** See [Using a Physical Device](#using-a-physical-device) and [Using a Custom Port](#using-a-custom-port) for more info on how to customize this.
90 |
91 | 4. Next, patch the package with:
92 |
93 | ```console
94 | npx patch-package @expo/metro-runtime
95 | npm install
96 | ```
97 |
98 | 5. If you're using Expo Router, skip this step. If you're not using Expo Router, import `@expo/metro-runtime` in your `App.tsx` file as early as possible:
99 |
100 | ```tsx
101 | import "@expo/metro-runtime";
102 | ```
103 |
104 | 6. Lastly, in the in your extension's `/src/popup.html` file, ensure that the development script tag is pointing to your development server and is uncommented.
105 |
106 | ```html
107 |
108 |
112 |
113 |
114 |
118 | ```
119 |
120 | > **Note:** If you're using a phsyical device, make sure to update the `src` to point to your computer's IP address instead of `localhost`. Make sure to also update the port if you're using a port other than `8081`.
121 |
122 | ## Prebuild + build your app
123 |
124 | If you are using EAS to build your app, run a build using eas-cli.
125 |
126 | ```console
127 | eas build --platform ios
128 | ```
129 |
130 | Or if you're building locally:
131 |
132 | ```console
133 | npx expo prebuild -p ios
134 | npx expo run:ios
135 | ```
136 |
137 | Now you're ready to view your extension! Once the app has successfully run, open the Safari app, navigate to any webpage, and press the `AA` button in the address bar. This will open a context menu. Select `Manage Extensions` and enable your extension by switching the toggle on. You should now see your extension as an option in the context menu below `Manage Extensions`. Click on your extension to open it.
138 |
139 | ## Setup for production
140 |
141 | Before publishing your app, there are a few things you'll need to do:
142 |
143 | 1. Create a static web build for your app: `npx expo export --platform web`
144 | 2. Copy the generated `dist` folder and paste it into your extension's `src` folder.
145 | 3. In your extension's `popup.html` file, uncomment the production script tag and comment out the development script tag. Update the `src` to point to your bundle file generated in step 2.
146 | 4. Re-build your app
147 |
148 | ```html
149 |
154 | ```
155 |
156 | ## Assets
157 |
158 | To load local assets in your extension, you'll need to create an `assets/assets/` folder in the root of your extension files. The reason for the terrible folder structure is because in expo web apps, local assets are found at `http://localhost:8081/assets/assets/image.png`, so we need to mimic that in our extension.
159 |
--------------------------------------------------------------------------------
/docs/static/img/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/static/img/undraw_docusaurus_tree.svg:
--------------------------------------------------------------------------------
1 |
41 |
--------------------------------------------------------------------------------
/docs/static/img/undraw_docusaurus_mountain.svg:
--------------------------------------------------------------------------------
1 |
172 |
--------------------------------------------------------------------------------
/docs/static/img/undraw_docusaurus_react.svg:
--------------------------------------------------------------------------------
1 |
171 |
--------------------------------------------------------------------------------