├── 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 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](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 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](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 | 2 | Focus on What Matters 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/static/img/undraw_docusaurus_mountain.svg: -------------------------------------------------------------------------------- 1 | 2 | Easy to Use 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /docs/static/img/undraw_docusaurus_react.svg: -------------------------------------------------------------------------------- 1 | 2 | Powered by React 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | --------------------------------------------------------------------------------