├── .npmignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── npm-publish.yml ├── app.plugin.js ├── src ├── isClip │ ├── index.ts │ ├── types.ts │ ├── isClip.ts │ ├── isClip.clip.ts │ └── isClip.ios.ts ├── getBundleIdentifier │ ├── types.ts │ ├── index.ts │ ├── getBundleIdentifier.ts │ └── getBundleIdentifier.ios.ts ├── ReactNativeAppClipModule │ ├── index.ts │ ├── ReactNativeAppClipModule.ios.ts │ ├── types.ts │ └── ReactNativeAppClipModule.ts └── index.ts ├── example ├── bun.lockb ├── assets │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ └── adaptive-icon.png ├── babel.config.js ├── tsconfig.json ├── .gitignore ├── webpack.config.js ├── eas.json ├── App.tsx ├── package.json ├── app.json └── metro.config.js ├── .eslintrc.js ├── expo-module.config.json ├── plugin ├── tsconfig.json └── src │ ├── xcode │ ├── addTargetDependency.ts │ ├── addToPbxProjectSection.ts │ ├── addProductFile.ts │ ├── addToPbxNativeTargetSection.ts │ ├── addXCConfigurationList.ts │ ├── addPbxGroup.ts │ └── addBuildPhases.ts │ ├── cliPlugin.ts │ ├── withEntitlements.ts │ ├── lib │ └── getAppClipEntitlements.ts │ ├── withXcode.ts │ ├── withConfig.ts │ ├── index.ts │ ├── withPlist.ts │ └── withPodfile.ts ├── tsconfig.json ├── .gitignore ├── ios ├── ReactNativeAppClip.podspec └── ReactNativeAppClipModule.swift ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | .github/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bndkt] 2 | -------------------------------------------------------------------------------- /app.plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./plugin/build"); 2 | -------------------------------------------------------------------------------- /src/isClip/index.ts: -------------------------------------------------------------------------------- 1 | import isClip from "./isClip"; 2 | export default isClip; 3 | -------------------------------------------------------------------------------- /src/isClip/types.ts: -------------------------------------------------------------------------------- 1 | export type IsClipFn = (bundleIdSuffix?: string) => boolean; 2 | -------------------------------------------------------------------------------- /src/getBundleIdentifier/types.ts: -------------------------------------------------------------------------------- 1 | export type GetBundleIdentiferFn = () => string | null; 2 | -------------------------------------------------------------------------------- /example/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bndkt/react-native-app-clip/HEAD/example/bun.lockb -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bndkt/react-native-app-clip/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bndkt/react-native-app-clip/HEAD/example/assets/splash.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bndkt/react-native-app-clip/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bndkt/react-native-app-clip/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | schedule: 5 | interval: "weekly" 6 | -------------------------------------------------------------------------------- /src/getBundleIdentifier/index.ts: -------------------------------------------------------------------------------- 1 | import getBundleIdentifier from "./getBundleIdentifier"; 2 | export default getBundleIdentifier; 3 | -------------------------------------------------------------------------------- /expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["ios"], 3 | "ios": { 4 | "modules": ["ReactNativeAppClipModule"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/ReactNativeAppClipModule/index.ts: -------------------------------------------------------------------------------- 1 | import ReactNativeAppClipModule from "./ReactNativeAppClipModule"; 2 | export default ReactNativeAppClipModule; 3 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /src/isClip/isClip.ts: -------------------------------------------------------------------------------- 1 | import { IsClipFn } from "./types"; 2 | 3 | const isClip: IsClipFn = () => { 4 | return false; 5 | }; 6 | export default isClip; 7 | -------------------------------------------------------------------------------- /src/getBundleIdentifier/getBundleIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { GetBundleIdentiferFn } from "./types"; 2 | 3 | const getBundleIdentifier: GetBundleIdentiferFn = () => { 4 | return null; 5 | }; 6 | export default getBundleIdentifier; 7 | -------------------------------------------------------------------------------- /plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo-module-scripts/tsconfig.plugin", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": "src" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "react-native-app-clip": ["../src/index"], 7 | "react-native-app-clip/*": ["../src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /src/isClip/isClip.clip.ts: -------------------------------------------------------------------------------- 1 | import { IsClipFn } from "./types"; 2 | 3 | /** 4 | * If this file is bundled in, that means that the `.clip` extension is configured and the current build target is an app clip. 5 | * 6 | * This is more reliable than using the bundle identifier. 7 | */ 8 | const isClip: IsClipFn = () => { 9 | return true; 10 | }; 11 | export default isClip; 12 | -------------------------------------------------------------------------------- /src/isClip/isClip.ios.ts: -------------------------------------------------------------------------------- 1 | import getBundleIdentifier from "../getBundleIdentifier"; 2 | import { IsClipFn } from "./types"; 3 | 4 | const isClip: IsClipFn = (bundleIdSuffix = "Clip") => { 5 | const bundleIdentifier = getBundleIdentifier(); 6 | const isClip = 7 | bundleIdentifier?.slice(bundleIdentifier.lastIndexOf(".") + 1) === 8 | bundleIdSuffix; 9 | return isClip; 10 | }; 11 | export default isClip; 12 | -------------------------------------------------------------------------------- /src/ReactNativeAppClipModule/ReactNativeAppClipModule.ios.ts: -------------------------------------------------------------------------------- 1 | import { requireNativeModule } from "expo-modules-core"; 2 | import { ReactNativeAppClipModuleType } from "./types"; 3 | 4 | // It loads the native module object from the JSI or falls back to 5 | // the bridge module (from NativeModulesProxy) if the remote debugger is on. 6 | export default requireNativeModule( 7 | "ReactNativeAppClip", 8 | ); 9 | -------------------------------------------------------------------------------- /src/ReactNativeAppClipModule/types.ts: -------------------------------------------------------------------------------- 1 | import { GetBundleIdentiferFn } from "../getBundleIdentifier/types"; 2 | 3 | export interface ReactNativeAppClipModuleType { 4 | getContainerURL: (groupIdentifier: string) => string | null; 5 | getBundleIdentifier: GetBundleIdentiferFn; 6 | displayOverlay: () => void; 7 | setSharedCredential: (groupIdentifier: string, credential: string) => void; 8 | getSharedCredential: (groupIdentifier: string) => string | null; 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | CI: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | - name: Setup Bun 15 | uses: oven-sh/setup-bun@v1 16 | - name: Install dependencies 17 | run: bun install 18 | - name: Build react-native-app-clip 19 | run: bun run build 20 | -------------------------------------------------------------------------------- /src/ReactNativeAppClipModule/ReactNativeAppClipModule.ts: -------------------------------------------------------------------------------- 1 | import { ReactNativeAppClipModuleType } from "./types"; 2 | 3 | const ReactNativeAppClipModule: ReactNativeAppClipModuleType = { 4 | getContainerURL: (groupIdentifier: string) => { 5 | return null; 6 | }, 7 | getBundleIdentifier: () => { 8 | return null; 9 | }, 10 | displayOverlay: () => {}, 11 | setSharedCredential: () => {}, 12 | getSharedCredential: () => { 13 | return null; 14 | }, 15 | }; 16 | export default ReactNativeAppClipModule; 17 | -------------------------------------------------------------------------------- /src/getBundleIdentifier/getBundleIdentifier.ios.ts: -------------------------------------------------------------------------------- 1 | import ReactNativeAppClipModule from "../ReactNativeAppClipModule"; 2 | import { GetBundleIdentiferFn } from "./types"; 3 | 4 | let bundleIdentifier: string | null = null; 5 | const getBundleIdentifier: GetBundleIdentiferFn = () => { 6 | // Bundle identifier doesn't change during runtime 7 | if (bundleIdentifier) return bundleIdentifier; 8 | bundleIdentifier = ReactNativeAppClipModule.getBundleIdentifier(); 9 | return bundleIdentifier; 10 | }; 11 | export default getBundleIdentifier; 12 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | ios/ 38 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const createConfigAsync = require('@expo/webpack-config'); 2 | const path = require('path'); 3 | 4 | module.exports = async (env, argv) => { 5 | const config = await createConfigAsync( 6 | { 7 | ...env, 8 | babel: { 9 | dangerouslyAddModulePathsToTranspile: ['react-native-app-clip'], 10 | }, 11 | }, 12 | argv 13 | ); 14 | config.resolve.modules = [ 15 | path.resolve(__dirname, './node_modules'), 16 | path.resolve(__dirname, '../node_modules'), 17 | ]; 18 | 19 | return config; 20 | }; 21 | -------------------------------------------------------------------------------- /example/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 12.3.0", 4 | "appVersionSource": "remote" 5 | }, 6 | "build": { 7 | "simulator": { 8 | "developmentClient": true, 9 | "distribution": "internal", 10 | "ios": { 11 | "simulator": true 12 | } 13 | }, 14 | "development": { 15 | "developmentClient": true, 16 | "distribution": "internal" 17 | }, 18 | "preview": { 19 | "distribution": "internal" 20 | }, 21 | "production": {} 22 | }, 23 | "submit": { 24 | "production": {} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugin/src/xcode/addTargetDependency.ts: -------------------------------------------------------------------------------- 1 | import type { XcodeProject } from "expo/config-plugins"; 2 | 3 | export function addTargetDependency( 4 | xcodeProject: XcodeProject, 5 | target: { uuid: string }, 6 | ) { 7 | if (!xcodeProject.hash.project.objects.PBXTargetDependency) { 8 | xcodeProject.hash.project.objects.PBXTargetDependency = {}; 9 | } 10 | if (!xcodeProject.hash.project.objects.PBXContainerItemProxy) { 11 | xcodeProject.hash.project.objects.PBXContainerItemProxy = {}; 12 | } 13 | 14 | xcodeProject.addTargetDependency(xcodeProject.getFirstTarget().uuid, [ 15 | target.uuid, 16 | ]); 17 | } 18 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, View, Button } from "react-native"; 2 | import * as ReactNativeAppClip from "react-native-app-clip"; 3 | 4 | export default function App() { 5 | return ( 6 | 7 | {ReactNativeAppClip.isClip() ? "App Clip" : "Full App"} 8 | {ReactNativeAppClip.isClip() ? ( 9 |