├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── example ├── index.html ├── index.jsx └── styles.css ├── jest.config.js ├── package.json ├── release ├── __tests__ │ ├── get-url.test.d.ts │ └── share-content.test.d.ts ├── get-url.d.ts ├── index.d.ts ├── index.js ├── index.js.map ├── index.mjs ├── index.mjs.map ├── index.umd.js ├── index.umd.js.map └── share-content.d.ts ├── src ├── __tests__ │ ├── get-url.test.ts │ └── share-content.test.ts ├── get-url.ts ├── index.ts ├── share-content.ts └── typing.d.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | env: { 4 | browser: true, 5 | es6: true 6 | }, 7 | extends: [ 8 | 'plugin:react/recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint' 11 | ], 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module' 18 | }, 19 | plugins: ['react', 'react-hooks'], 20 | settings: { 21 | react: { 22 | version: 'detect' 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | .cache 24 | .rts2_cache_* 25 | 26 | dist 27 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm test 9 | - npm run build -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "--inspect-brk", 10 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 11 | "--runInBand" 12 | ], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "port": 9229 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Agney Menon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # useWebShare 2 | 3 | 4 | 5 | 6 | 7 | code style: prettier 8 | 9 | 10 | 11 | 12 | 13 | A custom react hook for triggering the native [web share](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share) dialog in [supported browsers](https://caniuse.com/#feat=web-share) 14 | 15 | [Demo](https://agneym.github.io/react-use-web-share/) 16 | 17 | ## Usage 18 | 19 | ``` 20 | npm install react-use-web-share 21 | ``` 22 | 23 | ```javascript 24 | const { loading, isSupported, share } = useWebShare(); 25 | 26 | function onClick() { 27 | share(); 28 | } 29 | ``` 30 | 31 | See [example](https://github.com/BoyWithSilverWings/react-use-web-share/tree/master/example) directory for full example. 32 | 33 | ### Parameters 34 | 35 | | Parameter | description | default | required | 36 | | :-------: | :---------------------------------------------------: | :------: | :------: | 37 | | onSuccess | called on successfully sharing the content | () => {} | false | 38 | | onError | called when caught error from navigator share content | () => {} | false | 39 | 40 | ### Returns 41 | 42 | | Name | Type | Description | 43 | | :---------: | :------: | :------------------------------------------------------------------------------------------: | 44 | | loading | boolean | Loading state | 45 | | isSupported | boolean | Detects whether the feature is supported in user's browser. Can be used to show the fallback | 46 | | share | function | can be called to trigger the native share popup | 47 | 48 | ### `share` 49 | 50 | This is the function that triggers the native share dialog in browser. 51 | 52 | This takes an object as argument. 53 | 54 | | Name | description | default | 55 | | :---: | :------------------: | :---------------------------------------------: | 56 | | title | title of shared item | `document.title` | 57 | | text | text of shared item | 58 | | url | url to be shared | canonical url if present, otherwise current url | 59 | 60 | ## Contributing 61 | 62 | 1. Install dependencies 63 | 64 | ``` 65 | npm install 66 | ``` 67 | 68 | 2. Run dev for lib 69 | 70 | ``` 71 | npm run dev 72 | ``` 73 | 74 | 3. Run demo 75 | 76 | ``` 77 | npm start 78 | ``` 79 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | useWebShare example 8 | 9 | 10 | 11 |
12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import "./styles.css"; 5 | import useWebShare from "../src"; 6 | 7 | function submitForm(target, actionFn) { 8 | const title = target.title.value; 9 | const text = target.text.value; 10 | const url = target.url.value; 11 | actionFn({ 12 | title, text, url 13 | }); 14 | } 15 | 16 | function App() { 17 | const { isSupported, loading, share } = useWebShare(); 18 | return ( 19 |
20 | { !loading && isSupported ? ( 21 |
22 |

Let's Share

23 |
{ event.preventDefault(); submitForm(event.target, share)}}> 24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 | 37 |
38 |
39 | ) : ( 40 |
41 |

42 | Your browser does not support 43 | Web Share API 44 |

45 | The Sadness :( 46 |
47 | )} 48 |
49 | ) 50 | } 51 | 52 | ReactDOM.render(, document.getElementById("root")); 53 | -------------------------------------------------------------------------------- /example/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: system; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local(".SFNSText-Light"), 6 | local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), 7 | local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), 8 | local("DroidSans"), local("Tahoma"); 9 | } 10 | 11 | body { 12 | font-family: "system"; 13 | margin: 0; 14 | } 15 | 16 | .App { 17 | min-height: calc(100vh - 4rem); 18 | } 19 | 20 | .container { 21 | width: 75%; 22 | margin: 0 auto; 23 | padding-top: 5rem; 24 | text-align: center; 25 | } 26 | 27 | .container form { 28 | margin: 2rem 0; 29 | } 30 | 31 | .container fieldset { 32 | border: none; 33 | text-align: left; 34 | } 35 | 36 | .container input { 37 | width: 100%; 38 | padding: 0.5rem; 39 | } 40 | 41 | .container button { 42 | padding: 0.8rem 2rem; 43 | margin: 0 auto; 44 | display: block; 45 | background-color: #1B98E0; 46 | box-shadow: none; 47 | color: #ffffff; 48 | border: none; 49 | cursor: pointer; 50 | } 51 | 52 | .footer { 53 | text-align: right; 54 | padding: 0 1rem; 55 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.tsx?$": "ts-jest", 4 | }, 5 | rootDir: "src", 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-web-share", 3 | "version": "1.0.2", 4 | "main": "release/index.js", 5 | "umd:main": "release/index.umd.js", 6 | "source": "src/index.ts", 7 | "keywords": [ 8 | "react", 9 | "hook", 10 | "web share", 11 | "share" 12 | ], 13 | "scripts": { 14 | "start": "parcel example/index.html", 15 | "build": "microbundle", 16 | "dev": "microbundle watch", 17 | "prettier": "prettier --write {src,example}/**/*.{js,ts,jsx,tsx}", 18 | "test": "jest", 19 | "remove-dist": "rimraf dist", 20 | "build-example": "parcel build example/index.html --public-url ./", 21 | "deploy-example": "gh-pages -d dist", 22 | "update-example": "npm run remove-dist && npm run build-example && npm run deploy-example", 23 | "eslint": "eslint ./src/**/*.{ts,tsx}" 24 | }, 25 | "description": "A custom react hook for triggering the native web share dialog", 26 | "repository": "github:agneym/react-use-web-share", 27 | "homepage": "https://agneym.github.io/react-use-web-share/", 28 | "author": "Agney ", 29 | "bugs": { 30 | "url": "https://github.com/agneym/react-use-web-share/issues" 31 | }, 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@types/jest": "^25.1.3", 35 | "@types/react": "16.9.23", 36 | "@types/react-dom": "16.9.5", 37 | "@typescript-eslint/eslint-plugin": "^2.21.0", 38 | "@typescript-eslint/parser": "^2.21.0", 39 | "eslint": "^6.8.0", 40 | "eslint-config-prettier": "6.10.0", 41 | "eslint-plugin-react": "^7.18.3", 42 | "eslint-plugin-react-hooks": "^2.4.0", 43 | "gh-pages": "^2.2.0", 44 | "husky": "^4.2.3", 45 | "jest": "25.1.0", 46 | "jest-fetch-mock": "3.0.1", 47 | "microbundle": "^0.11.0", 48 | "parcel-bundler": "1.12.4", 49 | "prettier": "1.19.1", 50 | "pretty-quick": "^2.0.1", 51 | "react": "16.12.0", 52 | "react-dom": "16.12.0", 53 | "rimraf": "^3.0.2", 54 | "ts-jest": "25.2.1" 55 | }, 56 | "peerDependencies": { 57 | "react": ">= 16.8.0" 58 | }, 59 | "dependencies": { 60 | "@testing-library/react": "^9.4.1" 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "pretty-quick --staged" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /release/__tests__/get-url.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /release/__tests__/share-content.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /release/get-url.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the URL to be shared. 3 | * If the site uses canonical URL, then use that URL otherwise the current URL 4 | * @param url URL that might be passed on by the user 5 | */ 6 | declare function getUrl(url?: string): string; 7 | export default getUrl; 8 | -------------------------------------------------------------------------------- /release/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use native web share dialog when available 3 | * @param onSuccess function called on successfully sharing content 4 | * @param onError callback function called on error sharing content 5 | * @example 6 | * const { isSupported, isLoading, share } = useWebShare(successFn, errorFn); 7 | */ 8 | declare function useWebShare(onSuccess?: () => void, onError?: () => void): { 9 | loading: boolean; 10 | isSupported: boolean; 11 | share: (config: Partial) => void; 12 | }; 13 | export default useWebShare; 14 | -------------------------------------------------------------------------------- /release/index.js: -------------------------------------------------------------------------------- 1 | var t = require("react"); 2 | function e(t, e) { 3 | return function(n) { 4 | var r = (function(t) { 5 | if (t) return t; 6 | var e = document.querySelector("link[rel=canonical]"); 7 | return e ? e.href : window.location.href; 8 | })(n.url), 9 | i = n.title || document.title; 10 | navigator 11 | .share({ text: n.text, title: i, url: r }) 12 | .then(t) 13 | .catch(e); 14 | }; 15 | } 16 | module.exports = function(n, r) { 17 | void 0 === n && (n = function() {}), void 0 === r && (r = function() {}); 18 | var i = t.useState(!0), 19 | o = i[0], 20 | u = i[1], 21 | a = t.useState(!1), 22 | c = a[0], 23 | f = a[1]; 24 | return ( 25 | t.useEffect( 26 | function() { 27 | navigator.share ? f(!0) : f(!1), u(!1); 28 | }, 29 | [n, r] 30 | ), 31 | { loading: o, isSupported: c, share: e(n, r) } 32 | ); 33 | }; 34 | //# sourceMappingURL=index.js.map 35 | -------------------------------------------------------------------------------- /release/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":["../src/share-content.ts","../src/get-url.ts","../src/index.ts"],"sourcesContent":["import getUrl from \"./get-url\";\n\n/**\n * Trigger native share popup\n */\nfunction shareContent(onSuccess: () => void, onError: () => void) {\n return function (config: Partial) {\n const url = getUrl(config.url);\n const title = config.title || document.title;\n const text = config.text;\n navigator\n .share({ text, title, url })\n .then(onSuccess)\n .catch(onError);\n };\n}\n\nexport default shareContent;","/**\n * Get the URL to be shared.\n * If the site uses canonical URL, then use that URL otherwise the current URL\n * @param url URL that might be passed on by the user\n */\nfunction getUrl(url?: string): string {\n if(!!url) {\n return url;\n } else {\n const canonicalEl = document.querySelector('link[rel=canonical]') as HTMLLinkElement;\n return canonicalEl ? canonicalEl.href : window.location.href;\n }\n}\n\nexport default getUrl;","import { useState, useEffect } from 'react';\n\nimport shareContent from \"./share-content\";\n\n/**\n * Use native web share dialog when available\n * @param onSuccess function called on successfully sharing content\n * @param onError callback function called on error sharing content\n * @example \n * const { isSupported, isLoading, share } = useWebShare(successFn, errorFn);\n */\nfunction useWebShare(onSuccess = () => { }, onError = () => { }) {\n const [loading, setLoading] = useState(true);\n const [isSupported, setSupport] = useState(false);\n\n useEffect(\n () => {\n if (!!navigator.share) {\n setSupport(true);\n } else {\n setSupport(false);\n }\n setLoading(false);\n },\n [onSuccess, onError]\n );\n\n return {\n loading,\n isSupported,\n share: shareContent(onSuccess, onError),\n };\n}\n\nexport default useWebShare;\n"],"names":["shareContent","onSuccess","onError","config","url","canonicalEl","document","querySelector","href","window","location","getUrl","title","navigator","share","text","then","catch","useState","useEffect","setSupport","setLoading","loading","isSupported"],"mappings":"uBAKA,SAASA,EAAaC,EAAuBC,UACpC,SAAUC,OACTC,ECFV,SAAgBA,MACTA,SACIA,MAEDC,EAAcC,SAASC,cAAc,8BACpCF,EAAcA,EAAYG,KAAOC,OAAOC,SAASF,KDH5CG,CAAOR,EAAOC,KACpBQ,EAAQT,EAAOS,OAASN,SAASM,MAEvCC,UACGC,MAAM,MAFIX,EAAOY,WAEHH,MAAOR,IACrBY,KAAKf,GACLgB,MAAMf,mBEFb,SAAqBD,EAAuBC,mEACZgB,YAAS,mBACLA,YAAS,wBAE3CC,uBAEUN,UAAUC,MACdM,GAAW,GAEXA,GAAW,GAEbC,GAAW,IAEb,CAACpB,EAAWC,IAGP,SACLoB,cACAC,EACAT,MAAOd,EAAaC,EAAWC"} -------------------------------------------------------------------------------- /release/index.mjs: -------------------------------------------------------------------------------- 1 | import { useState as t, useEffect as n } from "react"; 2 | function r(t, n) { 3 | return function(r) { 4 | var e = (function(t) { 5 | if (t) return t; 6 | var n = document.querySelector("link[rel=canonical]"); 7 | return n ? n.href : window.location.href; 8 | })(r.url), 9 | o = r.title || document.title; 10 | navigator 11 | .share({ text: r.text, title: o, url: e }) 12 | .then(t) 13 | .catch(n); 14 | }; 15 | } 16 | export default function(e, o) { 17 | void 0 === e && (e = function() {}), void 0 === o && (o = function() {}); 18 | var i = t(!0), 19 | u = i[0], 20 | a = i[1], 21 | c = t(!1), 22 | f = c[0], 23 | l = c[1]; 24 | return ( 25 | n( 26 | function() { 27 | navigator.share ? l(!0) : l(!1), a(!1); 28 | }, 29 | [e, o] 30 | ), 31 | { loading: u, isSupported: f, share: r(e, o) } 32 | ); 33 | } 34 | //# sourceMappingURL=index.mjs.map 35 | -------------------------------------------------------------------------------- /release/index.mjs.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.mjs","sources":["../src/share-content.ts","../src/get-url.ts","../src/index.ts"],"sourcesContent":["import getUrl from \"./get-url\";\n\n/**\n * Trigger native share popup\n */\nfunction shareContent(onSuccess: () => void, onError: () => void) {\n return function (config: Partial) {\n const url = getUrl(config.url);\n const title = config.title || document.title;\n const text = config.text;\n navigator\n .share({ text, title, url })\n .then(onSuccess)\n .catch(onError);\n };\n}\n\nexport default shareContent;","/**\n * Get the URL to be shared.\n * If the site uses canonical URL, then use that URL otherwise the current URL\n * @param url URL that might be passed on by the user\n */\nfunction getUrl(url?: string): string {\n if(!!url) {\n return url;\n } else {\n const canonicalEl = document.querySelector('link[rel=canonical]') as HTMLLinkElement;\n return canonicalEl ? canonicalEl.href : window.location.href;\n }\n}\n\nexport default getUrl;","import { useState, useEffect } from 'react';\n\nimport shareContent from \"./share-content\";\n\n/**\n * Use native web share dialog when available\n * @param onSuccess function called on successfully sharing content\n * @param onError callback function called on error sharing content\n * @example \n * const { isSupported, isLoading, share } = useWebShare(successFn, errorFn);\n */\nfunction useWebShare(onSuccess = () => { }, onError = () => { }) {\n const [loading, setLoading] = useState(true);\n const [isSupported, setSupport] = useState(false);\n\n useEffect(\n () => {\n if (!!navigator.share) {\n setSupport(true);\n } else {\n setSupport(false);\n }\n setLoading(false);\n },\n [onSuccess, onError]\n );\n\n return {\n loading,\n isSupported,\n share: shareContent(onSuccess, onError),\n };\n}\n\nexport default useWebShare;\n"],"names":["shareContent","onSuccess","onError","config","url","canonicalEl","document","querySelector","href","window","location","getUrl","title","navigator","share","text","then","catch","useState","useEffect","setSupport","setLoading","loading","isSupported"],"mappings":"gDAKA,SAASA,EAAaC,EAAuBC,UACpC,SAAUC,OACTC,ECFV,SAAgBA,MACTA,SACIA,MAEDC,EAAcC,SAASC,cAAc,8BACpCF,EAAcA,EAAYG,KAAOC,OAAOC,SAASF,KDH5CG,CAAOR,EAAOC,KACpBQ,EAAQT,EAAOS,OAASN,SAASM,MAEvCC,UACGC,MAAM,MAFIX,EAAOY,WAEHH,MAAOR,IACrBY,KAAKf,GACLgB,MAAMf,mBEFb,SAAqBD,EAAuBC,mEACZgB,GAAS,mBACLA,GAAS,wBAE3CC,aAEUN,UAAUC,MACdM,GAAW,GAEXA,GAAW,GAEbC,GAAW,IAEb,CAACpB,EAAWC,IAGP,SACLoB,cACAC,EACAT,MAAOd,EAAaC,EAAWC"} -------------------------------------------------------------------------------- /release/index.umd.js: -------------------------------------------------------------------------------- 1 | !(function(e, t) { 2 | "object" == typeof exports && "undefined" != typeof module 3 | ? (module.exports = t(require("react"))) 4 | : "function" == typeof define && define.amd 5 | ? define(["react"], t) 6 | : (e.reactUseWebShare = t(e.react)); 7 | })(this, function(e) { 8 | function t(e, t) { 9 | return function(n) { 10 | var r = (function(e) { 11 | if (e) return e; 12 | var t = document.querySelector("link[rel=canonical]"); 13 | return t ? t.href : window.location.href; 14 | })(n.url), 15 | o = n.title || document.title; 16 | navigator 17 | .share({ text: n.text, title: o, url: r }) 18 | .then(e) 19 | .catch(t); 20 | }; 21 | } 22 | return function(n, r) { 23 | void 0 === n && (n = function() {}), void 0 === r && (r = function() {}); 24 | var o = e.useState(!0), 25 | i = o[0], 26 | u = o[1], 27 | c = e.useState(!1), 28 | a = c[0], 29 | f = c[1]; 30 | return ( 31 | e.useEffect( 32 | function() { 33 | navigator.share ? f(!0) : f(!1), u(!1); 34 | }, 35 | [n, r] 36 | ), 37 | { loading: i, isSupported: a, share: t(n, r) } 38 | ); 39 | }; 40 | }); 41 | //# sourceMappingURL=index.umd.js.map 42 | -------------------------------------------------------------------------------- /release/index.umd.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.umd.js","sources":["../src/share-content.ts","../src/get-url.ts","../src/index.ts"],"sourcesContent":["import getUrl from \"./get-url\";\n\n/**\n * Trigger native share popup\n */\nfunction shareContent(onSuccess: () => void, onError: () => void) {\n return function (config: Partial) {\n const url = getUrl(config.url);\n const title = config.title || document.title;\n const text = config.text;\n navigator\n .share({ text, title, url })\n .then(onSuccess)\n .catch(onError);\n };\n}\n\nexport default shareContent;","/**\n * Get the URL to be shared.\n * If the site uses canonical URL, then use that URL otherwise the current URL\n * @param url URL that might be passed on by the user\n */\nfunction getUrl(url?: string): string {\n if(!!url) {\n return url;\n } else {\n const canonicalEl = document.querySelector('link[rel=canonical]') as HTMLLinkElement;\n return canonicalEl ? canonicalEl.href : window.location.href;\n }\n}\n\nexport default getUrl;","import { useState, useEffect } from 'react';\n\nimport shareContent from \"./share-content\";\n\n/**\n * Use native web share dialog when available\n * @param onSuccess function called on successfully sharing content\n * @param onError callback function called on error sharing content\n * @example \n * const { isSupported, isLoading, share } = useWebShare(successFn, errorFn);\n */\nfunction useWebShare(onSuccess = () => { }, onError = () => { }) {\n const [loading, setLoading] = useState(true);\n const [isSupported, setSupport] = useState(false);\n\n useEffect(\n () => {\n if (!!navigator.share) {\n setSupport(true);\n } else {\n setSupport(false);\n }\n setLoading(false);\n },\n [onSuccess, onError]\n );\n\n return {\n loading,\n isSupported,\n share: shareContent(onSuccess, onError),\n };\n}\n\nexport default useWebShare;\n"],"names":["shareContent","onSuccess","onError","config","url","canonicalEl","document","querySelector","href","window","location","getUrl","title","navigator","share","text","then","catch","useState","useEffect","setSupport","setLoading","loading","isSupported"],"mappings":"iNAKA,SAASA,EAAaC,EAAuBC,UACpC,SAAUC,OACTC,ECFV,SAAgBA,MACTA,SACIA,MAEDC,EAAcC,SAASC,cAAc,8BACpCF,EAAcA,EAAYG,KAAOC,OAAOC,SAASF,KDH5CG,CAAOR,EAAOC,KACpBQ,EAAQT,EAAOS,OAASN,SAASM,MAEvCC,UACGC,MAAM,MAFIX,EAAOY,WAEHH,MAAOR,IACrBY,KAAKf,GACLgB,MAAMf,WEFb,SAAqBD,EAAuBC,mEACZgB,YAAS,mBACLA,YAAS,wBAE3CC,uBAEUN,UAAUC,MACdM,GAAW,GAEXA,GAAW,GAEbC,GAAW,IAEb,CAACpB,EAAWC,IAGP,SACLoB,cACAC,EACAT,MAAOd,EAAaC,EAAWC"} -------------------------------------------------------------------------------- /release/share-content.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Trigger native share popup 3 | */ 4 | declare function shareContent(onSuccess: () => void, onError: () => void): (config: Partial) => void; 5 | export default shareContent; 6 | -------------------------------------------------------------------------------- /src/__tests__/get-url.test.ts: -------------------------------------------------------------------------------- 1 | import getUrl from "../get-url"; 2 | 3 | describe("Get url function", () => { 4 | test("returns me passed url", () => { 5 | const url = getUrl("string"); 6 | expect(url).toBe("string"); 7 | }); 8 | 9 | test("returns me current url", () => { 10 | window = Object.create(window); 11 | Object.defineProperty(window, 'location', { 12 | value: { 13 | href: "string" 14 | } 15 | }); 16 | const url = getUrl("string"); 17 | expect(url).toBe("string"); 18 | }); 19 | 20 | test("returns me canonical url", () => { 21 | document.body.innerHTML = ` 22 | 23 | `; 24 | const url = getUrl("string"); 25 | expect(url).toBe("string"); 26 | }) 27 | }) -------------------------------------------------------------------------------- /src/__tests__/share-content.test.ts: -------------------------------------------------------------------------------- 1 | import shareContent from "../share-content"; 2 | 3 | describe("Share Content function", () => { 4 | const successFn = () => { }; 5 | const errorFn = () => { }; 6 | 7 | beforeEach(() => { 8 | navigator.share = jest.fn(() => Promise.resolve()); 9 | }); 10 | 11 | test("calls share function", () => { 12 | shareContent(successFn, errorFn)({}); 13 | expect(navigator.share).toBeCalled(); 14 | }); 15 | 16 | test("returns a function", () => { 17 | const result = shareContent(successFn, errorFn); 18 | expect(typeof result).toBe("function"); 19 | }); 20 | 21 | test("calls success on successful share", async () => { 22 | const mockedSuccessFn = jest.fn(); 23 | const mockedErrorFn = jest.fn(); 24 | await shareContent(mockedSuccessFn, mockedErrorFn)({}); 25 | expect(mockedSuccessFn).toBeCalled(); 26 | expect(mockedErrorFn).not.toBeCalled(); 27 | }); 28 | 29 | /* 30 | Have to find a way to make this work without throwing the error in console. 31 | test("calls error on failed share", async () => { 32 | navigator.share = jest.fn(); 33 | const share = jest.spyOn(navigator, "share"); 34 | share.mockImplementation(() => { 35 | throw new Error(); 36 | }); 37 | const mockedSuccessFn = jest.fn(); 38 | const mockedErrorFn = jest.fn(); 39 | await shareContent(mockedSuccessFn, mockedErrorFn)({}); 40 | expect(mockedSuccessFn).not.toBeCalled(); 41 | expect(mockedErrorFn).toBeCalled(); 42 | }) */ 43 | }) -------------------------------------------------------------------------------- /src/get-url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the URL to be shared. 3 | * If the site uses canonical URL, then use that URL otherwise the current URL 4 | * @param url URL that might be passed on by the user 5 | */ 6 | function getUrl(url?: string): string { 7 | if (!!url) { 8 | return url; 9 | } else { 10 | const canonicalEl = document.querySelector( 11 | "link[rel=canonical]" 12 | ) as HTMLLinkElement; 13 | return canonicalEl ? canonicalEl.href : window.location.href; 14 | } 15 | } 16 | 17 | export default getUrl; 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | import shareContent from "./share-content"; 4 | 5 | /** 6 | * Use native web share dialog when available 7 | * @param onSuccess function called on successfully sharing content 8 | * @param onError callback function called on error sharing content 9 | * @example 10 | * const { isSupported, isLoading, share } = useWebShare(successFn, errorFn); 11 | */ 12 | function useWebShare(onSuccess = () => { }, onError = () => { }) { 13 | const [loading, setLoading] = useState(true); 14 | const [isSupported, setSupport] = useState(false); 15 | 16 | useEffect( 17 | () => { 18 | if (!!navigator.share) { 19 | setSupport(true); 20 | } else { 21 | setSupport(false); 22 | } 23 | setLoading(false); 24 | }, 25 | [onSuccess, onError] 26 | ); 27 | 28 | return { 29 | loading, 30 | isSupported, 31 | share: shareContent(onSuccess, onError), 32 | }; 33 | } 34 | 35 | export default useWebShare; 36 | -------------------------------------------------------------------------------- /src/share-content.ts: -------------------------------------------------------------------------------- 1 | import getUrl from "./get-url"; 2 | 3 | /** 4 | * Trigger native share popup 5 | */ 6 | function shareContent(onSuccess: () => void, onError: () => void) { 7 | return function (config: Partial) { 8 | const url = getUrl(config.url); 9 | const title = config.title || document.title; 10 | const text = config.text; 11 | navigator 12 | .share({ text, title, url }) 13 | .then(onSuccess) 14 | .catch(onError); 15 | }; 16 | } 17 | 18 | export default shareContent; -------------------------------------------------------------------------------- /src/typing.d.ts: -------------------------------------------------------------------------------- 1 | interface IShareConfig { 2 | title: string; 3 | text?: string; 4 | url: string; 5 | } 6 | 7 | interface Navigator { 8 | share: (config: IShareConfig) => Promise; 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "lib": ["es2017", "dom"], 7 | "noEmitOnError": true, 8 | "strict": true, 9 | "target": "es5" 10 | } 11 | } 12 | --------------------------------------------------------------------------------