diff --git a/package.json b/package.json
index d139ffb..04bb997 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"events": "^3.3.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
+ "normalize.css": "^8.0.1",
"postcss-preset-env": "^6.7.0",
"re-resizable": "^6.9.0",
"react": "^17.0.0",
diff --git a/src/Header.jsx b/src/Header.jsx
index 3eb83b7..4226746 100644
--- a/src/Header.jsx
+++ b/src/Header.jsx
@@ -31,10 +31,15 @@ export function LeftNav({ children, className, hideMobile, ...rest }) {
);
}
-export function RightNav({ children, className, ...rest }) {
+export function RightNav({ children, className, hideMobile, ...rest }) {
return (
{children}
@@ -42,9 +47,9 @@ export function RightNav({ children, className, ...rest }) {
);
}
-export function HeaderLogo() {
+export function HeaderLogo({ className }) {
return (
-
+
);
diff --git a/src/Header.module.css b/src/Header.module.css
index 3baba13..734d445 100644
--- a/src/Header.module.css
+++ b/src/Header.module.css
@@ -42,6 +42,10 @@
margin-right: 24px;
}
+.rightNav.hideMobile {
+ display: none;
+}
+
.nav > :last-child {
margin-right: 0;
}
@@ -103,7 +107,8 @@
@media (min-width: 800px) {
.headerLogo,
.roomAvatar,
- .leftNav.hideMobile {
+ .leftNav.hideMobile,
+ .rightNav.hideMobile {
display: flex;
}
diff --git a/src/Home.jsx b/src/Home.jsx
index e19007a..d26878f 100644
--- a/src/Home.jsx
+++ b/src/Home.jsx
@@ -14,399 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useCallback, useState, useRef, useEffect } from "react";
-import { useHistory, Link } from "react-router-dom";
-import {
- useClient,
- useGroupCallRooms,
- usePublicRooms,
- createRoom,
- roomAliasFromRoomName,
- useInteractiveRegistration,
-} from "./ConferenceCallManagerHooks";
-import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
-import styles from "./Home.module.css";
-import { FieldRow, InputField, ErrorMessage } from "./Input";
-import { UserMenu } from "./UserMenu";
-import { Button } from "./button";
-import { CallList } from "./CallList";
-import classNames from "classnames";
+import React from "react";
+import { useClient } from "./ConferenceCallManagerHooks";
import { ErrorView, LoadingView } from "./FullScreenView";
-import { useModalTriggerState } from "./Modal";
-import { randomString } from "matrix-js-sdk/src/randomstring";
-import { JoinExistingCallModal } from "./JoinExistingCallModal";
-import { RecaptchaInput } from "./RecaptchaInput";
-import { UserMenuContainer } from "./UserMenuContainer";
+import { UnauthenticatedView } from "./home/UnauthenticatedView";
+import { RegisteredView } from "./home/RegisteredView";
export function Home() {
- const {
- isAuthenticated,
- isGuest,
- isPasswordlessUser,
- loading,
- error,
- client,
- } = useClient();
-
- const [{ privacyPolicyUrl, recaptchaKey }, register] =
- useInteractiveRegistration();
- const [recaptchaResponse, setRecaptchaResponse] = useState();
-
- const history = useHistory();
- const [creatingRoom, setCreatingRoom] = useState(false);
- const [createRoomError, setCreateRoomError] = useState();
- const { modalState, modalProps } = useModalTriggerState();
- const [existingRoomId, setExistingRoomId] = useState();
-
- const onCreateRoom = useCallback(
- (e) => {
- e.preventDefault();
- const data = new FormData(e.target);
- const roomName = data.get("roomName");
- const userName = data.get("userName");
-
- async function onCreateRoom() {
- let _client = client;
-
- if (!recaptchaResponse) {
- return;
- }
-
- if (!_client || isGuest) {
- _client = await register(
- userName,
- randomString(16),
- recaptchaResponse,
- true
- );
- }
-
- const roomIdOrAlias = await createRoom(_client, roomName);
-
- if (roomIdOrAlias) {
- history.push(`/room/${roomIdOrAlias}`);
- }
- }
-
- setCreateRoomError(undefined);
- setCreatingRoom(true);
-
- return onCreateRoom().catch((error) => {
- if (error.errcode === "M_ROOM_IN_USE") {
- setExistingRoomId(roomAliasFromRoomName(roomName));
- setCreateRoomError(undefined);
- modalState.open();
- } else {
- setCreateRoomError(error);
- }
-
- setCreatingRoom(false);
- });
- },
- [client, history, register, isGuest, recaptchaResponse]
- );
-
- const onJoinRoom = useCallback(
- (e) => {
- e.preventDefault();
- const data = new FormData(e.target);
- const roomId = data.get("roomId");
- history.push(`/${roomId}`);
- },
- [history]
- );
-
- const onJoinExistingRoom = useCallback(() => {
- history.push(`/${existingRoomId}`);
- }, [history, existingRoomId]);
+ const { isAuthenticated, isPasswordlessUser, loading, error, client } =
+ useClient();
if (loading) {
return
;
} else if (error) {
return
;
} else {
- return (
- <>
- {!isAuthenticated || isGuest ? (
-
- ) : (
-
- )}
- {modalState.isOpen && (
-
- )}
- >
+ return isAuthenticated ? (
+
+ ) : (
+
);
}
}
-
-function UnregisteredView({
- onCreateRoom,
- createRoomError,
- creatingRoom,
- onJoinRoom,
- privacyPolicyUrl,
- recaptchaKey,
- setRecaptchaResponse,
-}) {
- const acceptTermsRef = useRef();
- const [acceptTerms, setAcceptTerms] = useState(false);
-
- useEffect(() => {
- if (!acceptTermsRef.current) {
- return;
- }
-
- if (!acceptTerms) {
- acceptTermsRef.current.setCustomValidity(
- "You must accept the terms to continue."
- );
- } else {
- acceptTermsRef.current.setCustomValidity("");
- }
- }, [acceptTerms]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- Not registered yet?{" "}
- Create an account
-
-
-
-
-
-
-
- );
-}
-
-function RegisteredView({
- client,
- isPasswordlessUser,
- isGuest,
- onCreateRoom,
- createRoomError,
- creatingRoom,
- onJoinRoom,
-}) {
- const publicRooms = usePublicRooms(
- client,
- import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
- );
- const recentRooms = useGroupCallRooms(client);
-
- const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
-
- return (
-
-
-
-
-
-
-
-
-
- {(isPasswordlessUser || isGuest) && (
-
-
- Not registered yet?{" "}
- Create an account
-
-
- )}
-
-
-
- {!hideCallList && (
-
-
- {publicRooms.length > 0 && (
-
- )}
- {recentRooms.length > 0 && (
-
- )}
-
-
- )}
-
-
- );
-}
diff --git a/src/ProfileModal.jsx b/src/ProfileModal.jsx
index 9dd15f9..6378f59 100644
--- a/src/ProfileModal.jsx
+++ b/src/ProfileModal.jsx
@@ -8,7 +8,6 @@ export function ProfileModal({
client,
isAuthenticated,
isPasswordlessUser,
- isGuest,
...rest
}) {
const { onClose } = rest;
@@ -66,7 +65,7 @@ export function ProfileModal({
onChange={onChangeDisplayName}
/>
- {isAuthenticated && !isGuest && !isPasswordlessUser && (
+ {isAuthenticated && !isPasswordlessUser && (
{
if (!loading && !isAuthenticated) {
- setRegisteringGuest(true);
-
- registerGuest()
- .then(() => {
- setRegisteringGuest(false);
- })
- .catch((error) => {
- setRegistrationError(error);
- setRegisteringGuest(false);
- });
+ setRegistrationError(new Error("Must be registered"));
}
}, [loading, isAuthenticated]);
- if (loading || registeringGuest) {
+ if (loading) {
return ;
}
@@ -95,16 +78,10 @@ export function Room() {
return ;
}
- return (
-
- );
+ return ;
}
-export function GroupCall({ client, isGuest, isPasswordlessUser }) {
+export function GroupCall({ client, isPasswordlessUser }) {
const { roomId: maybeRoomId } = useParams();
const { hash, search } = useLocation();
const [simpleGrid, viaServers] = useMemo(() => {
@@ -132,7 +109,6 @@ export function GroupCall({ client, isGuest, isPasswordlessUser }) {
return (
{
leave();
- if (!isGuest && !isPasswordlessUser) {
+ if (!isPasswordlessUser) {
history.push("/");
} else {
setLeft(true);
}
- }, [leave, history, isGuest]);
+ }, [leave, history]);
if (error) {
return ;
@@ -215,7 +190,6 @@ export function GroupCallView({
- {!isGuest && }
+
{items.length === 0 ? (
diff --git a/src/UserMenuContainer.jsx b/src/UserMenuContainer.jsx
index d9f2f5c..d70cb25 100644
--- a/src/UserMenuContainer.jsx
+++ b/src/UserMenuContainer.jsx
@@ -8,14 +8,8 @@ import { UserMenu } from "./UserMenu";
export function UserMenuContainer({ disableLogout }) {
const location = useLocation();
const history = useHistory();
- const {
- isAuthenticated,
- isGuest,
- isPasswordlessUser,
- logout,
- userName,
- client,
- } = useClient();
+ const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
+ useClient();
const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState();
@@ -52,7 +46,6 @@ export function UserMenuContainer({ disableLogout }) {
diff --git a/src/form/Form.jsx b/src/form/Form.jsx
new file mode 100644
index 0000000..c46064f
--- /dev/null
+++ b/src/form/Form.jsx
@@ -0,0 +1,11 @@
+import classNames from "classnames";
+import React, { forwardRef } from "react";
+import styles from "./Form.module.css";
+
+export const Form = forwardRef(({ children, className, ...rest }, ref) => {
+ return (
+
+ );
+});
diff --git a/src/form/Form.module.css b/src/form/Form.module.css
new file mode 100644
index 0000000..c1f7262
--- /dev/null
+++ b/src/form/Form.module.css
@@ -0,0 +1,4 @@
+.form {
+ display: flex;
+ flex-direction: column;
+}
diff --git a/src/home/RegisteredView.jsx b/src/home/RegisteredView.jsx
new file mode 100644
index 0000000..eabec9b
--- /dev/null
+++ b/src/home/RegisteredView.jsx
@@ -0,0 +1,131 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import {
+ useGroupCallRooms,
+ usePublicRooms,
+} from "../ConferenceCallManagerHooks";
+import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
+import styles from "../Home.module.css";
+import { FieldRow, InputField, ErrorMessage } from "../Input";
+import { Button } from "../button";
+import { CallList } from "../CallList";
+import classNames from "classnames";
+import { UserMenuContainer } from "../UserMenuContainer";
+
+export function RegisteredView({
+ client,
+ isPasswordlessUser,
+ onCreateRoom,
+ createRoomError,
+ creatingRoom,
+ onJoinRoom,
+}) {
+ const publicRooms = usePublicRooms(
+ client,
+ import.meta.env.VITE_PUBLIC_SPACE_ROOM_ID
+ );
+ const recentRooms = useGroupCallRooms(client);
+
+ const hideCallList = publicRooms.length === 0 && recentRooms.length === 0;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {isPasswordlessUser && (
+
+
+ Not registered yet?{" "}
+ Create an account
+
+
+ )}
+
+
+
+ {!hideCallList && (
+
+
+ {publicRooms.length > 0 && (
+
+ )}
+ {recentRooms.length > 0 && (
+
+ )}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/home/UnauthenticatedView.jsx b/src/home/UnauthenticatedView.jsx
new file mode 100644
index 0000000..8c0b684
--- /dev/null
+++ b/src/home/UnauthenticatedView.jsx
@@ -0,0 +1,154 @@
+import React, { useCallback, useState } from "react";
+import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
+import { UserMenuContainer } from "../UserMenuContainer";
+import { useHistory } from "react-router-dom";
+import { FieldRow, InputField, ErrorMessage } from "../Input";
+import { Button } from "../button";
+import { randomString } from "matrix-js-sdk/src/randomstring";
+import {
+ createRoom,
+ useInteractiveRegistration,
+} from "../ConferenceCallManagerHooks";
+import { useModalTriggerState } from "../Modal";
+import { JoinExistingCallModal } from "../JoinExistingCallModal";
+import { useRecaptcha } from "../useRecaptcha";
+import { Body, Caption, Title, Link, Headline } from "../typography/Typography";
+import { Form } from "../form/Form";
+import styles from "./UnauthenticatedView.module.css";
+
+export function UnauthenticatedView() {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState();
+ const [{ privacyPolicyUrl, recaptchaKey }, register] =
+ useInteractiveRegistration();
+ const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
+ const onSubmit = useCallback(
+ (e) => {
+ e.preventDefault();
+ const data = new FormData(e.target);
+ const roomName = data.get("roomName");
+ const userName = data.get("userName");
+
+ async function submit() {
+ setError(undefined);
+ setLoading(true);
+ const recaptchaResponse = await execute();
+ const client = await register(
+ userName,
+ randomString(16),
+ recaptchaResponse,
+ true
+ );
+
+ const roomIdOrAlias = await createRoom(client, roomName);
+
+ if (roomIdOrAlias) {
+ history.push(`/room/${roomIdOrAlias}`);
+ }
+ }
+
+ submit().catch((error) => {
+ if (error.errcode === "M_ROOM_IN_USE") {
+ setExistingRoomId(roomAliasFromRoomName(roomName));
+ setError(undefined);
+ modalState.open();
+ } else {
+ console.error(error);
+ setLoading(false);
+ setError(error);
+ reset();
+ }
+ });
+ },
+ [register]
+ );
+
+ const { modalState, modalProps } = useModalTriggerState();
+ const [existingRoomId, setExistingRoomId] = useState();
+ const history = useHistory();
+ const onJoinExistingRoom = useCallback(() => {
+ history.push(`/${existingRoomId}`);
+ }, [history, existingRoomId]);
+
+ return (
+ <>
+
+
+
+
+ Enter a call name
+
+
+
+
+ {modalState.isOpen && (
+
+ )}
+ >
+ );
+}
diff --git a/src/home/UnauthenticatedView.module.css b/src/home/UnauthenticatedView.module.css
new file mode 100644
index 0000000..254ddb6
--- /dev/null
+++ b/src/home/UnauthenticatedView.module.css
@@ -0,0 +1,63 @@
+.container {
+ display: flex;
+ min-height: calc(100% - 64px);
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.main {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.footer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 28px;
+}
+
+.footer p {
+ margin-bottom: 0;
+}
+
+.footer .mobileLoginLink {
+ display: flex;
+ margin-bottom: 24px;
+}
+
+.logo {
+ display: flex;
+ margin-bottom: 54px;
+}
+
+.headline {
+ margin-bottom: 40px;
+}
+
+.form {
+ padding: 0 24px;
+ justify-content: center;
+ max-width: 360px;
+}
+
+.form > * + * {
+ margin-bottom: 24px;
+}
+
+@media (min-width: 800px) {
+ .logo {
+ display: none;
+ }
+
+ .mobileLoginLink {
+ display: none;
+ }
+
+ .container {
+ min-height: calc(100% - 76px);
+ }
+}
diff --git a/src/index.css b/src/index.css
index a665b9c..53f1746 100644
--- a/src/index.css
+++ b/src/index.css
@@ -20,6 +20,8 @@ limitations under the License.
Therefore we define a unicode-range to load which excludes the glyphs
(to avoid having to maintain a fork of Inter). */
+@import "normalize.css/normalize.css";
+
:root {
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
@@ -35,6 +37,7 @@ limitations under the License.
--textColor4: #a9b2bc;
--inputBorderColor: #394049;
--inputBorderColorFocused: #0086e6;
+ --linkColor: #0086e6;
}
@font-face {
@@ -139,6 +142,44 @@ body,
flex-direction: column;
}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+a {
+ margin-top: 0;
+}
+
+/* Headline Semi Bold */
+h1 {
+ font-weight: 600;
+ font-size: 32px;
+ line-height: 39px;
+}
+
+/* Title */
+h2 {
+ font-weight: 600;
+ font-size: 24px;
+ line-height: 29px;
+}
+
+/* Subtitle */
+h3 {
+ font-weight: 400;
+ font-size: 18px;
+ line-height: 22px;
+}
+
+/* Body */
+p {
+ font-size: 15px;
+ line-height: 24px;
+}
+
a {
color: var(--primaryColor);
text-decoration: none;
@@ -179,3 +220,7 @@ details[open] > summary {
position: relative;
height: 100%;
}
+
+.grecaptcha-badge {
+ visibility: hidden;
+}
diff --git a/src/typography/Typography.jsx b/src/typography/Typography.jsx
new file mode 100644
index 0000000..9193a48
--- /dev/null
+++ b/src/typography/Typography.jsx
@@ -0,0 +1,123 @@
+import React, { forwardRef } from "react";
+import classNames from "classnames";
+import { Link as RouterLink } from "react-router-dom";
+import styles from "./Typography.module.css";
+
+export const Headline = forwardRef(
+ ({ as: Component = "h1", children, className, fontWeight, ...rest }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+export const Title = forwardRef(
+ ({ as: Component = "h2", children, className, fontWeight, ...rest }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+export const Subtitle = forwardRef(
+ ({ as: Component = "h3", children, className, fontWeight, ...rest }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+export const Body = forwardRef(
+ ({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+export const Caption = forwardRef(
+ ({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+export const Micro = forwardRef(
+ ({ as: Component = "p", children, className, fontWeight, ...rest }, ref) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+export const Link = forwardRef(
+ (
+ {
+ as: Component = RouterLink,
+ children,
+ className,
+ color = "link",
+ href,
+ fontWeight,
+ ...rest
+ },
+ ref
+ ) => {
+ let externalLinkProps;
+
+ if (href) {
+ externalLinkProps = {
+ target: "_blank",
+ rel: "noreferrer noopener",
+ };
+ }
+
+ return (
+
+ {children}
+
+ );
+ }
+);
diff --git a/src/typography/Typography.module.css b/src/typography/Typography.module.css
new file mode 100644
index 0000000..ca6e354
--- /dev/null
+++ b/src/typography/Typography.module.css
@@ -0,0 +1,29 @@
+.caption {
+ font-size: 12px;
+ line-height: 15px;
+}
+
+.micro {
+ font-size: 10px;
+ line-height: 12px;
+}
+
+.regular {
+ font-weight: 400;
+}
+
+.semiBold {
+ font-weight: 600;
+}
+
+.bold {
+ font-weight: 700;
+}
+
+.link {
+ color: var(--linkColor);
+}
+
+.primary {
+ color: var(--primaryColor);
+}
diff --git a/src/typography/Typography.stories.jsx b/src/typography/Typography.stories.jsx
new file mode 100644
index 0000000..e31b9c3
--- /dev/null
+++ b/src/typography/Typography.stories.jsx
@@ -0,0 +1,25 @@
+import React from "react";
+import { Headline, Title, Subtitle, Body, Caption, Micro } from "./Typography";
+
+export default {
+ title: "Typography",
+ parameters: {
+ layout: "fullscreen",
+ },
+};
+
+export const Typography = () => (
+ <>
+ Headline Semi Bold
+ Title
+ Subtitle
+ Subtitle Semi Bold
+ Body
+ Body Semi Bold
+ Caption
+ Caption Semi Bold
+ Caption Bold
+ Micro
+ Micro bold
+ >
+);
diff --git a/src/useRecaptcha.js b/src/useRecaptcha.js
new file mode 100644
index 0000000..a743656
--- /dev/null
+++ b/src/useRecaptcha.js
@@ -0,0 +1,69 @@
+import { randomString } from "matrix-js-sdk/src/randomstring";
+import { useEffect, useCallback, useRef, useState } from "react";
+
+const RECAPTCHA_SCRIPT_URL =
+ "https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
+
+export function useRecaptcha(sitekey) {
+ const [recaptchaId] = useState(() => randomString(16));
+ const resolvePromiseRef = useRef();
+
+ useEffect(() => {
+ if (!sitekey) {
+ return;
+ }
+
+ const onRecaptchaLoaded = () => {
+ if (!document.getElementById(recaptchaId)) {
+ return;
+ }
+
+ window.grecaptcha.render(recaptchaId, {
+ sitekey,
+ size: "invisible",
+ callback: (response) => {
+ if (resolvePromiseRef.current) {
+ resolvePromiseRef.current(response);
+ }
+ },
+ });
+ };
+
+ if (
+ typeof window.grecaptcha !== "undefined" &&
+ typeof window.grecaptcha.render === "function"
+ ) {
+ onRecaptchaLoaded();
+ } else {
+ window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
+
+ if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
+ const scriptTag = document.createElement("script");
+ scriptTag.src = RECAPTCHA_SCRIPT_URL;
+ scriptTag.async = true;
+ document.body.appendChild(scriptTag);
+ }
+ }
+ }, [recaptchaId, sitekey]);
+
+ const execute = useCallback(() => {
+ if (!window.grecaptcha) {
+ return Promise.reject(new Error("Recaptcha not loaded"));
+ }
+
+ return new Promise((resolve) => {
+ resolvePromiseRef.current = resolve;
+ window.grecaptcha.execute();
+ });
+ }, [recaptchaId]);
+
+ const reset = useCallback(() => {
+ if (window.grecaptcha) {
+ window.grecaptcha.reset();
+ }
+ }, [recaptchaId]);
+
+ console.log(recaptchaId);
+
+ return { execute, reset, recaptchaId };
+}
diff --git a/yarn.lock b/yarn.lock
index 82ede08..73e7c5c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9155,6 +9155,11 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+normalize.css@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
+ integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
+
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"