├── .changeset ├── README.md ├── config.json └── flat-melons-matter.md ├── .eslintignore ├── .eslintrc.json ├── .github ├── actions │ ├── github-config │ │ └── action.yml │ ├── prepare-node │ │ └── action.yml │ ├── prepare-packages │ │ └── action.yml │ ├── publish │ │ └── action.yml │ └── release │ │ └── action.yml └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── README.md ├── example ├── example.js └── index.html ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── index.d.ts └── index.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/flat-melons-matter.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@macpaw/browser-deeplink": patch 3 | --- 4 | 5 | Patch changes to trigger release 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@macpaw/eslint-config-base", 3 | "rules": { 4 | "import/prefer-default-export": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/actions/github-config/action.yml: -------------------------------------------------------------------------------- 1 | name: 'github config' 2 | description: 'Update GIT config with signing key' 3 | inputs: 4 | gpg-key-base64: 5 | description: 'Base64 GPG key' 6 | required: true 7 | runs: 8 | using: "composite" 9 | steps: 10 | - run: | 11 | mkdir -p ${GITHUB_WORKSPACE}/.gpg 12 | echo ${{ inputs.gpg-key-base64 }} | base64 -d > ${GITHUB_WORKSPACE}/.gpg/private.key 13 | gpg --import ${GITHUB_WORKSPACE}/.gpg/private.key 14 | 15 | git config --global user.signingkey 16 | git config --global commit.gpgsign true 17 | git config user.name ci-macpaw 18 | git config user.email admin+ci-gh@macpaw.com 19 | shell: bash 20 | name: Update git config 21 | -------------------------------------------------------------------------------- /.github/actions/prepare-node/action.yml: -------------------------------------------------------------------------------- 1 | name: "Prepare Node" 2 | description: "Sets up Node.js and runs install if it's needed" 3 | inputs: 4 | node-version: 5 | description: 'The version of Node.js to use' 6 | required: true 7 | node-cache: 8 | description: 'The cache key to use for caching Node.js' 9 | required: false 10 | package-manager: 11 | description: 'The package manager to use' 12 | required: false 13 | default: 'npm' 14 | registry-url: 15 | description: 'The registry URL to use' 16 | required: false 17 | scope: 18 | description: 'The scope to use' 19 | required: false 20 | default: '' 21 | install-dependencies: 22 | description: 'Whether to install dependencies' 23 | required: false 24 | default: true 25 | 26 | runs: 27 | using: composite 28 | steps: 29 | - name: Install pnpm 30 | if: ${{ inputs.package-manager == 'pnpm' }} 31 | uses: pnpm/action-setup@v2.2.4 32 | with: 33 | version: 8 34 | run_install: false 35 | 36 | - name: Setup Node version 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: ${{ inputs.node-version }} 40 | cache: ${{ inputs.node-cache }} 41 | registry-url: ${{ inputs.registry-url }} 42 | scope: ${{ inputs.scope }} 43 | 44 | - name: Install dependencies 45 | shell: bash 46 | if: ${{ inputs.install-dependencies == 'true' }} 47 | run: ${{ inputs.package-manager }} install 48 | -------------------------------------------------------------------------------- /.github/actions/prepare-packages/action.yml: -------------------------------------------------------------------------------- 1 | name: "Prepare packages" 2 | description: "Builds and packs the packages for release" 3 | inputs: 4 | node-version: 5 | description: 'The version of Node.js to use' 6 | required: true 7 | node-cache: 8 | description: 'The cache key to use for caching Node.js' 9 | required: false 10 | package-manager: 11 | description: 'The package manager to use' 12 | required: false 13 | default: 'npm' 14 | registry-url: 15 | description: 'The registry URL to use' 16 | required: false 17 | artifact-name: 18 | description: 'The name of the artifact to upload' 19 | required: false 20 | default: 'package-artifact' 21 | artifact-retention-days: 22 | description: 'The number of days to retain the artifact' 23 | required: false 24 | default: 1 25 | build-command: 26 | description: 'The command to use to build the package' 27 | required: false 28 | default: 'build' 29 | 30 | runs: 31 | using: composite 32 | steps: 33 | - name: Prepare node 34 | uses: ./.github/actions/prepare-node 35 | id: prepare-node 36 | with: 37 | node-version: ${{ inputs.node-version }} 38 | node-cache: ${{ inputs.node-cache }} 39 | package-manager: ${{ inputs.package-manager }} 40 | registry-url: ${{ inputs.registry-url }} 41 | 42 | - name: Install dependencies 43 | shell: bash 44 | run: ${{ inputs.package-manager }} install 45 | 46 | - name: Build 47 | shell: bash 48 | run: | 49 | if [ "${{ inputs.package-manager }}" = "npm" ]; then 50 | npm run ${{ inputs.build-command }} 51 | else 52 | ${{ inputs.package-manager }} ${{ inputs.build-command }} 53 | fi 54 | 55 | - name: Pack artifact 56 | shell: bash 57 | run: tar -czf /tmp/artifact.tar.gz . 58 | 59 | - name: Upload artifact 60 | uses: actions/upload-artifact@v3 61 | with: 62 | name: ${{ inputs.artifact-name }} 63 | path: /tmp/artifact.tar.gz 64 | retention-days: ${{ inputs.artifact-retention-days }} 65 | -------------------------------------------------------------------------------- /.github/actions/publish/action.yml: -------------------------------------------------------------------------------- 1 | name: "Publish package to registry" 2 | description: "Publishes the package to the registry" 3 | inputs: 4 | node-version: 5 | description: 'The version of Node.js to use' 6 | required: true 7 | node-cache: 8 | description: 'The cache key to use for caching Node.js' 9 | required: false 10 | package-manager: 11 | description: 'The package manager to use' 12 | required: false 13 | default: 'npm' 14 | registry-url: 15 | description: 'The registry URL to use' 16 | required: false 17 | artifact-name: 18 | description: 'The name of the artifact to download' 19 | required: false 20 | default: 'package-artifact' 21 | scope: 22 | description: 'The scope to use' 23 | required: false 24 | default: '' 25 | auth-token: 26 | description: 'The auth token to use' 27 | required: true 28 | use-public-flag: 29 | description: 'Whether to use the public flag' 30 | required: false 31 | default: false 32 | 33 | runs: 34 | using: composite 35 | steps: 36 | - name: Prepare node 37 | uses: ./.github/actions/prepare-node 38 | with: 39 | node-version: ${{ inputs.node-version }} 40 | cache: ${{ inputs.node-cache }} 41 | registry-url: ${{ inputs.registry-url }} 42 | install-dependencies: false 43 | scope: ${{ inputs.scope }} 44 | 45 | - name: Download artifact 46 | uses: actions/download-artifact@v3 47 | with: 48 | name: ${{ inputs.artifact-name }} 49 | 50 | - name: Unpack artifact 51 | shell: bash 52 | run: tar xf artifact.tar.gz 53 | 54 | - name: Publish 55 | shell: bash 56 | run: | 57 | if [ "${{ inputs.use-public-flag }}" = "true" ]; then 58 | npm publish --access public 59 | else 60 | npm publish 61 | fi 62 | env: 63 | NODE_AUTH_TOKEN: ${{ inputs.auth-token }} 64 | -------------------------------------------------------------------------------- /.github/actions/release/action.yml: -------------------------------------------------------------------------------- 1 | name: "Release package action" 2 | description: "Matches changesets if they are in the release branch, creates a release PR with version updates, otherwise if there are versions update, it will create a release." 3 | inputs: 4 | node-version: 5 | description: 'The version of Node.js to use' 6 | required: true 7 | node-cache: 8 | description: 'The cache key to use for caching Node.js' 9 | required: false 10 | package-manager: 11 | description: 'The package manager to use' 12 | required: false 13 | default: 'npm' 14 | registry-url: 15 | description: 'The registry URL to use' 16 | required: false 17 | release-pr-title: 18 | description: 'The title of the release PR' 19 | required: false 20 | default: 'ci(changesets): :package: version packages' 21 | release-commit-message: 22 | description: 'The commit message of the release PR' 23 | required: false 24 | default: 'ci(changesets): version packages' 25 | github-token: 26 | description: 'The github token to use' 27 | required: true 28 | release-command: 29 | description: 'The command to use to release' 30 | required: false 31 | default: 'release' 32 | gpg-key-base64: 33 | description: 'The base64 encoded GPG key to use' 34 | required: true 35 | outputs: 36 | release-ready: 37 | description: "Random number" 38 | value: ${{ steps.output-generator.outputs.release-ready }} 39 | runs: 40 | using: composite 41 | steps: 42 | - name: Configure git user 43 | uses: ./.github/actions/github-config 44 | with: 45 | gpg-key-base64: ${{ inputs.gpg-key-base64 }} 46 | 47 | - name: Prepare node 48 | uses: ./.github/actions/prepare-node 49 | id: prepare-node 50 | with: 51 | node-version: ${{ inputs.node-version }} 52 | node-cache: ${{ inputs.node-cache }} 53 | package-manager: ${{ inputs.package-manager }} 54 | registry-url: ${{ inputs.registry-url }} 55 | 56 | - name: Create Release 57 | id: changesets 58 | uses: changesets/action@v1 59 | with: 60 | publish: ${{ inputs.package-manager == 'npm' && format('npm run {0}', inputs.release-command) || format('{0} {1}', inputs.package-manager, inputs.release-command) }} 61 | title: ${{ inputs.release-pr-title }} 62 | commit: ${{ inputs.release-commit-message }} 63 | setupGitUser: false 64 | env: 65 | GITHUB_TOKEN: ${{ inputs.github-token }} 66 | 67 | - name: Generate outputs 68 | shell: bash 69 | id: output-generator 70 | if: steps.changesets.outputs.published == 'true' 71 | run: echo "release-ready=true" >> $GITHUB_OUTPUT 72 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | 15 | - name: eslint 16 | run: | 17 | npm i 18 | npm run lint 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | 8 | jobs: 9 | release: 10 | name: Create release 11 | runs-on: ubuntu-latest 12 | continue-on-error: false 13 | outputs: 14 | releaseReady: ${{ steps.releaseOutputs.outputs.releaseReady }} 15 | steps: 16 | - name: Cancel previous jobs 17 | uses: styfle/cancel-workflow-action@0.11.0 18 | 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Make a release if needed 23 | uses: ./.github/actions/release 24 | id: release 25 | with: 26 | node-version: 16 27 | release-pr-title: 'chore(release): :package: version update for packages' 28 | release-commit-message: 'chore(release): version update for packages' 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | release-command: 'changes:release' 31 | gpg-key-base64: ${{ secrets.CI_GITHUB_GPG_KEY_BASE64 }} 32 | 33 | - name: Generate outputs 34 | id: releaseOutputs 35 | if: steps.release.outputs.release-ready == 'true' 36 | run: echo "releaseReady=true" >> $GITHUB_OUTPUT 37 | 38 | prepare: 39 | name: Prepare packages 40 | runs-on: ubuntu-latest 41 | continue-on-error: false 42 | needs: release 43 | if: ${{ needs.release.outputs.releaseReady == 'true' }} 44 | steps: 45 | - name: Cancel previous jobs 46 | uses: styfle/cancel-workflow-action@0.11.0 47 | 48 | - name: Checkout 49 | uses: actions/checkout@v3 50 | 51 | - name: Prepare 52 | uses: ./.github/actions/prepare-packages 53 | with: 54 | node-version: 16 55 | build-command: 'build' 56 | 57 | publish-npm: 58 | name: Publish to NPM Registry 59 | needs: prepare 60 | runs-on: ubuntu-latest 61 | continue-on-error: false 62 | steps: 63 | - name: Cancel previous jobs 64 | uses: styfle/cancel-workflow-action@0.11.0 65 | 66 | - name: Checkout 67 | uses: actions/checkout@v3 68 | 69 | - name: Publish to NPM 70 | uses: ./.github/actions/publish 71 | with: 72 | node-version: 16 73 | registry-url: 'https://registry.npmjs.org/' 74 | artifact-name: 'package-artifact' 75 | scope: '@macpaw' 76 | auth-token: ${{ secrets.NPM_TOKEN }} 77 | 78 | publish-github: 79 | name: Publish to Github Registry 80 | needs: prepare 81 | runs-on: ubuntu-latest 82 | continue-on-error: false 83 | steps: 84 | - name: Cancel previous jobs 85 | uses: styfle/cancel-workflow-action@0.11.0 86 | 87 | - name: Checkout 88 | uses: actions/checkout@v3 89 | 90 | - name: Publish to NPM 91 | uses: ./.github/actions/publish 92 | with: 93 | node-version: 16 94 | registry-url: https://npm.pkg.github.com/ 95 | artifact-name: 'package-artifact' 96 | scope: '@macpaw' 97 | auth-token: ${{ secrets.GITHUB_TOKEN }} 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gpg 2 | .idea 3 | .DS_Store 4 | node_modules/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browser-deeplink 2 | 3 | Tiny function to open application from browser. Resolves if the application was requested to open. 4 | 5 | ```js 6 | import { browserDeeplink } from '@macpaw/browser-deeplink'; 7 | 8 | browserDeeplink('cleanmymac://signin?email=1%401.com&src=dashboard') 9 | .then(() => { 10 | console.log('application is requested to open'); 11 | }) 12 | .catch(() => { 13 | console.log('application is not installed'); 14 | }); 15 | ``` 16 | 17 | ## Library Release Process 18 | 19 | Our library release process is designed to ensure quality, consistency, and proper versioning. The process is broken down into multiple stages to ensure every change is tracked, reviewed, and integrated appropriately. 20 | We use [changesets](https://github.com/changesets/changesets) for version and release management. 21 | 22 | ### 1. Adding Changes 23 | 24 | Whenever you introduce a new change, run the command: 25 | 26 | > You have to do this at least once per branch with some changes. 27 | 28 | ```bash 29 | npm run changes:add 30 | ``` 31 | 32 | - The CLI will prompt you with questions regarding your changes. You'll need to specify the nature and level of the changes (options: patch, minor, major). 33 | - After completing the CLI prompts, commit the changes with a commit message similar to `chore: update changesets`. 34 | 35 | ### 2. Releasing and Publishing 36 | 37 | Steps to make a release: 38 | - To initiate a release, create a pull request from `master` to release with the title Release. 39 | - Ensure all CI checks pass successfully. 40 | - Once CI checks are green and you have at least one approval, merge the pull request. 41 | - Post-merge, the release GitHub Actions will trigger and create an "update versions" pull request to the `release` branch. 42 | - Wait for the CI to turn green on the "update versions" pull request. 43 | - Once CI is green, merge the "update versions" pull request. 44 | - After this merge, the actions will trigger again. This time, they'll generate a new tag, create a new release, and publish packages to both GitHub and npm registries. 45 | 46 | ### 3. Post-Release Activities 47 | 48 | After a successful release, ensure you create a backmerge pull request from release to `master`. This ensures that the `master` branch stays up-to-date with the latest versions and changes. 49 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | import { browserDeeplink } from '../src/index.js'; 2 | 3 | const links = Array.from(document.querySelectorAll('#existing-app, #not-existing-app')); 4 | 5 | links.forEach((link) => { 6 | link.addEventListener('click', (event) => { 7 | event.preventDefault(); 8 | browserDeeplink(event.target.href).then(() => { 9 | console.log('installed'); 10 | }).catch(() => { 11 | console.log('not installed'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Browser Deeplink 6 | 7 | 8 | Open Telegram 9 |
10 | Open Not Existing App 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@macpaw/browser-deeplink", 3 | "version": "1.2.0", 4 | "main": "dist/index.js", 5 | "module": "src/index", 6 | "license": "MIT", 7 | "scripts": { 8 | "lint": "eslint ./src", 9 | "build": "rollup -c", 10 | "changes:release": "changeset tag", 11 | "changes:add": "changeset add" 12 | }, 13 | "devDependencies": { 14 | "@changesets/cli": "^2.26.2", 15 | "@macpaw/eslint-config-base": "latest", 16 | "@rollup/plugin-buble": "^0.21.3", 17 | "eslint": "^7.32.0", 18 | "rollup": "^2.57.0" 19 | }, 20 | "files": [ 21 | "dist", 22 | "src" 23 | ], 24 | "types": "src/index.d.ts", 25 | "homepage": "https://macpaw.github.io/browser-deeplink/example", 26 | "repository": "macpaw/browser-deeplink" 27 | } 28 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from '@rollup/plugin-buble'; 2 | 3 | const config = { 4 | input: './src/index.js', 5 | output: { 6 | file: './dist/index.js', 7 | format: 'cjs', 8 | indent: false 9 | }, 10 | plugins: [buble()], 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@macpaw/browser-deeplink' { 2 | type Options = { 3 | waitTimeout?: number 4 | } 5 | 6 | export function browserDeeplink(appLink: string, options?: Options): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const injectIframe = (src) => { 2 | const iframe = document.createElement('iframe'); 3 | 4 | iframe.src = src; 5 | document.body.appendChild(iframe); 6 | iframe.style.width = '1px'; 7 | iframe.style.height = '1px'; 8 | iframe.style.position = 'fixed'; 9 | iframe.style.left = '-1px'; 10 | 11 | return iframe; 12 | }; 13 | 14 | const ejectIframe = (iframe) => { 15 | document.body.removeChild(iframe); 16 | }; 17 | 18 | export const browserDeeplink = (appLink, options = {}) => { 19 | const defaults = { waitTimeout: 200 }; 20 | const currentOptions = { ...defaults, ...options }; 21 | 22 | return new Promise(((resolve, reject) => { 23 | const iframe = injectIframe(appLink); 24 | const timeout = setTimeout(() => { 25 | window.removeEventListener('blur', windowBlurListener); 26 | ejectIframe(iframe); 27 | reject(Error(`Can't open ${appLink}`)); 28 | }, currentOptions.waitTimeout); 29 | 30 | // eslint-disable-next-line func-style 31 | function windowBlurListener() { 32 | window.removeEventListener('blur', windowBlurListener); 33 | clearTimeout(timeout); 34 | ejectIframe(iframe); 35 | resolve(); 36 | } 37 | 38 | window.addEventListener('blur', windowBlurListener); 39 | })); 40 | }; 41 | --------------------------------------------------------------------------------