diff --git a/.eslintrc b/.eslintrc index 0c513bad1..8f16371b4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,6 +13,7 @@ "FB": "readonly" }, "env": { + "node": true, "commonjs": true, "browser": true, "es6": true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..bbd3d62ca --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build + +on: + push: + branches: + - '*' + tags-ignore: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Build + run: | + echo -e "[url \"https://github.com/\"]\n\tinsteadOf = ssh://git@github.com/" > ~/.gitconfig + yarn install && yarn build + - uses: actions/upload-artifact@v1 + with: + name: stremio-web + path: build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..9eef035ac --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +on: + push: + branches-ignore: + - '*' + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Build + run: | + echo -e "[url \"https://github.com/\"]\n\tinsteadOf = ssh://git@github.com/" > ~/.gitconfig + yarn install && yarn build + - run: zip -r stremio-web.zip ./build + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v1-release + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: stremio-web.zip + asset_name: stremio-web.zip + tag: ${{ github.ref }} + overwrite: true + - run: | + curl -H "Content-Type: application/zip" \ + -H "Authorization: Bearer ${{ secrets.netlify_access_token }}" \ + --data-binary "@stremio-web.zip" \ + https://api.netlify.com/api/v1/sites/stremio-staging.netlify.com/deploys + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..24508e55d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: Test + +on: + push: + branches: + - '*' + tags-ignore: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Test + run: | + echo -e "[url \"https://github.com/\"]\n\tinsteadOf = ssh://git@github.com/" > ~/.gitconfig + yarn install && yarn test + diff --git a/.gitignore b/.gitignore index 6a44f9718..cfc12513d 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /node_modules -/dist +/build /package-lock.json /npm-debug.log .DS_Store diff --git a/package.json b/package.json index 53d6f78c6..35703926e 100755 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lint": "eslint src" }, "dependencies": { + "@sentry/browser": "5.11.1", "a-color-picker": "1.2.1", "classnames": "2.2.6", "events": "1.1.1", @@ -50,6 +51,7 @@ "css-loader": "3.4.0", "cssnano": "4.1.10", "cssnano-preset-advanced": "4.0.7", + "dotenv": "8.2.0", "eslint": "6.7.2", "eslint-plugin-react": "7.17.0", "html-webpack-plugin": "3.2.0", diff --git a/src/App/App.js b/src/App/App.js index 0a560f919..22a1bb92a 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -2,6 +2,8 @@ require('spatial-navigation-polyfill'); const React = require('react'); const { Router } = require('stremio-router'); const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services'); +const { ToastProvider } = require('stremio/common'); +const CoreEventsToaster = require('./CoreEventsToaster'); const routerViewsConfig = require('./routerViewsConfig'); const styles = require('./styles'); @@ -21,11 +23,16 @@ const App = () => { setShellInitialized(services.shell.active || services.shell.error instanceof Error); }; const onCoreStateChanged = () => { - setCoreInitialized(services.core.active || services.core.error instanceof Error); if (services.core.active) { - services.core.dispatch({ action: 'LoadCtx' }); window.core = services.core; + services.core.dispatch({ + action: 'Load', + args: { + model: 'Ctx' + } + }); } + setCoreInitialized(services.core.active || services.core.error instanceof Error); }; services.shell.on('stateChanged', onShellStateChanged); services.core.on('stateChanged', onCoreStateChanged); @@ -45,12 +52,15 @@ const App = () => { { shellInitialized && coreInitialized ? - + + + + :
} diff --git a/src/App/CoreEventsToaster.js b/src/App/CoreEventsToaster.js new file mode 100644 index 000000000..1c76c9865 --- /dev/null +++ b/src/App/CoreEventsToaster.js @@ -0,0 +1,29 @@ +const React = require('react'); +const { useServices } = require('stremio/services'); +const { useToast } = require('stremio/common'); + +const CoreEventsToaster = () => { + const { core } = useServices(); + const toast = useToast(); + React.useEffect(() => { + const onEvent = ({ event, args }) => { + // UserAuthenticated are handled only in the /intro route + if (event === 'Error' && args.source.event !== 'UserAuthenticated') { + toast.show({ + type: 'error', + title: args.source.event, + message: args.error.message, + icon: 'ic_warning', + timeout: 10000 + }); + } + }; + core.on('Event', onEvent); + return () => { + core.off('Event', onEvent); + }; + }, []); + return null; +}; + +module.exports = CoreEventsToaster; diff --git a/src/App/styles.less b/src/App/styles.less index 4ab98b8ef..8844fde5e 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -10,6 +10,7 @@ --landscape-shape-ratio: 0.5625; --poster-shape-ratio: 1.464; --scroll-bar-width: 6px; + --nav-bar-size: 3.2rem; --focus-outline-size: 2px; --color-facebook: #4267b2; --color-twitter: #1DA1F2; @@ -71,6 +72,23 @@ html { width: 100%; height: 100%; + .toasts-container { + position: absolute; + top: calc(1.2 * var(--nav-bar-size)); + right: 0; + bottom: calc(1.2 * var(--nav-bar-size)); + left: auto; + z-index: 1; + padding: 0 calc(1.2 * var(--nav-bar-size)); + overflow-y: auto; + scrollbar-width: none; + pointer-events: none; + + &::-webkit-scrollbar { + width: var(--scroll-bar-width); + } + } + .router { width: 100%; height: 100%; diff --git a/src/common/ColorInput/ColorInput.js b/src/common/ColorInput/ColorInput.js index 7e846a4a8..d64d8938e 100644 --- a/src/common/ColorInput/ColorInput.js +++ b/src/common/ColorInput/ColorInput.js @@ -77,7 +77,7 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => { ColorInput.propTypes = { className: PropTypes.string, value: PropTypes.string, - dataset: PropTypes.objectOf(PropTypes.string), + dataset: PropTypes.object, onChange: PropTypes.func, onClick: PropTypes.func }; diff --git a/src/common/MetaItem/MetaItem.js b/src/common/MetaItem/MetaItem.js index 4d4f6cb1f..ffd75a754 100644 --- a/src/common/MetaItem/MetaItem.js +++ b/src/common/MetaItem/MetaItem.js @@ -121,7 +121,7 @@ MetaItem.propTypes = { playIcon: PropTypes.bool, progress: PropTypes.number, options: PropTypes.array, - dataset: PropTypes.objectOf(PropTypes.string), + dataset: PropTypes.object, optionOnSelect: PropTypes.func, onClick: PropTypes.func }; diff --git a/src/common/ModalDialog/ModalDialog.js b/src/common/ModalDialog/ModalDialog.js index abb594c09..441907f10 100644 --- a/src/common/ModalDialog/ModalDialog.js +++ b/src/common/ModalDialog/ModalDialog.js @@ -112,7 +112,7 @@ ModalDialog.propTypes = { PropTypes.arrayOf(PropTypes.node), PropTypes.node ]), - dataset: PropTypes.objectOf(PropTypes.string), + dataset: PropTypes.object, onCloseRequest: PropTypes.func }; diff --git a/src/common/NavBar/NavMenu/NavMenu.js b/src/common/NavBar/NavMenu/NavMenu.js index 2509185a2..40a846d9d 100644 --- a/src/common/NavBar/NavMenu/NavMenu.js +++ b/src/common/NavBar/NavMenu/NavMenu.js @@ -2,17 +2,19 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); +const { useServices } = require('stremio/services'); const Button = require('stremio/common/Button'); const Popup = require('stremio/common/Popup'); const useBinaryState = require('stremio/common/useBinaryState'); const useFullscreen = require('stremio/common/useFullscreen'); -const useUser = require('stremio/common/useUser'); +const useProfile = require('./useProfile'); const styles = require('./styles'); const NavMenu = ({ className }) => { + const { core } = useServices(); + const profile = useProfile(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); - const [user, logout] = useUser(); const popupLabelOnClick = React.useCallback((event) => { if (!event.nativeEvent.togglePopupPrevented) { toggleMenu(); @@ -22,7 +24,12 @@ const NavMenu = ({ className }) => { event.nativeEvent.togglePopupPrevented = true; }, []); const logoutButtonOnClick = React.useCallback(() => { - logout(); + core.dispatch({ + action: 'Ctx', + args: { + action: 'Logout' + } + }); }, []); return ( {
-
{user === null ? 'Anonymous user' : user.email}
+
{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}
-
diff --git a/src/common/NavBar/NavMenu/useProfile.js b/src/common/NavBar/NavMenu/useProfile.js new file mode 100644 index 000000000..943f1de17 --- /dev/null +++ b/src/common/NavBar/NavMenu/useProfile.js @@ -0,0 +1,23 @@ +const React = require('react'); +const { useServices } = require('stremio/services'); +const useModelState = require('stremio/common/useModelState'); + +const mapProfileState = (ctx) => { + return ctx.profile; +}; + +const useProfile = () => { + const { core } = useServices(); + const initProfileState = React.useCallback(() => { + const ctx = core.getState('ctx'); + return mapProfileState(ctx); + }, []); + const profile = useModelState({ + model: 'ctx', + init: initProfileState, + map: mapProfileState + }); + return profile; +}; + +module.exports = useProfile; diff --git a/src/common/NavBar/styles.less b/src/common/NavBar/styles.less index 3c9ff80d2..30304c8ec 100644 --- a/src/common/NavBar/styles.less +++ b/src/common/NavBar/styles.less @@ -1,5 +1,4 @@ .nav-bar-container { - --nav-bar-size: 3.2rem; display: flex; flex-direction: row; align-items: center; diff --git a/src/common/PaginationInput/PaginationInput.js b/src/common/PaginationInput/PaginationInput.js index 7358c55c7..dd5029117 100644 --- a/src/common/PaginationInput/PaginationInput.js +++ b/src/common/PaginationInput/PaginationInput.js @@ -35,7 +35,7 @@ const PaginationInput = ({ className, label, dataset, onSelect, ...props }) => { PaginationInput.propTypes = { className: PropTypes.string, label: PropTypes.string, - dataset: PropTypes.objectOf(PropTypes.string), + dataset: PropTypes.object, onSelect: PropTypes.func }; diff --git a/src/common/Popup/Popup.js b/src/common/Popup/Popup.js index 59b89f26e..1df4192b4 100644 --- a/src/common/Popup/Popup.js +++ b/src/common/Popup/Popup.js @@ -103,7 +103,7 @@ Popup.propTypes = { direction: PropTypes.oneOf(['top-left', 'bottom-left', 'top-right', 'bottom-right']), renderLabel: PropTypes.func.isRequired, renderMenu: PropTypes.func.isRequired, - dataset: PropTypes.objectOf(PropTypes.string), + dataset: PropTypes.object, onCloseRequest: PropTypes.func }; diff --git a/src/common/Slider/Slider.js b/src/common/Slider/Slider.js index 0d37bedf7..4df37cd59 100644 --- a/src/common/Slider/Slider.js +++ b/src/common/Slider/Slider.js @@ -1,7 +1,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { useFocusedRoute } = require('stremio-router'); +const { useRouteFocused } = require('stremio-router'); const useAnimationFrame = require('stremio/common/useAnimationFrame'); const useLiveRef = require('stremio/common/useLiveRef'); const styles = require('./styles'); @@ -13,7 +13,7 @@ const Slider = ({ className, value, minimumValue, maximumValue, onSlide, onCompl const onSlideRef = useLiveRef(onSlide, [onSlide]); const onCompleteRef = useLiveRef(onComplete, [onComplete]); const sliderContainerRef = React.useRef(null); - const routeFocused = useFocusedRoute(); + const routeFocused = useRouteFocused(); const [requestThumbAnimation, cancelThumbAnimation] = useAnimationFrame(); const calculateValueForMouseX = React.useCallback((mouseX) => { if (sliderContainerRef.current === null) { diff --git a/src/common/Toast/ToastContext.js b/src/common/Toast/ToastContext.js new file mode 100644 index 000000000..7418affbd --- /dev/null +++ b/src/common/Toast/ToastContext.js @@ -0,0 +1,10 @@ +const React = require('react'); + +const ToastContext = React.createContext({ + show: () => { }, + clear: () => { } +}); + +ToastContext.displayName = 'ToastContext'; + +module.exports = ToastContext; diff --git a/src/common/Toast/ToastItem/ToastItem.js b/src/common/Toast/ToastItem/ToastItem.js new file mode 100644 index 000000000..04ddbebe6 --- /dev/null +++ b/src/common/Toast/ToastItem/ToastItem.js @@ -0,0 +1,71 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const classnames = require('classnames'); +const Icon = require('stremio-icons/dom'); +const Button = require('stremio/common/Button'); +const styles = require('./styles'); + +const ToastItem = ({ type, title, message, icon, dataset, onSelect, onClose }) => { + const toastOnClick = React.useCallback((event) => { + if (!event.nativeEvent.selectPrevented && typeof onSelect === 'function') { + onSelect({ + type: 'select', + dataset: dataset, + reactEvent: event, + nativeEvent: event.nativeEvent + }); + } + }, [dataset, onSelect]); + const closeButtonOnClick = React.useCallback((event) => { + event.nativeEvent.selectPrevented = true; + if (typeof onClose === 'function') { + onClose({ + type: 'close', + dataset: dataset, + reactEvent: event, + nativeEvent: event.nativeEvent + }); + } + }, [dataset, onClose]); + return ( + + + ); +}; + +ToastItem.propTypes = { + type: PropTypes.oneOf(['success', 'alert', 'error']), + title: PropTypes.string, + message: PropTypes.string, + icon: PropTypes.string, + dataset: PropTypes.object, + onSelect: PropTypes.func, + onClose: PropTypes.func +}; + +module.exports = ToastItem; diff --git a/src/common/Toast/ToastItem/index.js b/src/common/Toast/ToastItem/index.js new file mode 100644 index 000000000..c00d404b2 --- /dev/null +++ b/src/common/Toast/ToastItem/index.js @@ -0,0 +1,3 @@ +const ToastItem = require('./ToastItem'); + +module.exports = ToastItem; diff --git a/src/common/Toast/ToastItem/styles.less b/src/common/Toast/ToastItem/styles.less new file mode 100644 index 000000000..4c09f6cff --- /dev/null +++ b/src/common/Toast/ToastItem/styles.less @@ -0,0 +1,73 @@ +.toast-item-container { + display: flex; + flex-direction: row; + width: 25rem; + min-height: 6rem; + margin-bottom: 1rem; + border: thin solid; + background-color: var(--color-surfacelighter); + overflow: visible; + box-shadow: 0 0.3rem 0.5rem var(--color-backgrounddarker40), + 0 0.6rem 1rem var(--color-backgrounddarker20); + pointer-events: auto; + + &.success { + color: var(--color-signal5); + fill: var(--color-signal5); + } + + &.alert { + color: var(--color-signal3); + fill: var(--color-signal3); + } + + &.error { + color: var(--color-signal2); + fill: var(--color-signal2); + } + + .icon-container { + flex: none; + align-self: stretch; + width: 4.5rem; + padding: 1.2rem 0 1.2rem 1.2rem; + + .icon { + display: block; + width: 100%; + height: 100%; + } + } + + .info-container { + flex: 1; + align-self: stretch; + padding: 1rem; + + .title-container { + font-size: 1.2rem; + } + + .message-container { + font-size: 1.1rem; + } + } + + .close-button-container { + flex: none; + align-self: flex-start; + width: 3rem; + height: 3rem; + padding: 1rem; + + &:hover { + background-color: var(--color-surfacelight); + } + + .icon { + display: block; + width: 100%; + height: 100%; + } + } +} \ No newline at end of file diff --git a/src/common/Toast/ToastProvider.js b/src/common/Toast/ToastProvider.js new file mode 100644 index 000000000..cb91f34ee --- /dev/null +++ b/src/common/Toast/ToastProvider.js @@ -0,0 +1,72 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const ToastItem = require('./ToastItem'); +const ToastContext = require('./ToastContext'); + +const DEFAULT_TIMEOUT = 3000; + +const ToastProvider = ({ className, children }) => { + const [container, setContainer] = React.useState(null); + const [items, dispatch] = React.useReducer( + (items, action) => { + switch (action.type) { + case 'add': + return items.concat(action.item); + case 'remove': + return items.filter((item) => item.id !== action.id); + case 'clear': + return []; + default: + return items; + } + }, + [] + ); + const itemOnClose = React.useCallback((event) => { + clearTimeout(event.dataset.id); + dispatch({ type: 'remove', id: event.dataset.id }); + }, []); + const toast = React.useMemo(() => ({ + show: (item) => { + const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ? + item.timeout + : + DEFAULT_TIMEOUT; + const id = setTimeout(() => { + dispatch({ type: 'remove', id }); + }, timeout); + dispatch({ + type: 'add', + item: { + ...item, + id, + dataset: { + ...item.dataset, + id + }, + onClose: itemOnClose + } + }); + }, + clear: () => { + dispatch({ type: 'clear' }); + } + }), []); + return ( + + {container instanceof HTMLElement ? children : null} +
+ {items.map((item, index) => ( + + ))} +
+
+ ); +}; + +ToastProvider.propTypes = { + className: PropTypes.string, + children: PropTypes.node +}; + +module.exports = ToastProvider; diff --git a/src/common/Toast/index.js b/src/common/Toast/index.js new file mode 100644 index 000000000..516e30a1e --- /dev/null +++ b/src/common/Toast/index.js @@ -0,0 +1,7 @@ +const ToastProvider = require('./ToastProvider'); +const useToast = require('./useToast'); + +module.exports = { + ToastProvider, + useToast +}; diff --git a/src/common/Toast/useToast.js b/src/common/Toast/useToast.js new file mode 100644 index 000000000..76ba83914 --- /dev/null +++ b/src/common/Toast/useToast.js @@ -0,0 +1,8 @@ +const React = require('react'); +const ToastContext = require('./ToastContext'); + +const useToast = () => { + return React.useContext(ToastContext); +}; + +module.exports = useToast; diff --git a/src/common/index.js b/src/common/index.js index ac8270b8b..ef266e228 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -16,15 +16,17 @@ const Popup = require('./Popup'); const SharePrompt = require('./SharePrompt'); const Slider = require('./Slider'); const TextInput = require('./TextInput'); +const { ToastProvider, useToast } = require('./Toast'); const routesRegexp = require('./routesRegexp'); const useAnimationFrame = require('./useAnimationFrame'); const useBinaryState = require('./useBinaryState'); +const useCoreEvent = require('./useCoreEvent'); +const useDeepEqualEffect = require('./useDeepEqualEffect'); const useDeepEqualState = require('./useDeepEqualState'); const useFullscreen = require('./useFullscreen'); const useInLibrary = require('./useInLibrary'); const useLiveRef = require('./useLiveRef'); const useModelState = require('./useModelState'); -const useUser = require('./useUser'); module.exports = { AddonDetailsModal, @@ -45,13 +47,16 @@ module.exports = { SharePrompt, Slider, TextInput, + ToastProvider, + useToast, routesRegexp, useAnimationFrame, useBinaryState, + useCoreEvent, + useDeepEqualEffect, useDeepEqualState, useFullscreen, useInLibrary, useLiveRef, - useModelState, - useUser + useModelState }; diff --git a/src/common/routesRegexp.js b/src/common/routesRegexp.js index d2bf19114..b5bf69db4 100644 --- a/src/common/routesRegexp.js +++ b/src/common/routesRegexp.js @@ -32,8 +32,8 @@ const routesRegexp = { urlParamsNames: [] }, player: { - regexp: /^\/player\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*)$/, - urlParamsNames: ['transportUrl', 'type', 'id', 'videoId', 'stream'] + regexp: /^\/player\/([^/]*)(?:\/([^/]*)\/([^/]*)\/([^/]*)\/([^/]*))?$/, + urlParamsNames: ['stream', 'transportUrl', 'type', 'id', 'videoId'] } }; diff --git a/src/common/useCoreEvent.js b/src/common/useCoreEvent.js new file mode 100644 index 000000000..7efa311ee --- /dev/null +++ b/src/common/useCoreEvent.js @@ -0,0 +1,18 @@ +const React = require('react'); +const { useRouteFocused } = require('stremio-router'); +const { useServices } = require('stremio/services'); + +const useCoreEvent = (onEvent) => { + const { core } = useServices(); + const routeFocused = useRouteFocused(); + React.useLayoutEffect(() => { + if (routeFocused) { + core.on('Event', onEvent); + } + return () => { + core.off('Event', onEvent); + }; + }, [routeFocused, onEvent]); +}; + +module.exports = useCoreEvent; diff --git a/src/common/useDeepEqualEffect.js b/src/common/useDeepEqualEffect.js new file mode 100644 index 000000000..150bcf8a1 --- /dev/null +++ b/src/common/useDeepEqualEffect.js @@ -0,0 +1,17 @@ +const React = require('react'); +const isEqual = require('lodash.isequal'); + +const useDeepEqualEffect = (cb, deps) => { + const mountedRef = React.useRef(false); + const depsRef = React.useRef(null); + React.useEffect(() => { + if (!mountedRef.current || !isEqual(depsRef.current, deps)) { + cb(); + } + + mountedRef.current = true; + depsRef.current = deps; + }, [deps]); +}; + +module.exports = useDeepEqualEffect; diff --git a/src/common/useModelState.js b/src/common/useModelState.js index 9d949df36..277b1c943 100644 --- a/src/common/useModelState.js +++ b/src/common/useModelState.js @@ -8,7 +8,7 @@ const UNLOAD_ACTION = { action: 'Unload', }; -const useModelState = ({ model, action, timeout, onNewState, map, mapWithCtx, init }) => { +const useModelState = ({ model, init, action, timeout, onNewState, map, mapWithCtx }) => { const modelRef = React.useRef(model); const mountedRef = React.useRef(false); const { core } = useServices(); @@ -52,7 +52,7 @@ const useModelState = ({ model, action, timeout, onNewState, map, mapWithCtx, in onNewStateThrottled.cancel(); core.off('NewState', onNewStateThrottled); }; - }, [routeFocused]); + }, [routeFocused, timeout, onNewState, map, mapWithCtx]); React.useLayoutEffect(() => { mountedRef.current = true; }, []); diff --git a/src/common/useUser.js b/src/common/useUser.js deleted file mode 100644 index 36ff01acd..000000000 --- a/src/common/useUser.js +++ /dev/null @@ -1,31 +0,0 @@ -const React = require('react'); -const { useServices } = require('stremio/services'); -const useModelState = require('stremio/common/useModelState'); - -const mapUserState = (ctx) => { - return ctx.content.auth ? ctx.content.auth.user : null; -}; - -const useUser = () => { - const { core } = useServices(); - const logout = React.useCallback(() => { - core.dispatch({ - action: 'UserOp', - args: { - userOp: 'Logout' - } - }); - }, []); - const initUserState = React.useCallback(() => { - const ctx = core.getState('ctx'); - return mapUserState(ctx); - }, []); - const user = useModelState({ - model: 'ctx', - map: mapUserState, - init: initUserState - }); - return [user, logout]; -}; - -module.exports = useUser; diff --git a/src/index.js b/src/index.js index a4c6e3704..24f9ad414 100755 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,10 @@ const React = require('react'); const ReactDOM = require('react-dom'); +const Sentry = require('@sentry/browser'); const App = require('./App'); +if (typeof process.env.SENTRY_DSN === 'string') { + Sentry.init({ dsn: process.env.SENTRY_DSN }); +} + ReactDOM.render(, document.getElementById('app')); diff --git a/src/router/Modal/Modal.js b/src/router/Modal/Modal.js index dc26b3f12..4b22a133c 100644 --- a/src/router/Modal/Modal.js +++ b/src/router/Modal/Modal.js @@ -5,10 +5,10 @@ const classnames = require('classnames'); const FocusLock = require('react-focus-lock').default; const { useModalsContainer } = require('../ModalsContainerContext'); -const Modal = ({ className, autoFocus, children, ...props }) => { +const Modal = ({ className, autoFocus, disabled, children, ...props }) => { const modalsContainer = useModalsContainer(); return ReactDOM.createPortal( - + {children} , modalsContainer @@ -18,6 +18,7 @@ const Modal = ({ className, autoFocus, children, ...props }) => { Modal.propTypes = { className: PropTypes.string, autoFocus: PropTypes.bool, + disabled: PropTypes.bool, children: PropTypes.node }; diff --git a/src/routes/Addons/Addon/Addon.js b/src/routes/Addons/Addon/Addon.js index d646ed2b9..3c97cd1bb 100644 --- a/src/routes/Addons/Addon/Addon.js +++ b/src/routes/Addons/Addon/Addon.js @@ -105,7 +105,7 @@ Addon.propTypes = { installed: PropTypes.bool, onToggle: PropTypes.func, onShare: PropTypes.func, - dataset: PropTypes.objectOf(PropTypes.string) + dataset: PropTypes.object }; module.exports = Addon; diff --git a/src/routes/Addons/useAddons.js b/src/routes/Addons/useAddons.js index cd0fae6f9..d92afbed9 100644 --- a/src/routes/Addons/useAddons.js +++ b/src/routes/Addons/useAddons.js @@ -21,16 +21,16 @@ const mapAddonsStateWithCtx = (addons, ctx) => { ...addons.catalog_resource, content: { ...addons.catalog_resource.content, - content: addons.catalog_resource.content.content.map((descriptor) => ({ - transportUrl: descriptor.transportUrl, - installed: ctx.content.addons.some((addon) => addon.transportUrl === descriptor.transportUrl), + content: addons.catalog_resource.content.content.map((addon) => ({ + transportUrl: addon.transportUrl, + installed: ctx.profile.addons.some(({ transportUrl }) => transportUrl === addon.transportUrl), manifest: { - id: descriptor.manifest.id, - name: descriptor.manifest.name, - version: descriptor.manifest.version, - logo: descriptor.manifest.logo, - description: descriptor.manifest.description, - types: descriptor.manifest.types + id: addon.manifest.id, + name: addon.manifest.name, + version: addon.manifest.version, + logo: addon.manifest.logo, + description: addon.manifest.description, + types: addon.manifest.types } })) } @@ -45,8 +45,10 @@ const onNewAddonsState = (addons) => { return { action: 'Load', args: { - load: 'CatalogFiltered', - args: addons.selectable.catalogs[0].load_request + model: 'CatalogFiltered', + args: { + request: addons.selectable.catalogs[0].request + } } }; } @@ -59,14 +61,16 @@ const useAddons = (urlParams) => { return { action: 'Load', args: { - load: 'CatalogFiltered', + model: 'CatalogFiltered', args: { - base: urlParams.transportUrl, - path: { - resource: 'addon_catalog', - type_name: urlParams.type, - id: urlParams.catalogId, - extra: [] + request: { + base: urlParams.transportUrl, + path: { + resource: 'addon_catalog', + type_name: urlParams.type, + id: urlParams.catalogId, + extra: [] + } } } } @@ -77,8 +81,10 @@ const useAddons = (urlParams) => { return { action: 'Load', args: { - load: 'CatalogFiltered', - args: addons.selectable.catalogs[0].load_request + model: 'CatalogFiltered', + args: { + request: addons.selectable.catalogs[0].request + } } }; } else { diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index b545a4450..8ca4145e5 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -2,7 +2,8 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); -const { Button, MainNavBar, MetaItem, MetaPreview, Multiselect, ModalDialog, PaginationInput, useBinaryState } = require('stremio/common'); +const { AddonDetailsModal, Button, MainNavBar, MetaItem, MetaPreview, Multiselect, ModalDialog, PaginationInput, useBinaryState } = require('stremio/common'); +const { useServices } = require('stremio/services'); const useDiscover = require('./useDiscover'); const useSelectableInputs = require('./useSelectableInputs'); const styles = require('./styles'); @@ -19,9 +20,12 @@ const getMetaItemAtIndex = (catalog_resource, index) => { }; const Discover = ({ urlParams, queryParams }) => { + const { core } = useServices(); + const state = core.getState(); const discover = useDiscover(urlParams, queryParams); const [selectInputs, paginationInput] = useSelectableInputs(discover); const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false); + const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false); const [selectedMetaItem, setSelectedMetaItem] = React.useState(() => { return getMetaItemAtIndex(discover.catalog_resource, 0); }); @@ -70,6 +74,17 @@ const Discover = ({ urlParams, queryParams }) => { null }
+ { + discover.catalog_resource != null && !state.ctx.content.addons.some((addon) => addon.transportUrl === discover.catalog_resource.request.base) ? +
+
This addon is not installed. Install now?
+ +
+ : + null + }
{ discover.selectable.types.length === 0 && discover.catalog_resource === null ? @@ -141,6 +156,15 @@ const Discover = ({ urlParams, queryParams }) => { : null } + { + addonModalOpen ? + + : + null + }
); }; diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index b1bbef1d4..7042188c1 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -30,9 +30,10 @@ align-self: stretch; display: grid; grid-template-columns: 1fr 28rem; - grid-template-rows: auto 1fr; + grid-template-rows: auto auto 1fr; grid-template-areas: "selectable-inputs-area meta-preview-area" + "missing-addon-warning-container meta-preview-area" "catalog-content-area meta-preview-area"; .selectable-inputs-container { @@ -105,6 +106,38 @@ } } + .missing-addon-warning-container { + grid-area: missing-addon-warning-container; + display: flex; + flex-direction: column; + align-items: center; + padding: 0 1.5rem 1.5rem 1.5rem; + + .warning-info { + margin-bottom: 1rem; + font-size: 1.2rem; + color: var(--color-surfacelighter); + } + + .install-button { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 10rem; + padding: 1rem; + background-color: var(--color-signal5); + + &:hover { + filter: brightness(1.2); + } + + .label { + color: var(--color-surfacelighter); + } + } + } + .catalog-content-container { grid-area: catalog-content-area; @@ -229,9 +262,10 @@ .discover-container { .discover-content { grid-template-columns: 1fr; - grid-template-rows: auto 1fr; + grid-template-rows: auto auto 1fr; grid-template-areas: "selectable-inputs-area" + "missing-addon-warning-container" "catalog-content-area"; .catalog-content-container { diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 3e8347db0..223294e12 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -3,7 +3,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); const { useRouteFocused } = require('stremio-router'); -const { Button } = require('stremio/common'); +const { Button, useCoreEvent } = require('stremio/common'); const { useServices } = require('stremio/services'); const CredentialsTextInput = require('./CredentialsTextInput'); const ConsentCheckbox = require('./ConsentCheckbox'); @@ -71,29 +71,21 @@ const Intro = ({ queryParams }) => { error: '' } ); - React.useEffect(() => { - const onEvent = ({ event, args }) => { - switch (event) { - case 'CtxActionErr': { - const [, error] = args; - dispatch({ type: 'error', error: error.args.message }); - break; - } - case 'CtxChanged': { - const state = core.getState(); - if (state.ctx.content.auth !== null) { - window.location.replace('#/'); - } - } + useCoreEvent(React.useCallback(({ event, args }) => { + switch (event) { + case 'UserAuthenticated': { + window.location.replace('#/'); + break; + } + case 'Error': { + if (args.source.event === 'UserAuthenticated') { + // TODO use error.code to match translated message; + dispatch({ type: 'error', error: args.error.message }); + } + break; } - }; - if (routeFocused) { - core.on('Event', onEvent); } - return () => { - core.off('Event', onEvent); - }; - }, [routeFocused]); + }, [])); const loginWithFacebook = React.useCallback(() => { FB.login((response) => { if (response.status === 'connected') { @@ -131,10 +123,11 @@ const Intro = ({ queryParams }) => { return; } core.dispatch({ - action: 'UserOp', + action: 'Ctx', args: { - userOp: 'Login', + action: 'Authenticate', args: { + type: 'Login', email: state.email, password: state.password } @@ -147,9 +140,9 @@ const Intro = ({ queryParams }) => { return; } core.dispatch({ - action: 'UserOp', + action: 'Ctx', args: { - userOp: 'Logout' + action: 'Logout' } }); window.location.replace('#/'); @@ -176,10 +169,11 @@ const Intro = ({ queryParams }) => { return; } core.dispatch({ - action: 'UserOp', + action: 'Ctx', args: { - userOp: 'Register', + action: 'Authenticate', args: { + type: 'Register', email: state.email, password: state.password, gdpr_consent: { @@ -236,6 +230,11 @@ const Intro = ({ queryParams }) => { const toggleMarketingAccepted = React.useCallback(() => { dispatch({ type: 'toggle-checkbox', name: 'marketingAccepted' }); }, []); + const switchFormOnClick = React.useCallback(() => { + const nextQueryParams = new URLSearchParams(queryParams); + nextQueryParams.set('form', state.form === SIGNUP_FORM ? LOGIN_FORM : SIGNUP_FORM); + window.location.replace(`#/intro?${nextQueryParams}`); + }, [queryParams, state.form]); React.useEffect(() => { if ([LOGIN_FORM, SIGNUP_FORM].includes(queryParams.get('form'))) { dispatch({ type: 'set-form', form: queryParams.get('form') }); @@ -258,7 +257,6 @@ const Intro = ({ queryParams }) => {
Continue with Facebook
-
We won't post anything on your behalf
{ : null } -
diff --git a/src/routes/Intro/styles.less b/src/routes/Intro/styles.less index 99c2419b4..872512687 100644 --- a/src/routes/Intro/styles.less +++ b/src/routes/Intro/styles.less @@ -47,6 +47,7 @@ .facebook-button { min-height: 4.5rem; + margin-bottom: 2rem; background: var(--color-facebook); &:hover, &:focus { @@ -58,13 +59,6 @@ } } - .facebook-statement { - margin-top: 0.5rem; - margin-bottom: 2rem; - text-align: center; - color: var(--color-surface); - } - .credentials-text-input { display: block; width: 100%; diff --git a/src/routes/Player/Video/Video.js b/src/routes/Player/Video/Video.js index bd34ece5d..3f15b1c85 100644 --- a/src/routes/Player/Video/Video.js +++ b/src/routes/Player/Video/Video.js @@ -1,7 +1,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const hat = require('hat'); -const useVideoImplementation = require('./useVideoImplementation'); +const selectVideoImplementation = require('./selectVideoImplementation'); const Video = React.forwardRef(({ className, ...props }, ref) => { const [onEnded, onError, onPropValue, onPropChanged, onImplementationChanged] = React.useMemo(() => [ @@ -16,7 +16,7 @@ const Video = React.forwardRef(({ className, ...props }, ref) => { const id = React.useMemo(() => `video-${hat()}`, []); const dispatch = React.useCallback((args) => { if (args && args.commandName === 'load' && args.commandArgs) { - const Video = useVideoImplementation(args.commandArgs.shell, args.commandArgs.stream); + const Video = selectVideoImplementation(args.commandArgs.shell, args.commandArgs.stream); if (typeof Video !== 'function') { videoRef.current = null; } else if (videoRef.current === null || videoRef.current.constructor !== Video) { diff --git a/src/routes/Player/Video/useVideoImplementation.js b/src/routes/Player/Video/selectVideoImplementation.js similarity index 74% rename from src/routes/Player/Video/useVideoImplementation.js rename to src/routes/Player/Video/selectVideoImplementation.js index 89b41e33d..0fd9e01af 100644 --- a/src/routes/Player/Video/useVideoImplementation.js +++ b/src/routes/Player/Video/selectVideoImplementation.js @@ -1,6 +1,6 @@ const { HTMLVideo, YouTubeVideo, MPVVideo } = require('stremio-video'); -const useVideoImplementation = (shell, stream) => { +const selectVideoImplementation = (shell, stream) => { if (shell) { return MPVVideo; } @@ -16,4 +16,4 @@ const useVideoImplementation = (shell, stream) => { return null; }; -module.exports = useVideoImplementation; +module.exports = selectVideoImplementation; diff --git a/src/routes/Player/usePlayer.js b/src/routes/Player/usePlayer.js index 6a692b192..321cd43b0 100644 --- a/src/routes/Player/usePlayer.js +++ b/src/routes/Player/usePlayer.js @@ -1,13 +1,51 @@ const React = require('react'); const { useModelState } = require('stremio/common'); -const initPlayer = () => ({ - selected: null, +const initPlayerState = () => ({ + selected: { + transport_url: null, + type_name: null, + id: null, + video_id: null, + stream: null, + }, meta_resource: null, subtitles_resources: [], next_video: null }); +const mapPlayerStateWithCtx = (player, ctx) => { + const selected = player.selected; + const meta_resource = player.meta_resource; + const subtitles_resources = player.subtitles_resources.map((subtitles_resource) => { + if (subtitles_resource.content.type === 'Ready') { + const origin = ctx.content.addons.reduce((origin, addon) => { + if (addon.transportUrl === subtitles_resource.request.base) { + return typeof addon.manifest.name === 'string' && addon.manifest.name.length > 0 ? + addon.manifest.name + : + addon.manifest.id; + } + + return origin; + }, subtitles_resource.request.base); + subtitles_resource.content.content = subtitles_resource.content.content.map((subtitles) => ({ + ...subtitles, + origin + })); + } + + return subtitles_resource; + }, []); + const next_video = player.next_video; + return { + selected, + meta_resource, + subtitles_resources, + next_video + }; +}; + const usePlayer = (urlParams) => { const loadPlayerAction = React.useMemo(() => { try { @@ -34,7 +72,8 @@ const usePlayer = (urlParams) => { return useModelState({ model: 'player', action: loadPlayerAction, - init: initPlayer + init: initPlayerState, + mapWithCtx: mapPlayerStateWithCtx }); }; diff --git a/src/routes/Player/useSubtitlesSettings.js b/src/routes/Player/useSubtitlesSettings.js new file mode 100644 index 000000000..af9217836 --- /dev/null +++ b/src/routes/Player/useSubtitlesSettings.js @@ -0,0 +1,25 @@ +const React = require('react'); +const { useModelState } = require('stremio/common'); +const { useServices } = require('stremio/services'); + +const mapSubtitlesSettings = (ctx) => ({ + size: ctx.content.settings.subtitles_size, + text_color: ctx.content.settings.subtitles_text_color, + background_color: ctx.content.settings.subtitles_background_color, + outline_color: ctx.content.settings.subtitles_outline_color, +}); + +const useSubtitlesSettings = () => { + const { core } = useServices(); + const initSubtitlesSettings = React.useCallback(() => { + const ctx = core.getState('ctx'); + return mapSubtitlesSettings(ctx); + }, []); + return useModelState({ + model: 'ctx', + map: mapSubtitlesSettings, + init: initSubtitlesSettings + }); +}; + +module.exports = useSubtitlesSettings; diff --git a/storybook/RouterDecorator/RouterDecorator.js b/storybook/RouterDecorator/RouterDecorator.js index 6a3d3a717..d2997fd3b 100644 --- a/storybook/RouterDecorator/RouterDecorator.js +++ b/storybook/RouterDecorator/RouterDecorator.js @@ -2,20 +2,23 @@ const React = require('react'); const classnames = require('classnames'); const Route = require('stremio-router/Route'); const { RouteFocusedProvider } = require('stremio-router/RouteFocusedContext'); +const { ToastsContainerProvider } = require('stremio/common/Toasts/ToastsContainerContext'); const appStyles = require('stremio/App/styles'); const styles = require('./styles'); const RouterDecorator = ({ children }) => (
-
- - -
- {children} -
-
-
-
+ +
+ + +
+ {children} +
+
+
+
+
); diff --git a/storybook/stories/Toast/SimpleToast/SimpleToast.js b/storybook/stories/Toast/SimpleToast/SimpleToast.js new file mode 100644 index 000000000..e5e3ed388 --- /dev/null +++ b/storybook/stories/Toast/SimpleToast/SimpleToast.js @@ -0,0 +1,42 @@ +const React = require('react'); +const { storiesOf } = require('@storybook/react'); +const { Toasts } = require('stremio/common'); +const styles = require('./styles'); + +storiesOf('Toast', module).add('SimpleToast', () => { + const toastRef = React.useRef(null); + + const showToast = React.useCallback((message) => { + toastRef.current.show({ + title: 'Something to take your attention', + timeout: 0, + type: 'info', + icon: 'ic_sub', + closeButton: true, + ...message + }); + }, [toastRef.current]); + + const clickSuccess = () => showToast({ + title: 'You clicked it', + text: 'Congratulations! Click event handled successfully.', + type: 'success', + icon: 'ic_check', + timeout: 2e3 + }); + + return ( +
+ + + + + + + + + + +
+ ); +}); diff --git a/storybook/stories/Toast/SimpleToast/index.js b/storybook/stories/Toast/SimpleToast/index.js new file mode 100644 index 000000000..ac1b818ff --- /dev/null +++ b/storybook/stories/Toast/SimpleToast/index.js @@ -0,0 +1 @@ +require('./SimpleToast'); \ No newline at end of file diff --git a/storybook/stories/Toast/SimpleToast/styles.less b/storybook/stories/Toast/SimpleToast/styles.less new file mode 100644 index 000000000..e021c2764 --- /dev/null +++ b/storybook/stories/Toast/SimpleToast/styles.less @@ -0,0 +1,16 @@ +.root-container { + width: 100%; + height: 100%; + color: var(--color-surfacelighter); + + button { + color: var(--color-surfacelighter); + background-color: var(--color-primarydark); + padding: .5rem; + margin: .5rem; + } +} + +.toasts-container { + width: 30rem; +} \ No newline at end of file diff --git a/storybook/stories/Toast/index.js b/storybook/stories/Toast/index.js new file mode 100644 index 000000000..ab8a632c3 --- /dev/null +++ b/storybook/stories/Toast/index.js @@ -0,0 +1 @@ +require('./SimpleToast'); diff --git a/storybook/stories/index.js b/storybook/stories/index.js index 9e66e8536..7562692e3 100644 --- a/storybook/stories/index.js +++ b/storybook/stories/index.js @@ -11,3 +11,4 @@ require('./Popup'); require('./SeasonsBar'); require('./SharePrompt'); require('./Stream'); +require('./Toast'); diff --git a/tests/routesRegexp.spec.js b/tests/routesRegexp.spec.js index 8f398f127..e81165204 100644 --- a/tests/routesRegexp.spec.js +++ b/tests/routesRegexp.spec.js @@ -514,8 +514,8 @@ describe('routesRegexp', () => { }); it('not match /player/', async () => { - expect('/player/'.match(routesRegexp.player.regexp)) - .toBe(null); + expect(Array.from('/player/'.match(routesRegexp.player.regexp))) + .toEqual(['/player/', '', undefined, undefined, undefined, undefined]); }); it('not match /player//', async () => { @@ -534,8 +534,8 @@ describe('routesRegexp', () => { }); it('not match /player/1', async () => { - expect('/player/1'.match(routesRegexp.player.regexp)) - .toBe(null); + expect(Array.from('/player/1'.match(routesRegexp.player.regexp))) + .toEqual(['/player/1', '1', undefined, undefined, undefined, undefined]); }); it('not match /player/1/', async () => { diff --git a/webpack.config.js b/webpack.config.js index 466f5cc30..cf679670e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,8 +6,11 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); -module.exports = { +module.exports = (env, argv) => ({ entry: './src/index.js', + output: { + path: path.join(__dirname, 'build') + }, module: { rules: [ { @@ -125,6 +128,10 @@ module.exports = { ] }, plugins: [ + new webpack.EnvironmentPlugin({ + DEBUG: argv.mode !== 'production', + ...env + }), new webpack.ProgressPlugin(), new CopyWebpackPlugin([ { from: 'node_modules/stremio-core-web/static', to: '' }, @@ -142,4 +149,4 @@ module.exports = { cleanAfterEveryBuildPatterns: ['./main.js', './main.css'] }) ] -}; +}); diff --git a/yarn.lock b/yarn.lock index 6ed5ab0f5..73917d842 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1240,6 +1240,58 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" +"@sentry/browser@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.1.tgz#337ffcb52711b23064c847a07629e966f54a5ebb" + integrity sha512-oqOX/otmuP92DEGRyZeBuQokXdeT9HQRxH73oqIURXXNLMP3PWJALSb4HtT4AftEt/2ROGobZLuA4TaID6My/Q== + dependencies: + "@sentry/core" "5.11.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + tslib "^1.9.3" + +"@sentry/core@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.11.1.tgz#9e2da485e196ae32971545c1c49ee6fe719930e2" + integrity sha512-BpvPosVNT20Xso4gAV54Lu3KqDmD20vO63HYwbNdST5LUi8oYV4JhvOkoBraPEM2cbBwQvwVcFdeEYKk4tin9A== + dependencies: + "@sentry/hub" "5.11.1" + "@sentry/minimal" "5.11.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + tslib "^1.9.3" + +"@sentry/hub@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.11.1.tgz#ddcb865563fae53852d405885c46b4c6de68a91b" + integrity sha512-ucKprYCbGGLLjVz4hWUqHN9KH0WKUkGf5ZYfD8LUhksuobRkYVyig0ZGbshECZxW5jcDTzip4Q9Qimq/PkkXBg== + dependencies: + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + tslib "^1.9.3" + +"@sentry/minimal@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.11.1.tgz#0e705d01a567282d8fbbda2aed848b4974cc3cec" + integrity sha512-HK8zs7Pgdq7DsbZQTThrhQPrJsVWzz7MaluAbQA0rTIAJ3TvHKQpsVRu17xDpjZXypqWcKCRsthDrC4LxDM1Bg== + dependencies: + "@sentry/hub" "5.11.1" + "@sentry/types" "5.11.0" + tslib "^1.9.3" + +"@sentry/types@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.11.0.tgz#40f0f3174362928e033ddd9725d55e7c5cb7c5b6" + integrity sha512-1Uhycpmeo1ZK2GLvrtwZhTwIodJHcyIS6bn+t4IMkN9MFoo6ktbAfhvexBDW/IDtdLlCGJbfm8nIZerxy0QUpg== + +"@sentry/utils@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.11.1.tgz#aa19fcc234cf632257b2281261651d2fac967607" + integrity sha512-O0Zl4R2JJh8cTkQ8ZL2cDqGCmQdpA5VeXpuBbEl1v78LQPkBDISi35wH4mKmLwMsLBtTVpx2UeUHBj0KO5aLlA== + dependencies: + "@sentry/types" "5.11.0" + tslib "^1.9.3" + "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" @@ -4420,16 +4472,16 @@ dotenv-webpack@^1.7.0: dependencies: dotenv-defaults "^1.0.2" +dotenv@8.2.0, dotenv@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + dotenv@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w== -dotenv@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"