Merge pull request #1948 from SkynetLabs/dashboard-v2-portal-aware-subscription-plans

Dashboard v2 portal aware subscription plans
This commit is contained in:
Karol Wypchło 2022-03-31 10:31:41 +02:00 committed by GitHub
commit 25d573b9ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 468 additions and 129 deletions

View File

@ -7,13 +7,16 @@ import "@fontsource/source-sans-pro/400.css"; // normal
import "@fontsource/source-sans-pro/600.css"; // semibold
import "./src/styles/global.css";
import { MODAL_ROOT_ID } from "./src/components/Modal";
import { PortalSettingsProvider } from "./src/contexts/portal-settings";
export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment;
return (
<Layout {...props}>
{element}
<div id={MODAL_ROOT_ID} />
</Layout>
<PortalSettingsProvider>
<Layout {...props}>
{element}
<div id={MODAL_ROOT_ID} />
</Layout>
</PortalSettingsProvider>
);
}

View File

@ -7,13 +7,16 @@ import "@fontsource/source-sans-pro/400.css"; // normal
import "@fontsource/source-sans-pro/600.css"; // semibold
import "./src/styles/global.css";
import { MODAL_ROOT_ID } from "./src/components/Modal";
import { PortalSettingsProvider } from "./src/contexts/portal-settings";
export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment;
return (
<Layout {...props}>
{element}
<div id={MODAL_ROOT_ID} />
</Layout>
<PortalSettingsProvider>
<Layout {...props}>
{element}
<div id={MODAL_ROOT_ID} />
</Layout>
</PortalSettingsProvider>
);
}

View File

@ -39,7 +39,7 @@
"react-dropzone": "^12.0.4",
"react-helmet": "^6.1.0",
"react-use": "^17.3.2",
"skynet-js": "^3.0.2",
"skynet-js": "4.0.27-beta",
"swr": "^1.2.2",
"tailwindcss": "^3.0.23",
"yup": "^0.32.11"

View File

@ -0,0 +1,7 @@
import { ContainerLoadingIndicator } from "./ContainerLoadingIndicator";
export const FullScreenLoadingIndicator = () => (
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
<ContainerLoadingIndicator className="!text-palette-200/50" />
</div>
);

View File

@ -1 +1,2 @@
export * from "./ContainerLoadingIndicator";
export * from "./FullScreenLoadingIndicator";

View File

@ -2,6 +2,7 @@ import { Link, navigate } from "gatsby";
import styled from "styled-components";
import { screen } from "../../lib/cssHelpers";
import { useUser } from "../../contexts/user";
import { DropdownMenu, DropdownMenuLink } from "../DropdownMenu";
import { CogIcon, LockClosedIcon, SkynetLogoIcon } from "../Icons";
import { PageContainer } from "../PageContainer";
@ -49,48 +50,60 @@ const NavBarBody = styled.nav.attrs({
}
`;
export const NavBar = () => (
<NavBarContainer>
<PageContainer className="px-0">
<NavBarBody>
<NavBarSection className="logo-area pl-2 pr-4 md:px-0 md:w-[110px] justify-center sm:justify-start">
<SkynetLogoIcon size={48} />
</NavBarSection>
<NavBarSection className="navigation-area border-t border-palette-100">
<NavBarLink to="/" as={Link} activeClassName="!border-b-primary">
Dashboard
</NavBarLink>
<NavBarLink to="/files" as={Link} activeClassName="!border-b-primary">
Files
</NavBarLink>
<NavBarLink to="/payments" as={Link} activeClassName="!border-b-primary">
Payments
</NavBarLink>
</NavBarSection>
<NavBarSection className="dropdown-area justify-end">
<DropdownMenu title="My account">
<DropdownMenuLink
to="/settings"
as={Link}
activeClassName="text-primary"
icon={CogIcon}
label="Settings"
partiallyActive
/>
<DropdownMenuLink
onClick={async () => {
await accountsService.post("logout");
navigate("/auth/login");
// TODO: handle errors
}}
activeClassName="text-primary"
className="cursor-pointer"
icon={LockClosedIcon}
label="Log out"
/>
</DropdownMenu>
</NavBarSection>
</NavBarBody>
</PageContainer>
</NavBarContainer>
);
export const NavBar = () => {
const { mutate: setUserState } = useUser();
const onLogout = async () => {
try {
await accountsService.post("logout");
// Don't refresh user state from server, as it will now respond with UNAUTHORIZED
// and user will be redirected to /auth/login with return_to query param (which we want empty).
await setUserState(null, { revalidate: false });
navigate("/auth/login");
} catch {
// Do nothing.
}
};
return (
<NavBarContainer>
<PageContainer className="px-0">
<NavBarBody>
<NavBarSection className="logo-area pl-2 pr-4 md:px-0 md:w-[110px] justify-center sm:justify-start">
<SkynetLogoIcon size={48} />
</NavBarSection>
<NavBarSection className="navigation-area border-t border-palette-100">
<NavBarLink to="/" as={Link} activeClassName="!border-b-primary">
Dashboard
</NavBarLink>
<NavBarLink to="/files" as={Link} activeClassName="!border-b-primary">
Files
</NavBarLink>
<NavBarLink to="/payments" as={Link} activeClassName="!border-b-primary">
Payments
</NavBarLink>
</NavBarSection>
<NavBarSection className="dropdown-area justify-end">
<DropdownMenu title="My account">
<DropdownMenuLink
to="/settings"
as={Link}
activeClassName="text-primary"
icon={CogIcon}
label="Settings"
partiallyActive
/>
<DropdownMenuLink
onClick={onLogout}
activeClassName="text-primary"
className="cursor-pointer"
icon={LockClosedIcon}
label="Log out"
/>
</DropdownMenu>
</NavBarSection>
</NavBarBody>
</PageContainer>
</NavBarContainer>
);
};

View File

@ -40,7 +40,8 @@ const Slider = ({ slides, breakpoints, scrollerClassName, className }) => {
);
React.useEffect(() => {
const maxIndex = slides.length - visibleSlides;
// Prevent negative values for activeIndex.
const maxIndex = Math.max(slides.length - visibleSlides, 0);
// Make sure to not scroll too far when screen size changes.
if (activeIndex > maxIndex) {

View File

@ -5,12 +5,10 @@ import { StatusCodes } from "http-status-codes";
import copy from "copy-text-to-clipboard";
import path from "path-browserify";
import { useTimeoutFn } from "react-use";
import { SkynetClient } from "skynet-js";
import { ProgressBar } from "./ProgressBar";
import UploaderItemIcon from "./UploaderItemIcon";
import buildUploadErrorMessage from "./buildUploadErrorMessage";
const skynetClient = new SkynetClient("https://siasky.net"); //TODO: proper API url
import skynetClient from "../../services/skynetClient";
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;

View File

@ -61,11 +61,6 @@ export const SignUpForm = ({ onSuccess, onFailure }) => (
>
{({ errors, touched }) => (
<Form className="flex flex-col gap-4">
<div className="mt-4 mb-8">
<h3>Create your free account</h3>
<p className="font-light font-sans text-lg">Includes 100 GB storage at basic speed</p>
</div>
<TextField
type="text"
id="email"

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
import freePlan from "../../lib/tiers";
import { usePortalSettings } from "../portal-settings";
import { PlansContext } from "./PlansContext";
@ -12,8 +13,9 @@ import { PlansContext } from "./PlansContext";
*
* @see https://github.com/SkynetLabs/skynet-accounts/blob/7337e740b71b77e6d08016db801e293b8ad81abc/database/user.go#L53-L101
*/
const aggregatePlansAndLimits = (plans, limits) => {
const sortedPlans = [freePlan, ...plans].sort((planA, planB) => planA.tier - planB.tier);
const aggregatePlansAndLimits = (plans, limits, { includeFreePlan }) => {
const allPlans = includeFreePlan ? [freePlan, ...plans] : [...plans];
const sortedPlans = allPlans.sort((planA, planB) => planA.tier - planB.tier);
// Decorate each plan with its corresponding limits data, if available.
if (limits?.length) {
@ -26,10 +28,11 @@ const aggregatePlansAndLimits = (plans, limits) => {
};
export const PlansProvider = ({ children }) => {
const { data: rawPlans, error: plansError } = useSWR("stripe/prices");
const { data: limits, error: limitsError } = useSWR("limits");
const { settings } = usePortalSettings();
const { data: rawPlans, error: plansError } = useSWRImmutable("stripe/prices");
const { data: limits, error: limitsError } = useSWRImmutable("limits");
const [plans, setPlans] = useState([freePlan]);
const [plans, setPlans] = useState(settings.isSubscriptionRequired ? [] : [freePlan]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@ -39,9 +42,11 @@ export const PlansProvider = ({ children }) => {
setError(plansError || limitsError);
} else if (rawPlans) {
setLoading(false);
setPlans(aggregatePlansAndLimits(rawPlans, limits?.userLimits));
setPlans(
aggregatePlansAndLimits(rawPlans, limits?.userLimits, { includeFreePlan: !settings.isSubscriptionRequired })
);
}
}, [rawPlans, limits, plansError, limitsError]);
}, [rawPlans, limits, plansError, limitsError, settings.isSubscriptionRequired]);
return <PlansContext.Provider value={{ plans, error, loading }}>{children}</PlansContext.Provider>;
};

View File

@ -0,0 +1,9 @@
import { createContext } from "react";
export const defaultSettings = {
areAccountsEnabled: false,
isAuthenticationRequired: false,
isSubscriptionRequired: false,
};
export const PortalSettingsContext = createContext(defaultSettings);

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from "react";
import useSWRImmutable from "swr/immutable";
import skynetClient from "../../services/skynetClient";
import { defaultSettings, PortalSettingsContext } from "./PortalSettingsContext";
const fetcher = async (path) => {
try {
const baseUrl = await skynetClient.portalUrl();
return fetch(`${baseUrl}/${path}`).then((response) => response.json());
} catch (error) {
return fetch(path).then((response) => response.json());
}
};
export const PortalSettingsProvider = ({ children }) => {
const { data, error } = useSWRImmutable("/__internal/do/not/use/accounts", fetcher);
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState(defaultSettings);
useEffect(() => {
if (data || error) {
setLoading(false);
}
if (data) {
setSettings({
areAccountsEnabled: data.enabled,
isAuthenticationRequired: data.auth_required,
isSubscriptionRequired: data.subscription_required,
});
}
}, [data, error]);
return (
<PortalSettingsContext.Provider value={{ settings, error, loading }}>{children}</PortalSettingsContext.Provider>
);
};

View File

@ -0,0 +1,2 @@
export * from "./PortalSettingsProvider";
export * from "./usePortalSettings";

View File

@ -0,0 +1,5 @@
import { useContext } from "react";
import { PortalSettingsContext } from "./PortalSettingsContext";
export const usePortalSettings = () => useContext(PortalSettingsContext);

View File

@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
import { UserContext } from "./UserContext";
export const UserProvider = ({ children }) => {
const { data: user, error, mutate } = useSWR("user");
const { data: user, error, mutate } = useSWRImmutable("user");
const [loading, setLoading] = useState(true);
useEffect(() => {

View File

@ -0,0 +1,31 @@
import { navigate } from "gatsby";
import { useEffect, useState } from "react";
import { usePortalSettings } from "../contexts/portal-settings";
import { useUser } from "../contexts/user";
import freeTier from "../lib/tiers";
export default function useUpgradeRedirect() {
const [verifyingSubscription, setVerifyingSubscription] = useState(true);
const { user, loading: userDataLoading } = useUser();
const { settings, loading: portalSettingsLoading } = usePortalSettings();
useEffect(() => {
setVerifyingSubscription(true);
const isDataLoaded = !userDataLoading && !portalSettingsLoading && user && settings;
const hasPaidSubscription = user.tier > freeTier.tier;
if (isDataLoaded) {
if (settings.isSubscriptionRequired && !hasPaidSubscription) {
console.log("redirecting", user, settings);
navigate("/upgrade");
} else {
setVerifyingSubscription(false);
}
}
}, [user, userDataLoading, settings.isSubscriptionRequired, portalSettingsLoading, settings]);
return {
verifyingSubscription,
};
}

View File

@ -8,7 +8,7 @@ import { PageContainer } from "../components/PageContainer";
import { NavBar } from "../components/Navbar";
import { Footer } from "../components/Footer";
import { UserProvider, useUser } from "../contexts/user";
import { ContainerLoadingIndicator } from "../components/LoadingIndicator";
import { FullScreenLoadingIndicator } from "../components/LoadingIndicator";
const Wrapper = styled.div.attrs({
className: "min-h-screen overflow-hidden",
@ -24,11 +24,7 @@ const Layout = ({ children }) => {
// Prevent from flashing the dashboard screen to unauthenticated users.
return (
<Wrapper>
{!user && (
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
<ContainerLoadingIndicator className="!text-palette-200/50" />
</div>
)}
{!user && <FullScreenLoadingIndicator />}
{user && <>{children}</>}
</Wrapper>
);

View File

@ -15,12 +15,15 @@ const redirectUnauthenticated = (key) =>
});
const redirectAuthenticated = (key) =>
fetch(`${baseUrl}/${key}`).then((response) => {
if (response.status === StatusCodes.OK) {
navigate(`/`);
fetch(`${baseUrl}/${key}`).then(async (response) => {
if (response.ok) {
await navigate("/");
return response.json();
}
return response.json();
// If there was an error, let's throw so useSWR's "error" property is populated instead "data".
const data = await response.json();
throw new Error(data?.message || `Error occured when trying to fetch: ${key}`);
});
export const allUsers = {

View File

@ -1,19 +1,31 @@
import { useEffect } from "react";
import { navigate } from "gatsby";
import AuthLayout from "../../layouts/AuthLayout";
import { LoginForm } from "../../components/forms";
import { useUser } from "../../contexts/user";
const LoginPage = ({ location }) => {
const { user, mutate: refreshUserState } = useUser();
const query = new URLSearchParams(location.search);
const redirectTo = query.get("return_to");
useEffect(() => {
if (user) {
navigate(redirectTo || "/");
}
}, [user, redirectTo]);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
<LoginForm onSuccess={() => navigate(redirectTo || "/")} />
<LoginForm
onSuccess={async () => {
await refreshUserState();
}}
/>
</div>
);
};

View File

@ -1,10 +1,41 @@
import { useEffect, useState } from "react";
import { navigate } from "gatsby";
import bytes from "pretty-bytes";
import AuthLayout from "../../layouts/AuthLayout";
import { Alert } from "../../components/Alert";
import HighlightedLink from "../../components/HighlightedLink";
import { SignUpForm } from "../../components/forms/SignUpForm";
import { navigate } from "gatsby";
import { usePortalSettings } from "../../contexts/portal-settings";
import { PlansProvider, usePlans } from "../../contexts/plans";
const FreePortalHeader = () => {
const { plans } = usePlans();
const freePlan = plans.find(({ price }) => price === 0);
const freeStorage = freePlan ? bytes(freePlan.limits?.storageLimit, { binary: true }) : null;
return (
<div className="mt-4 mb-8 font-sans">
<h3>Create your free account</h3>
{freeStorage && <p className="font-light text-lg">Includes {freeStorage} storage at basic speed</p>}
</div>
);
};
const PaidPortalHeader = () => (
<div className="mt-4 mb-8 font-sans">
<h3>Create your account</h3>
<p className="font-light text-sm">
If you're looking for a free portal, try{" "}
<HighlightedLink as="a" href="https://skynetfree.net">
SkynetFree.net
</HighlightedLink>{" "}
with 100GB of free storage.
</p>
</div>
);
const State = {
Pure: "PURE",
@ -14,40 +45,52 @@ const State = {
const SignUpPage = () => {
const [state, setState] = useState(State.Pure);
const { settings } = usePortalSettings();
useEffect(() => {
if (state === State.Success) {
const timer = setTimeout(() => navigate("/"), 3000);
const timer = setTimeout(() => navigate(settings.isSubscriptionRequired ? "/upgrade" : "/"), 3000);
return () => clearTimeout(timer);
}
}, [state]);
}, [state, settings.isSubscriptionRequired]);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && (
<SignUpForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
)}
{state === State.Success && (
<div className="text-center">
<p className="text-primary font-semibold">Please check your inbox and confirm your email address.</p>
<p>You will be redirected to your dashboard shortly.</p>
<HighlightedLink to="/">Click here to go there now.</HighlightedLink>
<PlansProvider>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
{!settings.areAccountsEnabled && <Alert $variant="info">Accounts are not enabled on this portal.</Alert>}
<p className="text-sm text-center mt-8">
Already have an account? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
</div>
{settings.areAccountsEnabled && (
<>
{settings.isSubscriptionRequired ? <PaidPortalHeader /> : <FreePortalHeader />}
{state !== State.Success && (
<SignUpForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
)}
{state === State.Success && (
<div className="text-center">
<p className="text-primary font-semibold">Please check your inbox and confirm your email address.</p>
<p>You will be redirected to your dashboard shortly.</p>
<HighlightedLink to="/">Click here to go there now.</HighlightedLink>
</div>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
</>
)}
<p className="text-sm text-center mt-8">
Already have an account? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
</div>
</PlansProvider>
);
};

View File

@ -12,9 +12,16 @@ import Slider from "../components/Slider/Slider";
import CurrentUsage from "../components/CurrentUsage";
import Uploader from "../components/Uploader/Uploader";
import CurrentPlan from "../components/CurrentPlan";
import { FullScreenLoadingIndicator } from "../components/LoadingIndicator";
import useUpgradeRedirect from "../hooks/useUpgradeRedirect";
const IndexPage = () => {
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
const { verifyingSubscription } = useUpgradeRedirect();
if (verifyingSubscription) {
return <FullScreenLoadingIndicator />;
}
return (
<PlansProvider>

View File

@ -10,13 +10,11 @@ import { Panel } from "../components/Panel";
import Slider from "../components/Slider/Slider";
import { CheckmarkIcon } from "../components/Icons";
import { Button } from "../components/Button";
import { usePortalSettings } from "../contexts/portal-settings";
import { Alert } from "../components/Alert";
import HighlightedLink from "../components/HighlightedLink";
const SLIDER_BREAKPOINTS = [
{
name: "xl",
scrollable: true,
visibleSlides: 4,
},
const PAID_PORTAL_BREAKPOINTS = [
{
name: "lg",
scrollable: true,
@ -33,6 +31,15 @@ const SLIDER_BREAKPOINTS = [
},
];
const FREE_PORTAL_BREAKPOINTS = [
{
name: "xl",
scrollable: true,
visibleSlides: 4,
},
...PAID_PORTAL_BREAKPOINTS,
];
const PlanSummaryItem = ({ children }) => (
<li className="flex items-start gap-1 my-2">
<CheckmarkIcon size={32} className="text-primary shrink-0" />
@ -68,6 +75,7 @@ const localizedNumber = (value) => value.toLocaleString();
const PlansSlider = () => {
const { user, error: userError } = useUser();
const { plans, loading, activePlan, error: plansError } = useActivePlan(user);
const { settings } = usePortalSettings();
if (userError || plansError) {
return (
@ -80,6 +88,18 @@ const PlansSlider = () => {
return (
<div className="w-full mb-24">
{settings.isSubscriptionRequired && !activePlan && (
<Alert $variant="info" className="mb-6">
<p className="font-semibold mt-0">This Skynet portal requires a paid subscription.</p>
<p>
If you're not ready for that yet, you can use your account on{" "}
<HighlightedLink as="a" href="https://skynetfree.net">
SkynetFree.net
</HighlightedLink>{" "}
to store up to 100GB for free.
</p>
</Alert>
)}
{!loading && (
<Slider
slides={plans.map((plan) => {
@ -114,8 +134,7 @@ const PlansSlider = () => {
</Panel>
);
})}
breakpoints={SLIDER_BREAKPOINTS}
scrollerClassName="gap-4 xl:gap-8"
breakpoints={settings.isSubscriptionRequired ? PAID_PORTAL_BREAKPOINTS : FREE_PORTAL_BREAKPOINTS}
className="px-8 sm:px-4 md:px-0 lg:px-0"
/>
)}

View File

@ -0,0 +1,3 @@
import { SkynetClient } from "skynet-js";
export default new SkynetClient("https://skynetpro.net"); // TODO: proper API url

View File

@ -1107,7 +1107,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.17.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
@ -2100,6 +2100,19 @@
escape-string-regexp "^2.0.0"
lodash.deburr "^4.1.0"
"@skynetlabs/tus-js-client@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@skynetlabs/tus-js-client/-/tus-js-client-2.3.0.tgz#a14fd4197e2bc4ce8be724967a0e4c17d937cb64"
integrity sha512-piGvPlJh+Bu3Qf08bDlc/TnFLXE81KnFoPgvnsddNwTSLyyspxPFxJmHO5ki6SYyOl3HmUtGPoix+r2M2UpFEA==
dependencies:
buffer-from "^0.1.1"
combine-errors "^3.0.3"
is-stream "^2.0.0"
js-base64 "^2.6.1"
lodash.throttle "^4.1.1"
proper-lockfile "^2.0.1"
url-parse "^1.4.3"
"@storybook/addon-actions@6.4.19", "@storybook/addon-actions@^6.4.19":
version "6.4.19"
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.4.19.tgz#10631d9c0a6669810264ea7fac3bff7201553084"
@ -4493,6 +4506,13 @@ async-each@^1.0.1:
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
async-mutex@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df"
integrity sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==
dependencies:
tslib "^2.3.1"
async-retry-ng@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/async-retry-ng/-/async-retry-ng-2.0.1.tgz#f5285ec1c52654a2ba6a505d0c18b1eadfaebd41"
@ -4558,13 +4578,20 @@ axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
axios@^0.21.0, axios@^0.21.1:
axios@^0.21.1:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
dependencies:
follow-redirects "^1.14.0"
axios@^0.26.0:
version "0.26.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
dependencies:
follow-redirects "^1.14.8"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@ -4827,6 +4854,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base32-decode@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/base32-decode/-/base32-decode-1.0.0.tgz#2a821d6a664890c872f20aa9aca95a4b4b80e2a7"
integrity sha512-KNWUX/R7wKenwE/G/qFMzGScOgVntOmbE27vvc6GrniDGYb6a5+qWcuoXl8WIOQL7q0TpK7nZDm1Y04Yi3Yn5g==
base32-encode@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/base32-encode/-/base32-encode-1.2.0.tgz#e150573a5e431af0a998e32bdfde7045725ca453"
@ -5109,6 +5141,11 @@ buffer-equal@0.0.1:
resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=
buffer-from@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0"
integrity sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@ -5670,6 +5707,14 @@ colors@1.4.0:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
combine-errors@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/combine-errors/-/combine-errors-3.0.3.tgz#f4df6740083e5703a3181110c2b10551f003da86"
integrity sha1-9N9nQAg+VwOjGBEQwrEFUfAD2oY=
dependencies:
custom-error-instance "2.1.1"
lodash.uniqby "4.5.0"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -6273,6 +6318,11 @@ csstype@^3.0.2, csstype@^3.0.6:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
custom-error-instance@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz#3cf6391487a6629a6247eb0ca0ce00081b7e361a"
integrity sha1-PPY5FIemYppiR+sMoM4ACBt+Nho=
cyclist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@ -7893,6 +7943,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
follow-redirects@^1.14.8:
version "1.14.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -10202,6 +10257,11 @@ jpeg-js@^0.4.0:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
js-base64@^2.6.1:
version "2.6.4"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
@ -10543,6 +10603,43 @@ lodash-es@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash._baseiteratee@~4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz#34a9b5543572727c3db2e78edae3c0e9e66bd102"
integrity sha1-NKm1VDVycnw9sueO2uPA6eZr0QI=
dependencies:
lodash._stringtopath "~4.8.0"
lodash._basetostring@~4.12.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz#9327c9dc5158866b7fa4b9d42f4638e5766dd9df"
integrity sha1-kyfJ3FFYhmt/pLnUL0Y45XZt2d8=
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
integrity sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=
dependencies:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
lodash._root@~3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=
lodash._stringtopath@~4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz#941bcf0e64266e5fc1d66fed0a6959544c576824"
integrity sha1-lBvPDmQmbl/B1m/tCmlZVExXaCQ=
dependencies:
lodash._basetostring "~4.12.0"
lodash.clonedeep@4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@ -10603,6 +10700,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
@ -10613,6 +10715,14 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash.uniqby@4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz#a3a17bbf62eeb6240f491846e97c1c4e2a5e1e21"
integrity sha1-o6F7v2LutiQPSRhG6XwcTipeHiE=
dependencies:
lodash._baseiteratee "~4.7.0"
lodash._baseuniq "~4.6.0"
lodash.without@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
@ -10969,6 +11079,11 @@ mime@^2.4.4, mime@^2.5.2:
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
mime@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -12159,6 +12274,11 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
post-me@^0.4.5:
version "0.4.5"
resolved "https://registry.yarnpkg.com/post-me/-/post-me-0.4.5.tgz#6171b721c7b86230c51cfbe48ddea047ef8831ce"
integrity sha512-XgPdktF/2M5jglgVDULr9NUb/QNv3bY3g6RG22iTb5MIMtB07/5FJB5fbVmu5Eaopowc6uZx7K3e7x1shPwnXw==
postcss-calc@^8.2.0:
version "8.2.4"
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5"
@ -12676,6 +12796,14 @@ prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.7.2,
object-assign "^4.1.1"
react-is "^16.13.1"
proper-lockfile@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-2.0.1.tgz#159fb06193d32003f4b3691dd2ec1a634aa80d1d"
integrity sha1-FZ+wYZPTIAP0s2kd0uwaY0qoDR0=
dependencies:
graceful-fs "^4.1.2"
retry "^0.10.0"
proper-lockfile@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f"
@ -13558,6 +13686,11 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
retry@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
retry@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
@ -13957,24 +14090,35 @@ sjcl@^1.0.8:
resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a"
integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==
skynet-js@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-3.0.2.tgz#d08a33066ee85b86e4ffc7c31591239a88da6fbe"
integrity sha512-rbmpOGbDwg2FcsZ7HkmGhVaUwWO6kaysRFKTBC3yGiV+b6fbnpPPNCskvh8kWwbTsj+koWkSRUFYqG7cc+eTuA==
skynet-js@^4.0.27-beta:
version "4.0.27-beta"
resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-4.0.27-beta.tgz#4257bffda8757830656e0beb89d0d2e44da17e2f"
integrity sha512-JV+QE/2l2YwVN1jQHVMFXgggwtBrPAnuyXySbLgafEJAde5dUwSEr5YRMV+3LvEgYkGhxSb74pyq0u0wrF2sUg==
dependencies:
"@babel/runtime" "^7.11.2"
axios "^0.21.0"
"@skynetlabs/tus-js-client" "^2.3.0"
async-mutex "^0.3.2"
axios "^0.26.0"
base32-decode "^1.0.0"
base32-encode "^1.1.1"
base64-js "^1.3.1"
blakejs "^1.1.0"
buffer "^6.0.1"
mime "^2.5.2"
mime "^3.0.0"
path-browserify "^1.0.1"
post-me "^0.4.5"
randombytes "^2.1.0"
sjcl "^1.0.8"
skynet-mysky-utils "^0.3.0"
tweetnacl "^1.0.3"
url-join "^4.0.1"
url-parse "^1.4.7"
url-parse "^1.5.1"
skynet-mysky-utils@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/skynet-mysky-utils/-/skynet-mysky-utils-0.3.0.tgz#87fdc0a5f8547cf660280ef86b7a762269919bad"
integrity sha512-X9L6SrVTdwTUFook/E6zUWCOpXHdyspLAu0elQbbPkZCWeFpr/XXTMbiyPV3m1liYsesngAKxzaSqylaTWOGUA==
dependencies:
post-me "^0.4.5"
slash@^2.0.0:
version "2.0.0"
@ -15061,7 +15205,7 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@~2.3.0:
tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@~2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
@ -15419,7 +15563,7 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"
url-parse@^1.4.7:
url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==