diff --git a/.github/workflows/test-files-on-demand.yml b/.github/workflows/test-files-on-demand.yml index 62d756b8b8..296b371f3e 100644 --- a/.github/workflows/test-files-on-demand.yml +++ b/.github/workflows/test-files-on-demand.yml @@ -15,7 +15,7 @@ jobs: **/node_modules ~/.cache/Cypress **/build - key: ${{ runner.os }}-node_modules-files-build-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-node_modules-files-build-${{ hashFiles('./yarn.lock') }} restore-keys: | ${{ runner.os }}-node_modules-build- diff --git a/.github/workflows/test-files.yml b/.github/workflows/test-files.yml index 035d5e3947..e292cd49ca 100644 --- a/.github/workflows/test-files.yml +++ b/.github/workflows/test-files.yml @@ -8,6 +8,7 @@ on: - package.json - .eslintrc.json - tsconfig.json + - yarn.lock jobs: cypress-run: runs-on: ubuntu-latest @@ -23,7 +24,7 @@ jobs: **/node_modules ~/.cache/Cypress **/build - key: ${{ runner.os }}-node_modules-files-build-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-node_modules-files-build-${{ hashFiles('./yarn.lock') }} restore-keys: | ${{ runner.os }}-node_modules-build- diff --git a/.github/workflows/test-storage.yml b/.github/workflows/test-storage.yml index f03c0dc27e..a8734d3111 100644 --- a/.github/workflows/test-storage.yml +++ b/.github/workflows/test-storage.yml @@ -8,6 +8,7 @@ on: - package.json - .eslintrc.json - tsconfig.json + - yarn.lock jobs: cypress-run: runs-on: ubuntu-latest @@ -23,7 +24,7 @@ jobs: **/node_modules ~/.cache/Cypress **/build - key: ${{ runner.os }}-node_modules-build-storage-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-node_modules-build-storage-${{ hashFiles('./yarn.lock') }} restore-keys: | ${{ runner.os }}-node_modules-build- diff --git a/packages/common-components/src/Button/Button.tsx b/packages/common-components/src/Button/Button.tsx index 01b7a8c37a..0d5e02a831 100644 --- a/packages/common-components/src/Button/Button.tsx +++ b/packages/common-components/src/Button/Button.tsx @@ -2,7 +2,6 @@ import React, { ReactNode } from "react" import clsx from "clsx" import { ITheme, makeStyles, createStyles } from "@chainsafe/common-theme" import { Loading } from "../Spinner" -import { Typography } from "../Typography" const useStyles = makeStyles( ({ constants, typography, animation, palette, overrides }: ITheme) => @@ -20,7 +19,6 @@ const useStyles = makeStyles( border: "none", outline: "none", "& svg": { - transitionDuration: `${animation.transform}ms`, margin: `${0}px ${constants.generalUnit / 2}px 0` }, "&.large": { @@ -47,13 +45,13 @@ const useStyles = makeStyles( justifyContent: "center", textAlign: "center", alignItems: "center", + color: "inherit", textDecoration: "underline", cursor: "pointer", - transitionDuration: `${animation.transform}ms`, border: "none", + background: "none", outline: "none", "& svg": { - transitionDuration: `${animation.transform}ms`, margin: `${0}px ${constants.generalUnit / 2}px 0` } }, @@ -93,7 +91,7 @@ const useStyles = makeStyles( fill: palette.common.white.main }, "&:hover": { - backgroundColor: palette.primary.hover, + backgroundColor: palette.primary.main, color: palette.common.white.main, ...overrides?.Button?.variants?.secondary?.hover }, @@ -113,12 +111,15 @@ const useStyles = makeStyles( backgroundColor: "transparent", color: palette.additional["gray"][9], "&:hover": { + color: palette.primary.main, ...overrides?.Button?.variants?.text?.hover }, "&:focus": { + color: palette.primary.main, ...overrides?.Button?.variants?.text?.focus }, "&:active": { + color: palette.primary.main, ...overrides?.Button?.variants?.text?.active }, ...overrides?.Button?.variants?.text?.root @@ -331,19 +332,7 @@ const Button: React.FC = ({ }: IButtonProps) => { const classes = useStyles() - return variant === "link" ? ( - {loading && loadingText ? loadingText : children} - ) : ( + return ( - - - ), []) - return ( { themes={{ light: lightTheme, dark: darkTheme }} > window.location.reload()} > diff --git a/packages/files-ui/src/Components/Elements/TeamModal.tsx b/packages/files-ui/src/Components/Elements/BetaModal.tsx similarity index 76% rename from packages/files-ui/src/Components/Elements/TeamModal.tsx rename to packages/files-ui/src/Components/Elements/BetaModal.tsx index 592c413fe9..1c1a2a4daf 100644 --- a/packages/files-ui/src/Components/Elements/TeamModal.tsx +++ b/packages/files-ui/src/Components/Elements/BetaModal.tsx @@ -35,11 +35,11 @@ interface Props { onHide: () => void } -const TeamModal = ({ onHide }: Props) => { +const BetaModal = ({ onHide }: Props) => { const classes = useStyles() - const onSignupTeamClick = useCallback(() => { - window.open(ROUTE_LINKS.TeamSignup, "_blank") + const onFormdButtonClick = useCallback(() => { + window.open(ROUTE_LINKS.SubscriptionWhitelistForm, "_blank") onHide() }, [onHide]) @@ -57,22 +57,24 @@ const TeamModal = ({ onHide }: Props) => { variant="h2" className={classes.title} > - Teams + Need more storage? - A better sharing experience is coming soon. + + Join our new limited-access subscription plans to upgrade to a plan with more storage. +
)} -export default TeamModal +export default BetaModal diff --git a/packages/files-ui/src/Components/Elements/InvoiceLines.tsx b/packages/files-ui/src/Components/Elements/InvoiceLines.tsx index 6749e9063e..3b939312a4 100644 --- a/packages/files-ui/src/Components/Elements/InvoiceLines.tsx +++ b/packages/files-ui/src/Components/Elements/InvoiceLines.tsx @@ -5,12 +5,15 @@ import { Typography, Loading, Button } from "@chainsafe/common-components" import { Trans } from "@lingui/macro" import dayjs from "dayjs" import { useBilling } from "../../Contexts/BillingContext" +import clsx from "clsx" const useStyles = makeStyles( ({ constants, breakpoints, palette, typography }: CSFTheme) => createStyles({ heading: { + fontSize: 16, marginBottom: constants.generalUnit * 4, + marginTop: constants.generalUnit * 2, [breakpoints.down("md")]: { marginBottom: constants.generalUnit * 2 } @@ -26,37 +29,51 @@ const useStyles = makeStyles( padding: `0 ${constants.generalUnit}px` } }, - setOption: { + invoiceLine: { width: "100%", backgroundColor: palette.additional["gray"][4], color: palette.additional["gray"][9], padding: constants.generalUnit * 1.5, - borderRadius: 16, + borderRadius: 10, marginTop: constants.generalUnit * 1.5, "& > div": { display: "flex", alignItems: "center", - "& > span": { - display: "block", + justifyContent: "space-between", + "& > *": { + width: "100%", lineHeight: "16px", fontWeight: typography.fontWeight.regular, - "&.receiptDate": { - marginLeft: constants.generalUnit, - marginRight: constants.generalUnit, - flex: "1 1 0" - } + marginLeft: constants.generalUnit, + marginRight: constants.generalUnit + }, + "& > button": { + maxWidth: 100 } } }, + unpaidInvoice: { + border: `1px solid ${palette.additional["volcano"][7]}`, + color: palette.additional["volcano"][7] + }, price: { - fontWeight: "bold !important" as "bold" + fontWeight: "bold !important" as "bold", + fontSize: 16 + }, + link: { + color: constants.settingsPage.linkButton.color, + paddingRight: "0px !important", + fontSize: 16 + }, + text: { + fontSize: 16 } }) ) interface IInvoiceProps { lineNumber?: number - payInvoice?: () => void + payInvoice: (invoiceId: string) => void } const InvoiceLines = ({ lineNumber, payInvoice }: IInvoiceProps) => { @@ -82,10 +99,10 @@ const InvoiceLines = ({ lineNumber, payInvoice }: IInvoiceProps) => { )} {invoicesToShow && !invoicesToShow.length && ( -
+
No invoice found @@ -93,32 +110,48 @@ const InvoiceLines = ({ lineNumber, payInvoice }: IInvoiceProps) => {
)} {!!invoicesToShow?.length && ( - invoicesToShow.map(({ amount, currency, uuid, period_start, status }) => + invoicesToShow.map(({ amount, currency, uuid, period_start, status, product }) =>
- {amount} {currency} + {product.name} {product.price.recurring.interval_count} {product.price.recurring.interval} {dayjs.unix(period_start).format("MMM D, YYYY")} + + {amount.toFixed(2)} {currency.toUpperCase()} + {(status === "paid") && ( - )} {(status === "open") && ( - )}
diff --git a/packages/files-ui/src/Components/Elements/Notifications/NotificationLine.tsx b/packages/files-ui/src/Components/Elements/Notifications/NotificationLine.tsx new file mode 100644 index 0000000000..f403808def --- /dev/null +++ b/packages/files-ui/src/Components/Elements/Notifications/NotificationLine.tsx @@ -0,0 +1,60 @@ +import React from "react" +import { Typography } from "@chainsafe/common-components" +import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" +import dayjs from "dayjs" +import relativeTime from "dayjs/plugin/relativeTime" +import { Notification } from "./NotificationsDropdown" +dayjs.extend(relativeTime) + +const useStyles = makeStyles(({ palette, constants }: ITheme) => + createStyles({ + notificationBody: { + padding: `${constants.generalUnit}px ${constants.generalUnit * 1.5}px`, + display: "flex", + alignItems: "center", + cursor: "pointer", + "&:hover": { + backgroundColor: palette.additional["gray"][4] + } + }, + notificationTitle: { + color: palette.additional["gray"][9], + paddingRight: constants.generalUnit * 1.5, + width: 180 + }, + notificationTime: { + color: palette.additional["blue"][6], + width: "100%", + textAlign: "right" + } + }) +) + +interface Props { + notification: Notification +} +const NotificationLine = ({ notification }: Props) => { + const classes = useStyles() + + return
+ + {notification.title} + + + {dayjs.unix(notification.createdAt).fromNow()} + +
+} + +export default NotificationLine diff --git a/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx b/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx index e7c78307f1..3d80a7e006 100644 --- a/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx +++ b/packages/files-ui/src/Components/Elements/Notifications/NotificationList.tsx @@ -1,53 +1,50 @@ import React from "react" -import { Typography, ScrollbarWrapper } from "@chainsafe/common-components" +import { Typography, ScrollbarWrapper, Divider } from "@chainsafe/common-components" import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" import { Notification } from "./NotificationsDropdown" import { Trans } from "@lingui/macro" import { MoonStarIcon } from "@chainsafe/common-components" -import clsx from "clsx" -dayjs.extend(relativeTime) +import NotificationLine from "./NotificationLine" const useStyles = makeStyles(({ palette, constants }: ITheme) => createStyles({ - notificationBody: { - padding: `${constants.generalUnit}px ${constants.generalUnit * 1.5}px`, + notificationsBody: { + backgroundColor: palette.additional["gray"][2] + }, + emptyBody: { + cursor: "pointer", display: "flex", alignItems: "center", - cursor: "pointer", - backgroundColor: palette.additional["gray"][2], - "&.empty": { - display: "flex", - flexDirection: "column", - padding: `${constants.generalUnit * 3 }px ${constants.generalUnit * 1.5}px`, - color: palette.additional["gray"][7] - }, - "&:hover": { - backgroundColor: palette.additional["gray"][3] - }, + flexDirection: "column", + padding: `${constants.generalUnit * 3}px ${constants.generalUnit * 1.5}px`, + color: palette.additional["gray"][7], "& svg>path": { stroke: palette.additional["gray"][7] - }, - borderBottom: `1px solid ${palette.additional["gray"][5]}`, - "&:last-child": { - border: "none" } }, icon: { transition: "none", marginBottom: 2 * constants.generalUnit }, + scrollContent: { + minWidth: 350 + }, + header: { + padding: `${constants.generalUnit * 3}px ${constants.generalUnit * 3}px`, + paddingBottom: 0 + }, notificationTitle: { color: palette.additional["gray"][9], paddingRight: constants.generalUnit * 1.5, width: 180 }, - scrollContent: { - minWidth: 300 + notifs: { + padding: `${constants.generalUnit * 3}px ${constants.generalUnit * 3}px`, + paddingTop: 0 }, - notificationTime: { - color: palette.additional["blue"][6] + timeHeader: { + paddingLeft: constants.generalUnit * 1.5 } }) ) @@ -58,6 +55,8 @@ interface INotificationListProps { const NotificationList = ({ notifications }: INotificationListProps) => { const classes = useStyles() + const thisWeeksNotifications = notifications.filter(n => dayjs(Date.now()).diff(dayjs.unix(n.createdAt), "day") <= 7) + const olderNotifications = notifications.filter(n => dayjs(Date.now()).diff(dayjs.unix(n.createdAt), "day") > 7) return ( { maxHeight={300} className={classes.scrollContent} > -
+
{notifications.length === 0 && ( -
+
There are no notifications!
)} - {notifications.map((n, i) => ( -
+ {notifications.length !== 0 && <> +
- {n.title} + Notifications + +
+ {!!thisWeeksNotifications.length &&
- {dayjs.unix(n.createdAt).fromNow()} + This week -
- ))} + {thisWeeksNotifications.map((n, i) => )} +
+ } + {!!olderNotifications.length &&
+ + Older notifications + + {olderNotifications.map((n, i) => )} +
+ } + }
- )} + ) +} export default NotificationList \ No newline at end of file diff --git a/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx b/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx index dbe37f6d69..f9f3ba2bde 100644 --- a/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx +++ b/packages/files-ui/src/Components/Elements/Notifications/NotificationsDropdown.tsx @@ -1,20 +1,61 @@ -import React from "react" -import { Button, BellIcon, MenuDropdown } from "@chainsafe/common-components" -import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" +import React, { useState } from "react" +import { BellIcon, MenuDropdown } from "@chainsafe/common-components" +import { createStyles, makeStyles } from "@chainsafe/common-theme" import NotificationList from "./NotificationList" import { useNotifications } from "../../../Contexts/NotificationsContext" +import { CSFTheme } from "../../../Themes/types" +import clsx from "clsx" -const useStyles = makeStyles(({ palette, constants }: ITheme) => +const useStyles = makeStyles(({ animation, palette, constants, breakpoints, overrides }: CSFTheme) => createStyles({ notificationsButton: { - position: "relative" + position: "relative", + transition: "none", + height: constants.generalUnit * 4, + padding: `0 ${constants.generalUnit}px !important`, + backgroundColor: palette.additional["gray"][3], + color: palette.additional["gray"][10], + borderRadius: `${constants.generalUnit / 4}px`, + display: "flex", + justifyContent: "center", + textAlign: "center", + alignItems: "center", + textDecoration: "none", + cursor: "pointer", + transitionDuration: `${animation.transform}ms`, + border: "none", + outline: "none", + ...overrides?.Button?.variants?.tertiary?.root, + "& svg": { + transitionDuration: `${animation.transform}ms`, + margin: `${0}px ${constants.generalUnit / 2}px 0`, + fill: palette.common.white.main + }, + "&:hover": { + backgroundColor: palette.primary.main, + color: palette.common.white.main, + ...overrides?.Button?.variants?.tertiary?.hover + }, + "&:focus": { + backgroundColor: palette.primary.main, + color: palette.common.white.main, + ...overrides?.Button?.variants?.tertiary?.focus + }, + "&:active, &.active": { + backgroundColor: palette.primary.main, + color: palette.common.white.main, + ...overrides?.Button?.variants?.tertiary?.active + }, + [breakpoints.down("md")]: { + backgroundColor: palette.additional["gray"][3] + } }, badge: { position: "absolute", background: palette.additional["volcano"][6], color: palette.additional["gray"][1], - top: "-2px", - left: "13px", + top: "0px", + right: "5px", borderRadius: constants.generalUnit, padding: `${constants.generalUnit * 0.25}px ${constants.generalUnit * 0.5}px`, fontSize: "10px", @@ -22,11 +63,11 @@ const useStyles = makeStyles(({ palette, constants }: ITheme) => height: "0.92rem", minWidth: "1rem" }, - icon: { - transition: "none" - }, - button: { - height: constants.generalUnit * 4 + optionsOpen: { + [breakpoints.down("md")]: { + minWidth: "100vw", + backgroundColor: palette.additional["gray"][3] + } } }) ) @@ -41,27 +82,28 @@ export interface Notification { const NotificationsDropdown = () => { const classes = useStyles() const { notifications } = useNotifications() + const [isActive, setIsActive] = useState(false) return ( } + dropdown={} hideIndicator={true} anchor="bottom-right" autoclose + classNames={{ options: classes.optionsOpen }} + onClose={() => setIsActive(false)} > - + } + ) } diff --git a/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx b/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx index 321d3d6d81..ef9ec57aa4 100644 --- a/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx +++ b/packages/files-ui/src/Components/Elements/ShareTransferRequestModal.tsx @@ -1,5 +1,5 @@ import { Button, Loading, Typography } from "@chainsafe/common-components" -import { createStyles, makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" +import { createStyles, makeStyles } from "@chainsafe/common-theme" import { Trans } from "@lingui/macro" import dayjs from "dayjs" import React, { useCallback, useEffect, useMemo, useState } from "react" @@ -49,7 +49,6 @@ const useStyles = makeStyles(({ constants }: CSFTheme) => const ShareTransferRequestModal = ({ requests }: Props) => { const { approveShareTransferRequest, rejectShareTransferRequest, clearShareTransferRequests } = useThresholdKey() - const { desktop } = useThemeSwitcher() const classes = useStyles() const [isLoadingApprove, setIsLoadingApprove] = useState(false) const [isLoadingReject, setIsLoadingReject] = useState(false) @@ -103,7 +102,7 @@ const ShareTransferRequestModal = ({ requests }: Props) => {
- ) : ( - <> - {!searchActive && ( - <> - setNavOpen(!navOpen)} - variant={navOpen ? "active" : "default"} - className={classes.hamburgerMenu} - testId="icon-hamburger-menu" - /> - - - + <> + setNavOpen(!navOpen)} + variant={navOpen ? "active" : "default"} + className={classes.hamburgerMenu} + testId="icon-hamburger-menu" + /> + {!searchActive && ( + <> + - Files - -   - beta - - setSearchActive(true)} - /> - - )} + + + Files + +   + beta + +
+ setSearchActive(true)} + /> + +
+ + )} + {searchActive && ( { /> )} - )} + } )} - {isTeamModalOpen && setIsTeamModalOpen(false)}/>} + {isBetaModalOpen && setIsBetaModalOpen(false)}/>} ) } diff --git a/packages/files-ui/src/Components/Layouts/AppNav.tsx b/packages/files-ui/src/Components/Layouts/AppNav.tsx index 09bb56c062..a2a934e929 100644 --- a/packages/files-ui/src/Components/Layouts/AppNav.tsx +++ b/packages/files-ui/src/Components/Layouts/AppNav.tsx @@ -204,6 +204,7 @@ const useStyles = makeStyles( marginBottom: constants.generalUnit }, profileButton: { + maxWidth: 144, borderRadius: 4, display: "flex", alignItems: "center", @@ -229,6 +230,11 @@ const useStyles = makeStyles( }, menuTitle: { padding: `${constants.generalUnit * 1.5}px 0` + }, + profileLabel: { + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis" } }) } @@ -318,6 +324,7 @@ const AppNav = ({ navOpen, setNavOpen }: IAppNav) => { {profileTitle} diff --git a/packages/files-ui/src/Components/Modules/ErrorModal.tsx b/packages/files-ui/src/Components/Modules/ErrorModal.tsx new file mode 100644 index 0000000000..63401716a6 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/ErrorModal.tsx @@ -0,0 +1,107 @@ +/* eslint-disable max-len */ +import React from "react" +import { Button, Modal, Typography } from "@chainsafe/common-components" +import { ROUTE_LINKS } from "../FilesRoutes" + +interface Props { + error: Error + componentStack: string | null + eventId: string | null + resetError: () => void +} + +const ErrorModal = ({ error, componentStack, resetError }: Props) => { + + const generalStyle = { + margin: "1rem", + backgrounColor: "var(--gray1)", + color: "var(--gray9)" + } + + return + + An unexpected error occured + + + If you would like to provide additional info to help us debug and resolve the issue, reach out on + Discord + + +
+ + Error: + + +
{error?.message.toString()}
+
+ + Stack: + + + {componentStack} + +
+ +
+
+} + +export default ErrorModal \ No newline at end of file diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateFolderModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateFolderModal.tsx index c754ae0000..2af1e773f7 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateFolderModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateFolderModal.tsx @@ -162,7 +162,7 @@ const CreateFolderModal = ({ modalOpen, close }: ICreateFolderModalProps) => { data-cy="button-cancel-create-folder" onClick={onCancel} size="medium" - variant={desktop ? "outline" : "gray"} + variant="outline" type="button" > Cancel diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolder.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolder.tsx deleted file mode 100644 index 99965c7246..0000000000 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolder.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { Button, ShareAltSvg, TagsInput, Typography, Grid, TextInput } from "@chainsafe/common-components" -import { createStyles, makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" -import React, { useState, useCallback } from "react" -import { CSFTheme } from "../../../Themes/types" -import { BucketKeyPermission } from "../../../Contexts/FilesContext" -import CustomButton from "../../Elements/CustomButton" -import { t, Trans } from "@lingui/macro" -import { useEffect } from "react" -import { SharedFolderModalMode } from "./types" -import { useCreateOrEditSharedFolder } from "./hooks/useCreateOrEditSharedFolder" -import { useLookupSharedFolderUser } from "./hooks/useLookupUser" -import { nameValidator } from "../../../Utils/validationSchema" -import { getUserDisplayName } from "../../../Utils/getUserDisplayName" - -const useStyles = makeStyles( - ({ breakpoints, constants, typography, palette }: CSFTheme) => { - return createStyles({ - root: { - padding: constants.generalUnit * 2, - flexDirection: "column", - display: "flex", - alignItems: "center", - [breakpoints.down("sm")]: { - padding: constants.generalUnit - } - }, - okButton: { - marginLeft: constants.generalUnit - }, - label: { - fontSize: 14, - lineHeight: "22px" - }, - heading: { - color: constants.modalDefault.color, - fontWeight: typography.fontWeight.semibold, - marginBottom: 10 - }, - iconBacking: { - backgroundColor: constants.modalDefault.iconBackingColor, - width: 48, - height: 48, - borderRadius: 24, - marginBottom: constants.generalUnit * 2, - marginTop: constants.generalUnit, - "& > svg": { - width: 16, - height: 16, - fill: palette.primary.main, - position: "relative", - display: "block", - transform: "translate(-50%, -50%)", - top: "50%", - left: "50%" - } - }, - modalFlexItem: { - width: "100%", - margin: 5 - }, - inputLabel: { - fontSize: 16, - fontWeight: 600 - }, - shareFolderNameInput: { - margin: `0 ${constants.generalUnit * 1.5}px ${constants.generalUnit}px`, - display: "block" - }, - footer: { - width: "100%", - padding: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px ${constants.generalUnit}px 0` - }, - errorText: { - marginLeft: constants.generalUnit * 1.5, - color: palette.error.main - } - }) - } -) - -interface ICreateOrManageSharedFolderProps { - mode?: SharedFolderModalMode - onClose: () => void - bucketToEdit?: BucketKeyPermission -} - -const CreateOrManageSharedFolder = ({ mode, onClose, bucketToEdit }: ICreateOrManageSharedFolderProps) => { - const classes = useStyles() - const { desktop } = useThemeSwitcher() - const { handleCreateSharedFolder, handleEditSharedFolder, isEditingSharedFolder, isCreatingSharedFolder } = useCreateOrEditSharedFolder() - const [sharedFolderName, setSharedFolderName] = useState("") - const { sharedFolderReaders, sharedFolderWriters, onNewUsers, handleLookupUser, usersError, resetUsers } = useLookupSharedFolderUser() - const [hasPermissionsChanged, setHasPermissionsChanged] = useState(false) - const [nameError, setNameError] = useState("") - - const onReset = useCallback(() => { - setSharedFolderName("") - setHasPermissionsChanged(false) - resetUsers() - }, [resetUsers]) - - useEffect(() => { - onReset() - - if (!bucketToEdit) return - - const newWriters = bucketToEdit.writers.map((writer) => ({ - label: getUserDisplayName(writer), - value: writer.uuid || "", - data: writer - }) - ) || [] - - const newReaders = bucketToEdit.readers.map((reader) => ({ - label: getUserDisplayName(reader), - value: reader.uuid || "", - data: reader - }) - ) || [] - - onNewUsers(newWriters, "write") - onNewUsers(newReaders, "read") - }, [bucketToEdit, onNewUsers, onReset]) - - const onNameChange = useCallback((value?: string | number) => { - if (value === undefined) return - - const name = value.toString() - setSharedFolderName(name) - - nameValidator - .validate({ name }) - .then(() => { - setNameError("") - }) - .catch((e: Error) => { - setNameError(e.message) - }) - }, []) - - const handleClose = useCallback(() => { - onReset() - onClose() - }, [onClose, onReset]) - - const onCreateSharedFolder = useCallback(() => { - handleCreateSharedFolder(sharedFolderName, sharedFolderReaders, sharedFolderWriters) - .catch(console.error) - .finally(handleClose) - }, [handleCreateSharedFolder, sharedFolderName, sharedFolderWriters, sharedFolderReaders, handleClose]) - - const onEditSharedFolder = useCallback(() => { - if (!bucketToEdit) return - handleEditSharedFolder(bucketToEdit, sharedFolderReaders, sharedFolderWriters) - .catch(console.error) - .finally(handleClose) - }, [handleEditSharedFolder, sharedFolderWriters, sharedFolderReaders, handleClose, bucketToEdit]) - - return ( -
-
- -
-
- - {mode === "create" - ? Create Shared Folder - : Manage Shared Folder - } - - -
- {mode === "create" && -
- - {nameError && ( - - {nameError} - - )} -
- } -
- { - setHasPermissionsChanged(true) - onNewUsers(values, "read") - }} - label={t`Give view-only permission to:`} - labelClassName={classes.inputLabel} - value={sharedFolderReaders} - fetchTags={(inputVal) => handleLookupUser(inputVal, "read")} - placeholder={t`Add by sharing address, username, wallet address or ENS`} - styles={{ - control: (provided) => ({ - ...provided, - minHeight: 90, - alignContent: "start" - }) - }} - loadingMessage={t`Loading`} - noOptionsMessage={t`No user found for this query.`} - data-cy="tag-view-permission-user" - /> -
-
- { - setHasPermissionsChanged(true) - onNewUsers(values, "write") - }} - label={t`Give edit permission to:`} - labelClassName={classes.inputLabel} - value={sharedFolderWriters} - fetchTags={(inputVal) => handleLookupUser(inputVal, "write")} - placeholder={t`Add by sharing address, username, wallet address or ENS`} - styles={{ - control: (provided) => ({ - ...provided, - minHeight: 90, - alignContent: "start" - }) - }} - loadingMessage={t`Loading`} - noOptionsMessage={t`No user found for this query.`} - data-cy="tag-edit-permission-user" - /> -
- - {!!usersError && ( - - {usersError} - - )} - - - Cancel - - - - -
- ) -} - -export default CreateOrManageSharedFolder diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolderModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolderModal.tsx deleted file mode 100644 index 3ab36a5845..0000000000 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateOrManageSharedFolderModal.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { createStyles, makeStyles } from "@chainsafe/common-theme" -import React from "react" -import CustomModal from "../../Elements/CustomModal" -import { CSFTheme } from "../../../Themes/types" -import { BucketKeyPermission } from "../../../Contexts/FilesContext" -import { SharedFolderModalMode } from "./types" -import LinkList from "./LinkSharing/LinkList" -import CreateOrManageSharedFolder from "./CreateOrManageSharedFolder" - -const useStyles = makeStyles( - ({ breakpoints, constants, zIndex }: CSFTheme) => { - return createStyles({ - modalRoot: { - zIndex: zIndex?.blocker, - [breakpoints.down("md")]: { - paddingBottom: Number(constants?.mobileButtonHeight) - } - }, - modalInner: { - backgroundColor: constants.modalDefault.backgroundColor, - color: constants.modalDefault.color, - [breakpoints.down("md")]: { - maxWidth: `${breakpoints.width("md")}px !important` - } - }, - subModal: { - width: "100%" - } - }) - } -) - -interface ICreateOrManageSharedFolderModalProps { - mode?: SharedFolderModalMode - isModalOpen: boolean - onClose: () => void - bucketToEdit?: BucketKeyPermission -} - -const CreateOrManageSharedFolderModal = ( - { mode, isModalOpen, onClose, bucketToEdit }: ICreateOrManageSharedFolderModalProps -) => { - const classes = useStyles() - - return ( - - )} - mobileStickyBottom={false} - > - - - ) -} - -export default CreateOrManageSharedFolderModal diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/CreateSharedFolderModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateSharedFolderModal.tsx new file mode 100644 index 0000000000..efd58f3708 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/CreateSharedFolderModal.tsx @@ -0,0 +1,229 @@ +import { createStyles, makeStyles } from "@chainsafe/common-theme" +import React, { useState } from "react" +import CustomModal from "../../Elements/CustomModal" +import { t, Trans } from "@lingui/macro" +import { Button, ShareAltSvg, TextInput, Typography } from "@chainsafe/common-components" +import { CSFTheme } from "../../../Themes/types" +import { useCallback } from "react" +import { useCreateOrEditSharedFolder } from "./hooks/useCreateOrEditSharedFolder" +import { BucketKeyPermission } from "../../../Contexts/FilesContext" +import clsx from "clsx" +import { useEffect } from "react" +import { nameValidator } from "../../../Utils/validationSchema" +import ManageSharedFolder from "./ManageSharedFolder" + +const useStyles = makeStyles( + ({ constants, palette, typography, zIndex }: CSFTheme) => { + return createStyles({ + root: { + padding: constants.generalUnit * 3, + flexDirection: "column", + display: "flex" + }, + modalRoot: { + zIndex: zIndex?.blocker + }, + modalInner: { + backgroundColor: constants.fileInfoModal.background, + color: constants.fileInfoModal.color + }, + topIconContainer: { + display: "flex", + flexDirection: "column", + alignItems: "center" + }, + buttonsArea: { + display: "flex", + justifyContent: "flex-end", + flexDirection: "column" + }, + buttonsContainer: { + display: "flex", + justifyContent: "flex-end", + alignItems: "center", + marginTop: constants.generalUnit * 2 + }, + mainButton: { + marginLeft: constants.generalUnit + }, + cancelButton: { + maxWidth: 100 + }, + heading: { + color: constants.modalDefault.color, + fontWeight: typography.fontWeight.semibold, + marginBottom: constants.generalUnit * 3 + }, + iconBacking: { + backgroundColor: constants.modalDefault.iconBackingColor, + width: 48, + height: 48, + borderRadius: 24, + marginBottom: 8, + "& > svg": { + width: 16, + height: 16, + fill: palette.primary.main, + position: "relative", + display: "block", + transform: "translate(-50%, -50%)", + top: "50%", + left: "50%" + } + }, + inputLabel: { + fontSize: 14, + fontWeight: 600, + marginBottom: constants.generalUnit + }, + modalFlexItem: { + width: "100%", + marginBottom: constants.generalUnit * 2 + }, + newFolderInput: { + margin: 0, + width: "100%" + }, + inputWrapper: { + marginBottom: 0 + }, + errorText: { + marginTop: constants.generalUnit * 1, + color: palette.error.main + } + }) + } +) + +interface ICreateSharedFolderModalProps { + onClose: () => void +} + +const CreateSharedFolderModal = ({ onClose }: ICreateSharedFolderModalProps) => { + const { handleCreateSharedFolder } = useCreateOrEditSharedFolder() + const [sharedFolderName, setSharedFolderName] = useState("") + const [bucketToManage, setBucketToManage] = useState() + const [isFolderCreationLoading, setIsFolderCreationLoading] = useState(false) + const [nameError, setNameError] = useState("") + + const classes = useStyles() + + useEffect(() => { + setSharedFolderName("") + setNameError("") + }, []) + + const onNameChange = useCallback((value?: string | number) => { + if (value === undefined) return + + const name = value.toString() + setSharedFolderName(name) + + nameValidator + .validate({ name }) + .then(() => { + setNameError("") + }) + .catch((e: Error) => { + setNameError(e.message) + }) + }, []) + + const handleCreate = useCallback(() => { + setIsFolderCreationLoading(true) + handleCreateSharedFolder(sharedFolderName, [], []) + .then((newBucket) => { + if (!newBucket) { + return + } + setBucketToManage(newBucket) + }) + .catch(console.error) + .finally(() => setIsFolderCreationLoading(false)) + + }, [handleCreateSharedFolder, sharedFolderName]) + + return ( + + {bucketToManage + ? + :
+
+
+ +
+
+ + Create Shared Folder + +
+
+
+
+ + {!!nameError && ( + + {nameError} + + )} +
+
+
+
+ + +
+
+
+ } + +
+ ) +} + +export default CreateSharedFolderModal diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx index 8fad35483c..020d7e7e60 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/FileInfoModal.tsx @@ -1,9 +1,4 @@ -import { - createStyles, - debounce, - makeStyles, - useThemeSwitcher -} from "@chainsafe/common-theme" +import { createStyles, debounce, makeStyles } from "@chainsafe/common-theme" import React, { useState, useEffect } from "react" import CustomModal from "../../Elements/CustomModal" import CustomButton from "../../Elements/CustomButton" @@ -152,7 +147,6 @@ const FileInfoModal = ({ filePath, close }: IFileInfoModuleProps) => { const [loadingFileInfo, setLoadingInfo] = useState(false) const [fullFileInfo, setFullFullInfo] = useState() const { bucket } = useFileBrowser() - const { desktop } = useThemeSwitcher() useEffect(() => { if (!bucket) return @@ -425,7 +419,7 @@ const FileInfoModal = ({ filePath, close }: IFileInfoModuleProps) => { close()} size="large" - variant={desktop ? "outline" : "gray"} + variant="outline" type="button" > Close diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolder.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolder.tsx new file mode 100644 index 0000000000..4a448795d7 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolder.tsx @@ -0,0 +1,433 @@ +import { Button, ShareAltSvg, Typography, Grid, TextInput, CrossIcon } from "@chainsafe/common-components" +import { createStyles, debounce, makeStyles, useOnClickOutside } from "@chainsafe/common-theme" +import React, { useState, useCallback, useRef, useEffect } from "react" +import { CSFTheme } from "../../../Themes/types" +import { BucketKeyPermission } from "../../../Contexts/FilesContext" +import CustomButton from "../../Elements/CustomButton" +import { t, Trans } from "@lingui/macro" +import { useCreateOrEditSharedFolder } from "./hooks/useCreateOrEditSharedFolder" +import { useLookupSharedFolderUser } from "./hooks/useLookupUser" +import { NonceResponsePermission, LookupUser } from "@chainsafe/files-api-client" +import clsx from "clsx" +import { Hashicon } from "@emeraldpay/hashicon-react" +import LinkList from "./Sharing/LinkList" +import PermissionsDropdown from "./Sharing/PermissionsDropdown" +import { UserName } from "../../Elements/UserName" + +const useStyles = makeStyles( + ({ breakpoints, constants, typography, palette }: CSFTheme) => { + return createStyles({ + root: { + padding: constants.generalUnit * 3, + flexDirection: "column", + display: "flex", + alignItems: "center", + [breakpoints.down("sm")]: { + padding: constants.generalUnit * 3 + } + }, + okButton: { + marginLeft: constants.generalUnit + }, + heading: { + color: constants.modalDefault.color, + fontWeight: typography.fontWeight.semibold, + marginBottom: constants.generalUnit * 3 + }, + iconBacking: { + backgroundColor: constants.modalDefault.iconBackingColor, + width: 48, + height: 48, + borderRadius: 24, + marginBottom: constants.generalUnit * 2, + marginTop: constants.generalUnit, + "& > svg": { + width: 16, + height: 16, + fill: palette.primary.main, + position: "relative", + display: "block", + transform: "translate(-50%, -50%)", + top: "50%", + left: "50%" + } + }, + inputLabel: { + fontSize: 16, + fontWeight: 600 + }, + footer: { + width: "100%", + paddingTop: constants.generalUnit * 2 + }, + options: { + backgroundColor: constants.header.optionsBackground, + color: constants.header.optionsTextColor, + border: `1px solid ${constants.header.optionsBorder}`, + minWidth: 145 + }, + dropdownTitle: { + padding: `${constants.generalUnit * 0.75}px ${constants.generalUnit}px`, + "& p": { + fontSize: "16px" + } + }, + dropdownTitleWithBorders: { + padding: `${constants.generalUnit * 0.75}px ${constants.generalUnit * 2}px`, + "& p": { + fontSize: "16px" + } + }, + permissionsInSuggestion: { + minWidth: 110 + }, + userNameSuggest: { + position: "relative", + width: "100%", + margin: 5 + }, + suggestionsDropDown: { + position: "absolute", + width: "100%", + backgroundColor: palette.additional["gray"][1], + border: `1px solid ${palette.additional["gray"][5]}`, + zIndex: 100 + }, + suggestionsBody: { + width: "100%", + padding: constants.generalUnit * 2 + }, + usernameBox: { + color: palette.additional["gray"][9], + padding: constants.generalUnit * 2, + cursor: "pointer", + ...typography.body1, + fontSize: "16px", + textOverflow: "ellipsis", + overflow: "hidden", + "&:hover": { + backgroundColor: palette.additional["blue"][1] + } + }, + usernameTextInput: { + margin: "0px !important", + width: "100%", + "& input": { + border: "0px", + "&:focus": { + border: "0px" + } + } + }, + usernameDropdownWrapper: { + display: "flex", + width: "100%", + justifyContent: "center", + alignItems: "center", + border: `1px solid ${palette.additional["gray"][6]}`, + borderRadius: "2px", + "&.focus": { + borderColor: palette.primary.border, + boxShadow: "0px 0px 4px rgba(24, 144, 255, 0.5)" + } + }, + subtitle: { + color: palette.additional["gray"][7] + }, + usersWrapper: { + margin: `${constants.generalUnit * 1.5}px 0`, + width: "100%" + }, + addedUserBox: { + display: "flex", + width: "100%", + justifyContent: "space-between", + padding: `${constants.generalUnit * 0.5}px 0px ${constants.generalUnit * 0.5}px ${constants.generalUnit}px` + }, + addedUserLabel: { + fontSize: "16px", + fontWeight: 600, + overflow: "hidden", + textOverflow: "ellipsis" + }, + hashIcon: { + marginRight: constants.generalUnit * 2, + marginTop: constants.generalUnit + }, + flexContainer: { + display: "flex", + alignItems: "center", + maxWidth: "calc(100% - 150px)" + }, + crossButton: { + padding: "0px !important", + "& svg": { + fill: palette.additional["gray"][7] + } + }, + crossIcon: { + fontSize: "22px" + }, + linksContainer: { + width: "100%" + } + }) + } +) + +interface ICreateOrManageSharedFolderProps { + onClose: () => void + bucketToEdit?: BucketKeyPermission +} + +const ManageSharedFolder = ({ onClose, bucketToEdit }: ICreateOrManageSharedFolderProps) => { + const classes = useStyles() + const { handleEditSharedFolder, isEditingSharedFolder, isCreatingSharedFolder } = useCreateOrEditSharedFolder() + const { sharedFolderReaders, + sharedFolderWriters, + onAddNewUser, + setSharedFolderReaders, + setSharedFolderWriters, + handleLookupUser, + resetUsers + } = useLookupSharedFolderUser() + const [hasPermissionsChanged, setHasPermissionsChanged] = useState(false) + const [newLinkPermission, setNewLinkPermission] = useState("read") + const [usernameSearch, setUsernameSearch] = useState() + const [suggestedUsers, setSuggestedUsers] = useState([]) + const [loadingUsers, setLoadingUsers] = useState(false) + const [searchActive, setSearchActive] = useState(false) + + const onReset = useCallback(() => { + setHasPermissionsChanged(false) + resetUsers() + }, [resetUsers]) + + useEffect(() => { + onReset() + + if (!bucketToEdit) return + + setSharedFolderReaders(bucketToEdit.readers) + setSharedFolderWriters(bucketToEdit.writers) + }, [bucketToEdit, setSharedFolderReaders, setSharedFolderWriters, onReset]) + + const handleClose = useCallback(() => { + onReset() + onClose() + }, [onClose, onReset]) + + const onEditSharedFolder = useCallback(() => { + if (!bucketToEdit) return + handleEditSharedFolder(bucketToEdit, sharedFolderReaders, sharedFolderWriters) + .catch(console.error) + .finally(handleClose) + }, [handleEditSharedFolder, sharedFolderWriters, sharedFolderReaders, handleClose, bucketToEdit]) + + const onLookupUser = (inputText?: string) => { + if (!inputText) return + handleLookupUser(inputText) + .then(setSuggestedUsers) + .catch(console.error) + .finally(() => setLoadingUsers(false)) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedHandleLookupUser = useCallback(debounce(onLookupUser, 400), [handleLookupUser]) + + const onUsernameChange = (v?: string | number) => { + setLoadingUsers(true) + setUsernameSearch(v?.toString()) + debouncedHandleLookupUser(v?.toString()) + } + + const ref = useRef(null) + useOnClickOutside(ref, () => { + if (searchActive) { + setSearchActive(false) + } + }) + + return ( +
+
+ +
+
+ + Manage Shared Folder + +
+
{ !searchActive && setSearchActive(true) }} + > +
+ setSearchActive(true)} + data-cy="input-edit-permission" + /> + setNewLinkPermission("read")} + onEditPermissionClick={() => setNewLinkPermission("write")} + permissions={["read", "write"]} + injectedClasses={{ + root: classes.permissionsInSuggestion, + options: classes.options, + dropdownTitle: classes.dropdownTitle + }} + withBorders={false} + /> +
+ {(!!usernameSearch && searchActive) &&
+ {suggestedUsers.length + ?
+ {suggestedUsers.map((u) =>
{ + onAddNewUser(u, newLinkPermission) + setSearchActive(false) + setUsernameSearch("") + setSuggestedUsers([]) + setHasPermissionsChanged(true) + }} + data-cy="user-lookup-result" + > + +
) + } +
+ :
+ + {loadingUsers + ? Loading... + : No users found + } + +
+ } +
+ } +
+
+ {[ + ...sharedFolderReaders.map((sr) => ({ user: sr, permission: "read" as NonceResponsePermission })), + ...sharedFolderWriters.map((sw) => ({ user: sw, permission: "write" as NonceResponsePermission })) + ].map((sharedFolderUser) =>
+
+
+ +
+ + + +
+
+ { + if (sharedFolderUser.permission === "write") { + setHasPermissionsChanged(true) + setSharedFolderWriters(sharedFolderWriters.filter((user) => sharedFolderUser.user.uuid !== user.uuid)) + setSharedFolderReaders([...sharedFolderReaders, sharedFolderUser.user]) + } + }} + onEditPermissionClick={() => { + if (sharedFolderUser.permission === "read") { + setHasPermissionsChanged(true) + setSharedFolderReaders(sharedFolderReaders.filter((user) => sharedFolderUser.user.uuid !== user.uuid)) + setSharedFolderWriters([...sharedFolderWriters, sharedFolderUser.user]) + } + }} + injectedClasses={{ + options: classes.options, + dropdownTitle: classes.dropdownTitleWithBorders + }} + permissions={["read", "write"]} + withBorders={true} + /> + +
+
)} +
+ {!!bucketToEdit &&
+ +
} + + + + Close + + + + +
+ ) +} + +export default ManageSharedFolder diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolderModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolderModal.tsx new file mode 100644 index 0000000000..169124a6f6 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ManageSharedFolderModal.tsx @@ -0,0 +1,48 @@ +import { createStyles, makeStyles } from "@chainsafe/common-theme" +import React from "react" +import CustomModal from "../../Elements/CustomModal" +import { CSFTheme } from "../../../Themes/types" +import { BucketKeyPermission } from "../../../Contexts/FilesContext" +import ManageSharedFolder from "./ManageSharedFolder" + +const useStyles = makeStyles( + ({ constants, zIndex }: CSFTheme) => { + return createStyles({ + modalRoot: { + zIndex: zIndex?.blocker + }, + modalInner: { + backgroundColor: constants.modalDefault.backgroundColor, + color: constants.modalDefault.color + } + }) + } +) + +interface ICreateOrManageSharedFolderModalProps { + isModalOpen: boolean + onClose: () => void + bucketToEdit?: BucketKeyPermission +} + +const CreateOrManageSharedFolderModal = ({ isModalOpen, onClose, bucketToEdit }: ICreateOrManageSharedFolderModalProps) => { + const classes = useStyles() + + return ( + + + + ) +} + +export default CreateOrManageSharedFolderModal diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx index b3d8dae63b..5c011b0e38 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/MoveFileModal.tsx @@ -243,7 +243,7 @@ const MoveFileModule = ({ filesToMove, onClose, onCancel, mode }: IMoveFileModul diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx index 6e5eaa1572..6f516f098c 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ReportFileModal.tsx @@ -1,9 +1,4 @@ -import { - createStyles, - debounce, - makeStyles, - useThemeSwitcher -} from "@chainsafe/common-theme" +import { createStyles, debounce, makeStyles } from "@chainsafe/common-theme" import React, { useState, useEffect } from "react" import CustomModal from "../../Elements/CustomModal" import CustomButton from "../../Elements/CustomButton" @@ -183,7 +178,6 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => { const { encryptForPublicKey } = useThresholdKey() const [copied, setCopied] = useState(false) const { filesApiClient } = useFilesApi() - const { desktop } = useThemeSwitcher() useEffect(() => { filesApiClient.abuseUser() @@ -382,7 +376,7 @@ const ReportFileModal = ({ filePath, close }: IReportFileModalProps) => { onClick={() => close()} size="large" className={classes.closeButton} - variant={desktop ? "outline" : "gray"} + variant="outline" type="button" > Close diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx index 9cc76ca907..b6caed723b 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/SearchFileBrowser.tsx @@ -17,6 +17,8 @@ const SearchFileBrowser: React.FC = ({ controls = false const { filesApiClient } = useFilesApi() const { buckets } = useFiles() const searchTerm = useMemo(() => decodeURIComponent(pathname.split("/").slice(2)[0]), [pathname]) + const [loadingSearchResults, setLoadingSearchResults] = useState(true) + const [searchResults, setSearchResults] = useState([]) const { redirect } = useHistory() const { addToast } = useToasts() @@ -27,28 +29,18 @@ const SearchFileBrowser: React.FC = ({ controls = false try { if (!searchString || !bucket) return [] - const results = (await filesApiClient.searchFiles({ bucket_id: bucket.id, query: searchString })) - .map(se => ({ ...se, content: parseFileContentResponse(se.content) })) - return results + const searchResults = await filesApiClient.searchFiles({ bucket_id: bucket.id, query: searchString }) + const results = searchResults?.map(se => ({ ...se, content: parseFileContentResponse(se.content) })) + return results || [] } catch (err) { addToast({ title: t`There was an error getting search results`, type: "error" }) - return Promise.reject(err) + return [] } }, [addToast, bucket, filesApiClient]) - useEffect(() => { - getSearchResults(searchTerm) - .then(setSearchResults) - .catch(console.error) - }, [searchTerm, getSearchResults]) - - const [loadingSearchResults, setLoadingSearchResults] = useState(true) - const [searchResults, setSearchResults] = useState([]) - - useEffect(() => { const onSearch = async () => { if (searchTerm) { @@ -63,8 +55,7 @@ const SearchFileBrowser: React.FC = ({ controls = false } } onSearch() - // eslint-disable-next-line - }, [searchTerm]) + }, [searchTerm, getSearchResults]) const getSearchEntry = useCallback((cid: string) => searchResults.find( diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx index 39c6a8f567..4361700925 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/ShareModal.tsx @@ -13,17 +13,12 @@ import { useFileBrowser } from "../../../Contexts/FileBrowserContext" import clsx from "clsx" import { useEffect } from "react" import { nameValidator } from "../../../Utils/validationSchema" -import CreateOrManageSharedFolder from "./CreateOrManageSharedFolder" -import LinkList from "./LinkSharing/LinkList" +import ManageSharedFolder from "./ManageSharedFolder" import { usePosthogContext } from "../../../Contexts/PosthogContext" import { useFilesApi } from "../../../Contexts/FilesApiContext" -interface StyleProps { - width: number -} - const useStyles = makeStyles( - ({ breakpoints, constants, palette, typography, zIndex }: CSFTheme) => { + ({ constants, palette, typography, zIndex }: CSFTheme) => { return createStyles({ root: { padding: constants.generalUnit * 3, @@ -33,14 +28,10 @@ const useStyles = makeStyles( modalRoot: { zIndex: zIndex?.blocker }, - modalInner: ({ width }: StyleProps) => ({ + modalInner: { backgroundColor: constants.fileInfoModal.background, - color: constants.fileInfoModal.color, - width, - [breakpoints.down("sm")]: { - width: "100%" - } - }), + color: constants.fileInfoModal.color + }, topIconContainer: { display: "flex", flexDirection: "column", @@ -52,19 +43,15 @@ const useStyles = makeStyles( flexDirection: "column" }, checkboxContainer: { - display: "flex", - justifyContent: "center", marginTop: constants.generalUnit * 4 }, buttonsContainer: { display: "flex", - flexDirection: "column", - alignItems: "center", - marginTop: constants.generalUnit * 2 + marginTop: constants.generalUnit * 2, + justifyContent: "flex-end" }, mainButton: { - width: 240, - marginBottom: constants.generalUnit * 0.5 + marginLeft: constants.generalUnit }, cancelButton: { maxWidth: 100 @@ -98,7 +85,7 @@ const useStyles = makeStyles( }, modalFlexItem: { width: "100%", - marginBottom: constants.generalUnit * 2 + marginBottom: constants.generalUnit }, newFolderInput: { margin: 0, @@ -122,23 +109,17 @@ const useStyles = makeStyles( errorText: { marginTop: constants.generalUnit * 1, color: palette.error.main - }, - subModal: ({ width }: StyleProps) => ({ - width, - [breakpoints.down("sm")]: { - width: "100%" - } - }) + } }) } ) -interface IShareFileProps { +interface IShareModalProps { fileSystemItems: FileSystemItem[] onClose: () => void } -const ShareModal = ({ onClose, fileSystemItems }: IShareFileProps) => { +const ShareModal = ({ onClose, fileSystemItems }: IShareModalProps) => { const { handleCreateSharedFolder } = useCreateOrEditSharedFolder() const { accountRestricted } = useFilesApi() const [sharedFolderName, setSharedFolderName] = useState("") @@ -153,9 +134,7 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareFileProps) => { const [nameError, setNameError] = useState("") const inSharedBucket = useMemo(() => bucket?.type === "share", [bucket]) - const classes = useStyles({ - width: bucketToUpload ? 600 : 500 - }) + const classes = useStyles() const isReader = useMemo(() => { if (!bucket) return false @@ -271,24 +250,15 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareFileProps) => { - )} + maxWidth={500} > {bucketToUpload - ? :
@@ -344,17 +314,20 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareFileProps) => { )}
{!hasNoSharedBucket && ( -
setIsUsingExistingBucket(!isUsingExistingBucket)} - > - - { - isUsingExistingBucket - ? Or Create a new shared folder - : Or Use an existing shared folder - } - +
+ or{" "} + setIsUsingExistingBucket(!isUsingExistingBucket)} + > + + { + isUsingExistingBucket + ? Create a new shared folder + : Use an existing shared folder + } + +
)}
@@ -371,9 +344,15 @@ const ShareModal = ({ onClose, fileSystemItems }: IShareFileProps) => {
)}
+ -
diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx index cb794e969f..7a12b8ee5e 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/SharedFoldersOverview.tsx @@ -17,16 +17,17 @@ import { BucketKeyPermission, useFiles } from "../../../Contexts/FilesContext" import { t, Trans } from "@lingui/macro" import { createStyles, makeStyles, useThemeSwitcher } from "@chainsafe/common-theme" import { CSFTheme } from "../../../Themes/types" -import CreateOrManageSharedFolderModal from "./CreateOrManageSharedFolderModal" import { useFilesApi } from "../../../Contexts/FilesApiContext" import { ROUTE_LINKS } from "../../FilesRoutes" import SharedFolderRow from "./views/FileSystemItem/SharedFolderRow" -import { SharedFolderModalMode } from "./types" import SharingExplainerModal from "../../SharingExplainerModal" import { useSharingExplainerModalFlag } from "./hooks/useSharingExplainerModalFlag" import { usePageTrack } from "../../../Contexts/PosthogContext" import RestrictedModeBanner from "../../Elements/RestrictedModeBanner" import clsx from "clsx" +import CreateSharedFolderModal from "./CreateSharedFolderModal" +import CreateOrManageSharedFolderModal from "./ManageSharedFolderModal" +import { useLanguageContext } from "../../../Contexts/LanguageContext" export const desktopSharedGridSettings = "50px 3fr 90px 140px 140px 45px !important" export const mobileSharedGridSettings = "3fr 80px 45px !important" @@ -114,14 +115,12 @@ const useStyles = makeStyles( } ) -type SortingType = "name" | "size" | "date_uploaded" +type SortingType = "name" | "size" const SharedFolderOverview = () => { const classes = useStyles() const { filesApiClient, accountRestricted } = useFilesApi() const { buckets, isLoadingBuckets, refreshBuckets } = useFiles() - const [createOrEditSharedFolderMode, setCreateOrEditSharedFolderMode] = useState(undefined) - const [bucketToEdit, setBucketToEdit] = useState(undefined) const [direction, setDirection] = useState("ascend") const [column, setColumn] = useState("name") const { redirect } = useHistory() @@ -131,6 +130,33 @@ const SharedFolderOverview = () => { const [isDeletingSharedFolder, setIsDeletingSharedFolder] = useState(false) const bucketsToShow = useMemo(() => buckets.filter(b => b.type === "share" && b.status !== "deleting"), [buckets]) const { hasSeenSharingExplainerModal, hideModal } = useSharingExplainerModalFlag() + const [isSharedFolderCreationModalOpen, setIsSharedFolderCreationModalOpen] = useState(false) + const [bucketToEdit, setBucketToEdit] = useState(undefined) + const { selectedLocale } = useLanguageContext() + const sortedBuckets = useMemo(() => { + let temp: BucketKeyPermission[] + + switch (column) { + case "size": { + temp = bucketsToShow.sort((a, b) => (a.size < b.size ? -1 : 1)) + break + } + // defaults to name sorting + default: { + temp = bucketsToShow.sort((a, b) => { + if(!a.name || !b.name) return 0 + + return a.name.localeCompare(b.name, selectedLocale, { + sensitivity: "base" + }) + }) + } + } + + return direction === "descend" + ? temp.reverse() + : temp + }, [bucketsToShow, column, direction, selectedLocale]) usePageTrack() @@ -138,7 +164,7 @@ const SharedFolderOverview = () => { refreshBuckets(true) }, [refreshBuckets]) - const handleSortToggle = (targetColumn: SortingType) => { + const handleSortToggle = useCallback((targetColumn: SortingType) => { if (column !== targetColumn) { setColumn(targetColumn) setDirection("descend") @@ -149,7 +175,7 @@ const SharedFolderOverview = () => { setDirection("ascend") } } - } + }, [column, direction]) const handleRename = useCallback((bucket: BucketKeyPermission, newName: string) => { filesApiClient.updateBucket(bucket.id, { @@ -192,10 +218,7 @@ const SharedFolderOverview = () => {
-
+
+ + setNewLinkPermission("read")} + onEditPermissionClick={() => setNewLinkPermission("write")} + withBorders={false} + injectedClasses={{ + options: classes.options, + dropdownTitle: classes.dropdownTitle + }} + testId="link-permission" + />
)} diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/Sharing/PermissionsDropdown.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/Sharing/PermissionsDropdown.tsx new file mode 100644 index 0000000000..09aa1b51da --- /dev/null +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/Sharing/PermissionsDropdown.tsx @@ -0,0 +1,141 @@ + +import { MenuDropdown, IMenuItem } from "@chainsafe/common-components" +import { createStyles, makeStyles } from "@chainsafe/common-theme" +import { NonceResponsePermission } from "@chainsafe/files-api-client" +import { t } from "@lingui/macro" +import clsx from "clsx" +import React, { ReactNode, useCallback } from "react" +import { CSFTheme } from "../../../../Themes/types" + +const useStyles = makeStyles( + ({ constants, palette }: CSFTheme) => { + return createStyles({ + icon: { + "& svg": { + fill: constants.header.iconColor + } + }, + menuItem: { + width: "100%", + display: "flex", + flexDirection: "row", + alignItems: "center", + color: constants.header.menuItemTextColor, + "& svg": { + width: constants.generalUnit * 2, + height: constants.generalUnit * 2, + marginRight: constants.generalUnit, + fill: palette.additional["gray"][7], + stroke: palette.additional["gray"][7] + } + }, + paddedTitle: { + paddingRight: constants.generalUnit * 2 + }, + permissionDropdownNoBorder: { + padding: `0px ${constants.generalUnit * 0.75}px`, + backgroundColor: palette.additional["gray"][1] + }, + permissionDropDownBorders: { + border: `1px solid ${palette.additional["gray"][5]}`, + width: "inherit", + marginRight: constants.generalUnit, + borderRadius: "2px" + } + }) + } +) + + +export interface LinkMenuItems { + id: NonceResponsePermission + onClick: () => void + contents: ReactNode +} + +export const readRights = t`view-only` +export const editRights = t`can-edit` +export const translatedPermission = (permission: NonceResponsePermission) => permission === "read" ? readRights : editRights + +interface Props { + onViewPermissionClick?: () => void + onEditPermissionClick?: () => void + selectedPermission: NonceResponsePermission + withBorders: boolean + injectedClasses: { + root?: string + options?: string + dropdownTitle?: string + } + permissions: NonceResponsePermission[] + testId?: string +} + +const PermissionsDropdown = ({ + onViewPermissionClick, + onEditPermissionClick, + selectedPermission, + withBorders, + permissions, + injectedClasses, + testId +}: Props) => { + + const classes = useStyles() + + const getMenuItems = useCallback(() => { + const menuItems: IMenuItem[] = [] + if (permissions.includes("read")) { + menuItems.push( + { + onClick: onViewPermissionClick, + contents: ( +
+ {readRights} +
+ ) + } + ) + } + if (permissions.includes("write")) { + menuItems.push( + { + onClick: onEditPermissionClick, + contents: ( +
+ {editRights} +
+ ) + } + ) + } + return menuItems + }, [classes.menuItem, onViewPermissionClick, onEditPermissionClick, permissions, testId]) + + + return ( + + ) +} + +export default PermissionsDropdown diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/SharingLink.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/Sharing/SharingLink.tsx similarity index 91% rename from packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/SharingLink.tsx rename to packages/files-ui/src/Components/Modules/FileBrowsers/Sharing/SharingLink.tsx index 12fbdfa5fb..9028201d63 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/LinkSharing/SharingLink.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/Sharing/SharingLink.tsx @@ -1,5 +1,5 @@ -import { CopyIcon, DeleteSvg, Loading, MoreIcon, Typography } from "@chainsafe/common-components" +import { CopyIcon, DeleteSvg, MoreIcon, Typography } from "@chainsafe/common-components" import { createStyles, debounce, makeStyles } from "@chainsafe/common-theme" import { NonceResponse } from "@chainsafe/files-api-client" import { Trans } from "@lingui/macro" @@ -9,7 +9,7 @@ import { useThresholdKey } from "../../../../Contexts/ThresholdKeyContext" import { CSFTheme } from "../../../../Themes/types" import Menu from "../../../../UI-components/Menu" import { ROUTE_LINKS } from "../../../FilesRoutes" -import { translatedPermission } from "./LinkList" +import { translatedPermission } from "./PermissionsDropdown" const useStyles = makeStyles( ({ constants, palette, zIndex, animation, typography }: CSFTheme) => { @@ -19,7 +19,7 @@ const useStyles = makeStyles( maxWidth: "100%", position: "relative", "&:not(:first-child)": { - marginTop: constants.generalUnit * 2 + marginTop: constants.generalUnit * 3 } }, linkWrapper: { @@ -127,7 +127,7 @@ const useStyles = makeStyles( interface Props { nonce: NonceResponse bucketEncryptionKey: string - refreshNonces: () => void + refreshNonces: (hideLoading?: boolean) => void } const SharingLink = ({ nonce, bucketEncryptionKey, refreshNonces }: Props) => { @@ -137,7 +137,6 @@ const SharingLink = ({ nonce, bucketEncryptionKey, refreshNonces }: Props) => { const [jwt, setJwt] = useState("") const { createJWT } = useThresholdKey() const [copied, setCopied] = useState(false) - const [isDeleting, setIsDeleting] = useState(false) useEffect(() => { if(!nonce?.bucket_id || !nonce?.id) { @@ -195,31 +194,13 @@ const SharingLink = ({ nonce, bucketEncryptionKey, refreshNonces }: Props) => { }, [debouncedSwitchCopied, link]) const onDeleteNonce = useCallback(() => { - setIsDeleting(true) filesApiClient.revokeNonce(nonce.id) .catch(console.error) .finally(() => { - refreshNonces() - setIsDeleting(false) + refreshNonces(true) }) }, [filesApiClient, nonce, refreshNonces]) - if (isDeleting) { - return ( - <> - - - - - ) - } - return (
{copied && ( @@ -243,7 +224,7 @@ const SharingLink = ({ nonce, bucketEncryptionKey, refreshNonces }: Props) => { data-cy="label-permission-type" > - {translatedPermission(nonce.permission)} + {translatedPermission(nonce.permission)}
{ const [isCreatingSharedFolder, setIsCreatingSharedFolder] = useState(false) const [isEditingSharedFolder, setIsEditingSharedFolder] = useState(false) const { createSharedFolder, editSharedFolder } = useFiles() - const getSharedUsers = (sharedUserTagData: SharedUserTagData[]) => sharedUserTagData.map(su => ({ - uuid: su.value, - pubKey: su.data.identity_pubkey?.slice(2) || "", - encryption_key: su.data.encryption_key - })) - const handleCreateSharedFolder = useCallback(( sharedFolderName: string, - sharedFolderReaders: SharedUserTagData[], - sharedFolderWriters: SharedUserTagData[] + sharedFolderReaders: LookupUser[], + sharedFolderWriters: LookupUser[] ) => { - const readers = getSharedUsers(sharedFolderReaders) - const writers = getSharedUsers(sharedFolderWriters) setIsCreatingSharedFolder(true) - return createSharedFolder(sharedFolderName.trim(), writers, readers) + return createSharedFolder(sharedFolderName.trim(), sharedFolderWriters, sharedFolderReaders) .then((bucket) => { setIsCreatingSharedFolder(false) return bucket @@ -34,13 +26,11 @@ export const useCreateOrEditSharedFolder = () => { const handleEditSharedFolder = useCallback(( bucketToEdit: BucketKeyPermission, - sharedFolderReaders: SharedUserTagData[], - sharedFolderWriters: SharedUserTagData[] + sharedFolderReaders: LookupUser[], + sharedFolderWriters: LookupUser[] ) => { - const readers = getSharedUsers(sharedFolderReaders) - const writers = getSharedUsers(sharedFolderWriters) setIsEditingSharedFolder(true) - return editSharedFolder(bucketToEdit, writers, readers) + return editSharedFolder(bucketToEdit, sharedFolderWriters, sharedFolderReaders) .then((bucket) => { setIsEditingSharedFolder(false) return bucket diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/hooks/useLookupUser.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/hooks/useLookupUser.tsx index 73b46f78d4..e4763fbcea 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/hooks/useLookupUser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/hooks/useLookupUser.tsx @@ -1,38 +1,17 @@ -import { t } from "@lingui/macro" import { useCallback, useState } from "react" import { useFilesApi } from "../../../../Contexts/FilesApiContext" import { useUser } from "../../../../Contexts/UserContext" -import { centerEllipsis } from "../../../../Utils/Helpers" -import { SharedUserTagData, SharedFolderUserPermission } from "../types" -import { ITagOption, ITagValueType } from "@chainsafe/common-components" -import { useEffect } from "react" import { ethers } from "ethers" +import { LookupUser, NonceResponsePermission } from "@chainsafe/files-api-client" export const useLookupSharedFolderUser = () => { const { filesApiClient } = useFilesApi() - const [sharedFolderReaders, setSharedFolderReaders] = useState([]) - const [sharedFolderWriters, setSharedFolderWriters] = useState([]) - const [usersError, setUsersError] = useState("") + const [sharedFolderReaders, setSharedFolderReaders] = useState([]) + const [sharedFolderWriters, setSharedFolderWriters] = useState([]) const { profile } = useUser() - useEffect(() => { - // check intersecting users - const foundIntersectingUsers = sharedFolderReaders - .filter( - reader => ( - sharedFolderWriters.some(writer => reader.data.uuid === writer.data.uuid) - )) - - if (foundIntersectingUsers.length) { - setUsersError(t`User ${centerEllipsis(foundIntersectingUsers[0].label) - } is both a reader and writer`) - } else { - setUsersError("") - } - }, [sharedFolderReaders, sharedFolderWriters]) - - const handleLookupUser = useCallback(async (inputVal: string, permission: SharedFolderUserPermission) => { + const handleLookupUser = useCallback(async (inputVal: string): Promise => { if (inputVal === "") return [] const lookupBody = { @@ -64,44 +43,38 @@ export const useLookupSharedFolderUser = () => { const result = await filesApiClient.lookupUser(lookupBody.username, lookupBody.public_address, lookupBody.identity_public_key) if (!result) return [] - const usersList = permission === "read" ? sharedFolderReaders : sharedFolderWriters - const currentUsers = usersList.map(su => su.value) + const currentUsers = [...sharedFolderReaders, ...sharedFolderWriters].map(su => su.uuid) // prevent the addition of current user since they are the owner if (currentUsers.includes(result.uuid) || result.uuid === profile?.userId) return [] - return [{ label: inputVal, value: result.uuid, data: result }] + return [result] } catch (e) { console.error("No user found", e) return Promise.resolve([]) } }, [filesApiClient, sharedFolderReaders, sharedFolderWriters, profile]) - const onNewUsers = useCallback((val: ITagValueType, permission: SharedFolderUserPermission) => { - const newSharedFolderUsers = val?.map(({ label, value, data }) => ({ label, value, data })) || [] - + const onAddNewUser = useCallback((user: LookupUser, permission: NonceResponsePermission) => { if (permission === "read") { - // setting readers - setSharedFolderReaders(newSharedFolderUsers) + setSharedFolderReaders([...sharedFolderReaders, user]) } else { - // setting writers - setSharedFolderWriters(newSharedFolderUsers) + setSharedFolderWriters([...sharedFolderWriters, user]) } - }, []) + }, [sharedFolderReaders, sharedFolderWriters]) const resetUsers = useCallback(() => { setSharedFolderReaders([]) setSharedFolderWriters([]) - setUsersError("") }, []) return { handleLookupUser, sharedFolderReaders, sharedFolderWriters, - onNewUsers, - usersError, - setUsersError, + setSharedFolderReaders, + setSharedFolderWriters, + onAddNewUser, resetUsers } } diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/types.ts b/packages/files-ui/src/Components/Modules/FileBrowsers/types.ts index 889f52e713..d5c9965f1d 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/types.ts +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/types.ts @@ -1,23 +1,6 @@ import { Crumb } from "@chainsafe/common-components" import { BucketType, FileSystemItem } from "../../../Contexts/FilesContext" -export type SharedFolderUserPermission = "read" | "write" -export type SharedFolderModalMode = "create" | "edit" - -export interface SharedFolderUser { - uuid?: string - username?: string - identity_pubkey?: string - public_address?: string - encryption_key?: string -} - -export interface SharedUserTagData { - label: string - value: string - data: SharedFolderUser -} - export type FileOperation = | "rename" | "delete" diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx index 9bd3fa29dc..69191e86ac 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemGridItem.tsx @@ -23,7 +23,6 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSFTheme) => alignItems: "center", justifyContent: "center", "& svg": { - width: constants.generalUnit * 2.5, fill: constants.fileSystemItemRow.icon } }, @@ -40,8 +39,11 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSFTheme) => maxWidth: constants.generalUnit * 24, border: `1px solid ${palette.additional["gray"][6]}`, boxShadow: constants.filesTable.gridItemShadow, + "& span": { + fontSize: "32px" + }, "& svg": { - width: "30%" + fontSize: "32px" }, [breakpoints.down("lg")]: { height: constants.generalUnit * 16 @@ -98,14 +100,15 @@ const useStyles = makeStyles(({ breakpoints, constants, palette }: CSFTheme) => }, gridFolderName: { textAlign: "center", - wordBreak: "break-all", - overflowWrap: "break-word", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", padding: constants.generalUnit }, gridViewIconNameBox: { display: "flex", flexDirection: "column", - width: "100%" + minWidth: "90%" }, menuTitleGrid: { padding: `0 ${constants.generalUnit * 0.5}px`, diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx index 57007b4475..9ba00d687d 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/FileSystemItem.tsx @@ -3,10 +3,6 @@ import { FormikTextInput, Typography, Button, - FileImageSvg, - FilePdfSvg, - FileTextSvg, - FolderFilledSvg, DownloadSvg, DeleteSvg, EditSvg, @@ -37,6 +33,7 @@ import { BucketUser } from "@chainsafe/files-api-client" import { useMemo } from "react" import { nameValidator } from "../../../../../Utils/validationSchema" import CustomButton from "../../../../Elements/CustomButton" +import { getIconForItem } from "../../../../../Utils/getItemIcon" const useStyles = makeStyles(({ breakpoints, constants }: CSFTheme) => { return createStyles({ @@ -151,7 +148,7 @@ const FileSystemItem = ({ }: IFileSystemItemProps) => { const { bucket, downloadFile, currentPath, handleUploadOnDrop, moveItems } = useFileBrowser() const { downloadMultipleFiles } = useFiles() - const { cid, name, isFolder, content_type } = file + const { cid, name, isFolder } = file const inSharedFolder = useMemo(() => bucket?.type === "share", [bucket]) const { @@ -202,17 +199,6 @@ const FileSystemItem = ({ formik.resetForm() }, [formik, setEditing]) - let Icon - if (isFolder) { - Icon = FolderFilledSvg - } else if (content_type.includes("image")) { - Icon = FileImageSvg - } else if (content_type.includes("pdf")) { - Icon = FilePdfSvg - } else { - Icon = FileTextSvg - } - const { desktop } = useThemeSwitcher() const classes = useStyles() const filePath = useMemo(() => getPathWithFile(currentPath, name), [currentPath, name]) @@ -478,6 +464,8 @@ const FileSystemItem = ({ } } + const Icon = getIconForItem(file) + const itemProps = { ref: fileOrFolderRef, owners, @@ -550,7 +538,7 @@ const FileSystemItem = ({ setEditing("")} size="medium" - variant={desktop ? "outline" : "gray"} + variant="gray" type="button" > Cancel diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx index e92b6511d2..a7ae684283 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FileSystemItem/SharedFolderRow.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from "react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { makeStyles, createStyles, useThemeSwitcher, useDoubleClick, useOnClickOutside } from "@chainsafe/common-theme" import { Button, @@ -159,6 +159,18 @@ const SharedFolderRow = ({ bucket, handleRename, openSharedFolder, handleDeleteS const [isRenaming, setIsRenaming] = useState(false) const formRef = useRef(null) const isOwner = useMemo(() => bucket.permission === "owner", [bucket.permission]) + const [ownerName, setOwnerName] = useState("") + + useEffect(() => { + if (isOwner) { + setOwnerName("me") + return + } + + getUserDisplayName(bucket.owners[0]) + .then(setOwnerName) + .catch(console.error) + }, [bucket, isOwner]) const menuItems: IMenuItem[] = isOwner ? [{ @@ -283,6 +295,7 @@ const SharedFolderRow = ({ bucket, handleRename, openSharedFolder, handleDeleteS
{desktop && - {isOwner - ? t`me` - : } + } - + {desktop && @@ -362,7 +379,7 @@ const SharedFolderRow = ({ bucket, handleRename, openSharedFolder, handleDeleteS setIsRenaming(false)} size="medium" - variant={desktop ? "outline" : "gray"} + variant="gray" type="button" > Cancel diff --git a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx index b404a43be2..657bdf5f28 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowsers/views/FilesList.tsx @@ -214,7 +214,7 @@ const useStyles = makeStyles( zIndex: zIndex?.layer4, bottom: 0, transform: "translateX(-50%)", - backgroundColor: palette.common.black.main, + backgroundColor: palette.additional["gray"][10], color: constants.filesTable.uploadText, opacity: 0, visibility: "hidden", @@ -267,19 +267,19 @@ const useStyles = makeStyles( }, gridRoot: { display: "grid", - gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr", + gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)", gridColumnGap: constants.generalUnit * 2, gridRowGap: constants.generalUnit * 2, marginBottom: constants.generalUnit * 4, marginTop: constants.generalUnit * 4, [breakpoints.down("lg")]: { - gridTemplateColumns: "1fr 1fr 1fr 1fr" + gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)" }, [breakpoints.down("md")]: { margin: `${constants.generalUnit * 4}px 0` }, [breakpoints.down("sm")]: { - gridTemplateColumns: "1fr 1fr", + gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)", margin: `${constants.generalUnit * 2}px 0` } }, @@ -316,6 +316,9 @@ const useStyles = makeStyles( }, buttonWrap: { whiteSpace: "nowrap" + }, + checkIcon: { + fill: palette.additional.gray[8] } }) } @@ -329,6 +332,8 @@ interface Props { isShared?: boolean } +type SortByType = "name" | "size" | "date_uploaded" + const FilesList = ({ isShared = false }: Props) => { const { themeKey, desktop } = useThemeSwitcher() const [isReportFileModalOpen, setIsReportFileModalOpen] = useState(false) @@ -362,7 +367,7 @@ const FilesList = ({ isShared = false }: Props) => { const [editing, setEditing] = useState() const [direction, setDirection] = useState("ascend") const [isSurveyBannerVisible, setIsSurveyBannerVisible] = useState(true) - const [column, setColumn] = useState<"name" | "size" | "date_uploaded">("name") + const [column, setColumn] = useState("name") const [selectedItems, setSelectedItems] = useState([]) const [fileIndex, setFileIndex] = useState() const { selectedLocale } = useLanguageContext() @@ -416,9 +421,7 @@ const FilesList = ({ isShared = false }: Props) => { selectedItems.some((item) => !!item.isFolder) , [selectedItems]) - const handleSortToggle = ( - targetColumn: "name" | "size" | "date_uploaded" - ) => { + const handleSortToggle = useCallback((targetColumn: SortByType) => { if (column !== targetColumn) { setColumn(targetColumn) setDirection("descend") @@ -429,7 +432,7 @@ const FilesList = ({ isShared = false }: Props) => { setDirection("ascend") } } - } + }, [column, direction]) const toggleSortDirection = () => { if (direction === "ascend") { @@ -865,7 +868,10 @@ const FilesList = ({ isShared = false }: Props) => { {isShared && bucket && (
- +
)}
@@ -1130,7 +1136,7 @@ const FilesList = ({ isShared = false }: Props) => { }, { contents: ( <> - {column === "name" && } + {column === "name" && } Name @@ -1140,7 +1146,7 @@ const FilesList = ({ isShared = false }: Props) => { }, { contents: ( <> - {column === "date_uploaded" && } + {column === "date_uploaded" && } Date Uploaded @@ -1150,7 +1156,7 @@ const FilesList = ({ isShared = false }: Props) => { }, { contents: ( <> - {column === "size" && } + {column === "size" && } Size diff --git a/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx b/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx index bbd278d157..5589911985 100644 --- a/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx +++ b/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx @@ -8,10 +8,10 @@ import { ArrowLeftIcon, ArrowRightIcon, Typography, - DownloadSvg, - MoreIcon, CloseCircleIcon, - ProgressBar + ProgressBar, + DownloadIcon, + CloseIcon } from "@chainsafe/common-components" import ImagePreview from "./PreviewRenderers/ImagePreview" import { useSwipeable } from "react-swipeable" @@ -26,8 +26,8 @@ import { CSFTheme } from "../../Themes/types" import { useFileBrowser } from "../../Contexts/FileBrowserContext" import { useGetFile } from "./FileBrowsers/hooks/useGetFile" import { useMemo } from "react" -import Menu from "../../UI-components/Menu" import { getPathWithFile } from "../../Utils/pathUtils" +import { getIconForItem } from "../../Utils/getItemIcon" export interface IPreviewRendererProps { contents: Blob @@ -48,7 +48,7 @@ const compatibleFilesMatcher = new MimeMatcher( ) const useStyles = makeStyles( - ({ constants, palette, zIndex, breakpoints }: CSFTheme) => + ({ constants, palette, zIndex }: CSFTheme) => createStyles({ root: { height: "100%", @@ -69,23 +69,24 @@ const useStyles = makeStyles( left: 0, top: 0, width: "100%", - maxWidth: breakpoints.values["md"] - 200, + backgroundColor: "rgba(0,0,0, 0.4)", height: constants.generalUnit * 8, - backgroundColor: constants.previewModal.controlsBackground, - color: constants.previewModal.controlsColor, - borderWidth: 1, - borderStyle: "solid", - borderColor: palette.additional["gray"][8] + "& > *":{ + margin: "0px 10px" + }, + "& > *:first-child": { + marginLeft: "24px" + } }, closePreviewButton: { marginRight: constants.generalUnit * 2, - marginLeft: constants.generalUnit * 2, - fill: constants.previewModal.closeButtonColor, - cursor: "pointer" + "& svg":{ + stroke: constants.previewModal.headerButtonColor + } + }, + downloadIcon: { + fill: constants.previewModal.headerButtonColor }, - // fileOperationsMenu: { - // fill: constants.previewModal.fileOpsColor - // }, fileName: { width: "100%", whiteSpace: "nowrap", @@ -102,9 +103,32 @@ const useStyles = makeStyles( alignItems: "center" }, prevNextButton: { - backgroundColor: palette.common.black.main, + backgroundColor: constants.previewModal.controlsBackgroundColor, + color: constants.previewModal.controlsButtonColor, padding: `${constants.generalUnit * 2}px !important`, - borderRadius: constants.generalUnit * 4 + borderRadius: constants.generalUnit * 4, + border: "none", + "& svg": { + fill: constants.previewModal.controlsButtonColor + }, + "&:hover": { + backgroundColor: constants.previewModal.controlsBackgroundHoverColor, + "& svg": { + fill: constants.previewModal.controlsButtonColor + } + }, + "&:active": { + backgroundColor: constants.previewModal.controlsBackgroundHoverColor, + "& svg": { + fill: constants.previewModal.controlsButtonColor + } + }, + "&:focus": { + backgroundColor: constants.previewModal.controlsBackgroundHoverColor, + "& svg": { + fill: constants.previewModal.controlsButtonColor + } + } }, previewContent: { color: constants.previewModal.message, @@ -147,21 +171,6 @@ const useStyles = makeStyles( item: { color: constants.previewModal.menuItemTextColor }, - dropdownIcon: { - width: 14, - height: 14, - padding: 0, - position: "relative", - fontSize: "unset", - "& svg": { - fill: constants.previewModal.fileOpsColor, - top: "50%", - left: 0, - width: 14, - height: 14, - position: "absolute" - } - }, focusVisible: { backgroundColor: "transparent !important" }, @@ -170,6 +179,16 @@ const useStyles = makeStyles( }, menuRoot: { zIndex: "2000 !important" as any + }, + fileIcon: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + "& svg": { + width: constants.generalUnit * 2.5, + fill: constants.previewModal.fileNameColor + } } }) ) @@ -225,7 +244,7 @@ const FilePreviewModal = ({ file, nextFile, previousFile, closePreview, filePath } }, [file, filePath, getFile, bucket, buckets, previewRendererKey]) - + const Icon = useMemo(() => getIconForItem(file), [file]) const PreviewComponent = !!content_type && @@ -260,20 +279,6 @@ const FilePreviewModal = ({ file, nextFile, previousFile, closePreview, filePath } }, [bucket, cid, downloadFile, file, fileContent, filePath, name]) - const menuItems = useMemo(() => [ - { - contents: ( - <> - - - Download - - - ), - onClick: handleDownload - } - ], [classes.menuIcon, handleDownload]) - if (!name || !cid || !content_type) { return null } @@ -281,11 +286,7 @@ const FilePreviewModal = ({ file, nextFile, previousFile, closePreview, filePath return (
- + {name} - } - options={menuItems} - style={{ - focusVisible: classes.focusVisible, - menuWrapper: classes.menuWrapper, - root: classes.menuRoot - }} - /> + +
{ const onSave = useCallback(() => { setIsAccepted(true) addNewDeviceShareAndSave() - .catch(console.error) + .catch(() => setIsAccepted(false)) }, [addNewDeviceShareAndSave]) const onDeny = useCallback(() => { diff --git a/packages/files-ui/src/Components/Modules/PreviewRenderers/ImagePreview.tsx b/packages/files-ui/src/Components/Modules/PreviewRenderers/ImagePreview.tsx index ddba1879cd..a40a5c574d 100644 --- a/packages/files-ui/src/Components/Modules/PreviewRenderers/ImagePreview.tsx +++ b/packages/files-ui/src/Components/Modules/PreviewRenderers/ImagePreview.tsx @@ -18,7 +18,7 @@ import { } from "@chainsafe/common-components" const useStyles = makeStyles( - ({ constants, palette, zIndex, breakpoints }: ITheme) => + ({ palette, zIndex, breakpoints }: ITheme) => createStyles({ root: { maxHeight: "100vh", @@ -32,14 +32,25 @@ const useStyles = makeStyles( zIndex: zIndex?.layer1, display: "flex", flexDirection: "row", - top: 0, - right: 0, - height: constants.generalUnit * 8, - backgroundColor: palette.additional["gray"][9], + bottom: 34, + height: 39, + left: "50%", + transform: "translate(-50%)", + backgroundColor: palette.additional["gray"][8], color: palette.additional["gray"][3], borderWidth: 1, borderStyle: "solid", - borderColor: palette.additional["gray"][8] + borderColor: palette.additional["gray"][8], + borderRadius: 2 + }, + imageControlButton: { + borderRadius: 0, + backgroundColor: "#262626", + color: "#D9D9D9", + border: "none", + "& svg": { + fill: "#D9D9D9" + } } }) ) @@ -91,18 +102,27 @@ const ImagePreview: React.FC = ({ contents, contentType } diff --git a/packages/files-ui/src/Components/Modules/PreviewRenderers/PDFPreview.tsx b/packages/files-ui/src/Components/Modules/PreviewRenderers/PDFPreview.tsx index e0e45f883f..fcebba1c5b 100644 --- a/packages/files-ui/src/Components/Modules/PreviewRenderers/PDFPreview.tsx +++ b/packages/files-ui/src/Components/Modules/PreviewRenderers/PDFPreview.tsx @@ -2,12 +2,11 @@ import React, { useEffect, useState } from "react" import { IPreviewRendererProps } from "../FilePreviewModal" import { makeStyles, ITheme, createStyles } from "@chainsafe/common-theme" import { Document, Page, pdfjs } from "react-pdf" -import { Button, Typography } from "@chainsafe/common-components" -import { Trans } from "@lingui/macro" +import { Button, CaretCircleLeftIcon, CaretCircleRightIcon, Typography } from "@chainsafe/common-components" pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.js" -const useStyles = makeStyles(({ constants, palette, zIndex }: ITheme) => +const useStyles = makeStyles(({ breakpoints, constants, zIndex }: ITheme) => createStyles({ controlsContainer: { position: "absolute", @@ -15,19 +14,40 @@ const useStyles = makeStyles(({ constants, palette, zIndex }: ITheme) => display: "flex", flexDirection: "row", alignItems: "center", - bottom: 0, - backgroundColor: palette.additional["gray"][9], - color: palette.additional["gray"][3], + bottom: 34, + backgroundColor: "#262626", + color: "#D9D9D9", borderWidth: 1, borderStyle: "solid", - borderColor: palette.additional["gray"][8] + borderColor: "#595959", + borderRadius: 2 }, pageButton: { - width: 80 + borderRadius: 0, + backgroundColor: "#262626", + color: "#D9D9D9", + border: "none", + "& svg": { + fill: "#D9D9D9" + } }, paginationInfo: { paddingLeft: constants.generalUnit * 2, - paddingRight: constants.generalUnit * 2 + paddingRight: constants.generalUnit * 2, + color: "#D9D9D9" + }, + pdfWrapper: { + "& canvas": { + maxHeight: "calc(100vh - 64px)", + width: "inherit !important", + [breakpoints.down("sm")]: { + width: "100% !important", + height: "auto !important" + } + } + }, + document: { + marginTop: "64px" } }) ) @@ -62,18 +82,22 @@ const PdfPreview: React.FC = ({ contents }) => { return ( <> - - - +
+ + + +
{pageNumber} of {numPages} @@ -81,8 +105,9 @@ const PdfPreview: React.FC = ({ contents }) => {
diff --git a/packages/files-ui/src/Components/Modules/PreviewRenderers/TextPreview.tsx b/packages/files-ui/src/Components/Modules/PreviewRenderers/TextPreview.tsx index 0efb73d222..7ee3882bd5 100644 --- a/packages/files-ui/src/Components/Modules/PreviewRenderers/TextPreview.tsx +++ b/packages/files-ui/src/Components/Modules/PreviewRenderers/TextPreview.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback } from "react" import { IPreviewRendererProps } from "../FilePreviewModal" -import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" import { CSFTheme } from "../../../Themes/types" import { Button, ZoomInIcon, ZoomOutIcon, ScrollbarWrapper } from "@chainsafe/common-components" @@ -38,17 +38,24 @@ const useStyles = makeStyles(({ palette, constants, typography, zIndex }: CSFThe zIndex: zIndex?.layer1, display: "flex", flexDirection: "row", - top: 0, - right: 0, - height: constants.generalUnit * 8, + bottom: 34, + height: 39, + left: "50%", + transform: "translate(-50%)", backgroundColor: palette.additional["gray"][9], color: palette.additional["gray"][3], borderWidth: 1, borderStyle: "solid", borderColor: palette.additional["gray"][8] }, - pageButton: { - width: 80 + controlButton: { + borderRadius: 0, + backgroundColor: "#262626", + color: "#D9D9D9", + border: "none", + "& svg": { + fill: "#D9D9D9" + } } }) ) @@ -73,27 +80,37 @@ const TextPreview: React.FC = ({ contents }) => { setFontSize(fontSize - 2) }, [fontSize]) + const { desktop } = useThemeSwitcher() + return (
{contentText} -
- - -
+ {desktop && ( +
+ + +
+ )}
) } diff --git a/packages/files-ui/src/Components/Modules/SearchModule.tsx b/packages/files-ui/src/Components/Modules/SearchModule.tsx index 978caf5a8e..9e4670e629 100644 --- a/packages/files-ui/src/Components/Modules/SearchModule.tsx +++ b/packages/files-ui/src/Components/Modules/SearchModule.tsx @@ -1,19 +1,7 @@ -import { - createStyles, - debounce, - makeStyles, - useOnClickOutside, - useThemeSwitcher -} from "@chainsafe/common-theme" +import { createStyles, debounce, makeStyles, useOnClickOutside, useThemeSwitcher } from "@chainsafe/common-theme" import React, { ChangeEvent, useCallback, useMemo, useRef } from "react" import { - ArrowLeftIcon, - Button, - SearchBar, - Typography, - useHistory, - useToasts -} from "@chainsafe/common-components" + SearchBar, Typography, useHistory, useToasts } from "@chainsafe/common-components" import { useState } from "react" import clsx from "clsx" import { ROUTE_LINKS } from "../FilesRoutes" @@ -37,6 +25,7 @@ const useStyles = makeStyles( position: "relative", [breakpoints.down("md")]: { display: "flex", + paddingLeft: constants.generalUnit * 5.5, "& input": { opacity: 0 }, @@ -146,16 +135,9 @@ interface ISearchModule { setSearchActive: (searchActive: boolean) => void } -const SearchModule: React.FC = ({ - className, - searchActive, - setSearchActive -}: ISearchModule) => { +const SearchModule = ({ className, searchActive, setSearchActive }: ISearchModule) => { const { themeKey, desktop } = useThemeSwitcher() - const classes = useStyles({ - themeKey - }) - + const classes = useStyles({ themeKey }) const [searchQuery, setSearchQuery] = useState("") const [searchResults, setSearchResults] = useState<{results: SearchEntry[]; query: string} | undefined>(undefined) const ref = useRef(null) @@ -169,13 +151,13 @@ const SearchModule: React.FC = ({ if (!searchString || !bucket) return [] const results = await filesApiClient.searchFiles({ bucket_id: bucket.id, query: searchString }) - return results + return results || [] } catch (err) { addToast({ title: t`There was an error getting search results`, type: "error" }) - return Promise.reject(err) + return [] } }, [addToast, bucket, filesApiClient]) @@ -212,12 +194,12 @@ const SearchModule: React.FC = ({ redirect(ROUTE_LINKS.Search(encodeURIComponent(searchQuery))) } - const searchResultsFiles = searchResults?.results.filter( + const searchResultsFiles = searchResults?.results?.filter( (searchResult) => searchResult.content.content_type !== CONTENT_TYPES.Directory ) - const searchResultsFolders = searchResults?.results.filter( + const searchResultsFolders = searchResults?.results?.filter( (searchResult) => searchResult.content.content_type === CONTENT_TYPES.Directory ) @@ -242,16 +224,6 @@ const SearchModule: React.FC = ({ active: searchActive })} > - {!desktop && searchActive && ( - - )} = ({ {searchQuery && searchResults?.query && (
- {searchResults?.query && !searchResults.results.length && ( + {searchResults?.query && !searchResults.results?.length && ( No search results for {` ${searchResults.query}`} diff --git a/packages/files-ui/src/Components/Modules/Settings/ProfileTab/LanguageSelection.tsx b/packages/files-ui/src/Components/Modules/Settings/DisplayTab/LanguageSelection.tsx similarity index 100% rename from packages/files-ui/src/Components/Modules/Settings/ProfileTab/LanguageSelection.tsx rename to packages/files-ui/src/Components/Modules/Settings/DisplayTab/LanguageSelection.tsx diff --git a/packages/files-ui/src/Components/Modules/Settings/DisplayTab/index.tsx b/packages/files-ui/src/Components/Modules/Settings/DisplayTab/index.tsx new file mode 100644 index 0000000000..92bd3d64d7 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/Settings/DisplayTab/index.tsx @@ -0,0 +1,150 @@ +import React from "react" +import { Grid, Typography, RadioInput, Divider } from "@chainsafe/common-components" +import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" +import { t, Trans } from "@lingui/macro" +import { CSFTheme } from "../../../../Themes/types" +import clsx from "clsx" +import LanguageSelection from "./LanguageSelection" + +const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: CSFTheme) => + createStyles({ + container: { + [breakpoints.down("md")]: { + paddingRight: constants.generalUnit * 2, + paddingLeft: constants.generalUnit * 2 + } + }, + profileBox: { + maxWidth: 420 + }, + themeBox: { + height: 87, + borderRadius: 4, + paddingLeft: 20, + paddingTop: 14, + margin: "6px 0", + [breakpoints.down("sm")]: { + width: "100%" + }, + cursor: "pointer", + "&:last-child": { + [breakpoints.up("sm")]: { + marginLeft: constants.generalUnit + } + } + }, + themeBoxDark: { + ...constants.settingsPage.darkSwitch + }, + themeBoxLight: { + ...constants.settingsPage.lightSwitch + }, + themeSubtitle: { + ...typography.body1, + color: palette.additional.gray[8] + }, + sectionSubHeading: { + fontWeight: 400, + marginTop: 25, + marginBottom: 14, + marginLeft: constants.generalUnit * 2 + }, + mainHeader: { + fontSize: 28, + marginBottom: constants.generalUnit * 2, + paddingLeft: constants.generalUnit * 2 + }, + paddedBox: { + paddingLeft: constants.generalUnit * 2 + }, + paddedBox2: { + paddingLeft: constants.generalUnit + } + }) +) + +const DisplayView = () => { + const { themeKey, setTheme } = useThemeSwitcher() + const classes = useStyles() + + return ( + + +
+ + Display + + +
+ + Theme + + + + + + + + + +
+ + Language + +
+ +
+
+
+
+ ) +} + +export default DisplayView diff --git a/packages/files-ui/src/Components/Modules/Settings/ProfileTab/index.tsx b/packages/files-ui/src/Components/Modules/Settings/ProfileTab/index.tsx index df9fa9e93d..e5c8e31a89 100644 --- a/packages/files-ui/src/Components/Modules/Settings/ProfileTab/index.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/ProfileTab/index.tsx @@ -1,35 +1,26 @@ import React, { useState, useCallback, useMemo } from "react" -import * as yup from "yup" import { - FormikTextInput, Grid, Button, Typography, useToasts, - RadioInput, TextInput, CheckIcon, - CheckboxInput, - Divider + Divider, + ToggleSwitch } from "@chainsafe/common-components" -import { - makeStyles, - createStyles, - debounce, - useThemeSwitcher -} from "@chainsafe/common-theme" -import { LockIcon, CopyIcon } from "@chainsafe/common-components" -import { Form, useFormik, FormikProvider } from "formik" +import { makeStyles, createStyles, debounce } from "@chainsafe/common-theme" +import { CopyIcon } from "@chainsafe/common-components" import { useUser } from "../../../../Contexts/UserContext" import { t, Trans } from "@lingui/macro" import { centerEllipsis } from "../../../../Utils/Helpers" import { CSFTheme } from "../../../../Themes/types" -import clsx from "clsx" -import LanguageSelection from "./LanguageSelection" import { useThresholdKey } from "../../../../Contexts/ThresholdKeyContext" import EthCrypto from "eth-crypto" +// import { Form, useFormik, FormikProvider } from "formik" +// import * as yup from "yup" -const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: CSFTheme) => +const useStyles = makeStyles(({ constants, breakpoints, palette }: CSFTheme) => createStyles({ container: { [breakpoints.down("md")]: { @@ -37,18 +28,17 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: C paddingLeft: constants.generalUnit * 2 } }, - sectionContainer: { - borderBottom: `1px solid ${palette.additional["gray"][4]}`, - marginBottom: 32, - [breakpoints.down("md")]: { - borderBottom: "none" - } - }, boxContainer: { - marginBottom: constants.generalUnit * 4 + marginBottom: constants.generalUnit * 4, + [breakpoints.up("md")]: { + marginLeft: constants.generalUnit * 2 + } }, inputBoxContainer: { - marginBottom: constants.generalUnit * 3 + marginBottom: constants.generalUnit * 3, + [breakpoints.up("md")]: { + marginLeft: constants.generalUnit * 2 + } }, labelContainer: { marginBottom: constants.generalUnit @@ -56,6 +46,7 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: C walletAddressContainer: { display: "flex", justifyContent: "space-between", + alignItems: "center", marginBottom: constants.generalUnit * 0.5 }, input: { @@ -64,8 +55,7 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: C marginBottom: constants.generalUnit }, label: { - marginBottom: constants.generalUnit * 1, - fontSize: 20 + marginBottom: constants.generalUnit * 1 }, subLabel: { marginBottom: constants.generalUnit * 1, @@ -90,12 +80,10 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: C }, button: { width: 200, - margin: `0px ${constants.generalUnit * 0.5}px ${constants.generalUnit * 4 - }px` - }, - icon: { - fontSize: "20px", - margin: "-2px 2px 0 2px" + margin: `0px ${constants.generalUnit * 0.5}px ${constants.generalUnit * 4}px`, + [breakpoints.up("md")]: { + marginLeft: constants.generalUnit * 2 + } }, copyIcon: { fontSize: "14px", @@ -111,10 +99,7 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: C wordBreak: "break-all", paddingRight: constants.generalUnit * 2, width: "90%", - ...typography.body1, - [breakpoints.down("md")]: { - ...typography.body2 - } + fontSize: 16 }, copyText: { padding: `${constants.generalUnit / 2}px ${constants.generalUnit}px`, @@ -122,34 +107,6 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: C borderRadius: 2, color: constants.loginModule.flagText }, - themeBox: { - height: 87, - borderRadius: 4, - paddingLeft: 20, - paddingTop: 14, - margin: "6px 0", - [breakpoints.down("sm")]: { - width: "100%" - }, - cursor: "pointer" - }, - themeBoxDark: { - ...constants.settingsPage.darkSwitch - }, - themeBoxLight: { - marginLeft: constants.generalUnit, - ...constants.settingsPage.lightSwitch - }, - themeSubtitle: { - ...typography.body1, - color: palette.additional.gray[8] - }, - sectionSubHeading: { - ...typography.h5, - fontWeight: 400, - marginTop: 25, - marginBottom: 14 - }, buttonLink: { color: palette.additional["gray"][10], outline: "none", @@ -171,53 +128,62 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: C flex: 1, margin: 0, paddingRight: constants.generalUnit + }, + mainHeader: { + fontSize: 28, + marginBottom: constants.generalUnit * 2, + [breakpoints.up("md")]: { + paddingLeft: constants.generalUnit * 2 + } + }, + lookupText: { + paddingLeft: constants.generalUnit } }) ) -const profileValidation = yup.object().shape({ - // email: yup.string().email("Email is invalid").required("Email is required"), - firstName: yup.string(), - lastName: yup.string(), - username: yup.string() -}) +// const profileValidation = yup.object().shape({ +// email: yup.string().email("Email is invalid").required("Email is required"), +// firstName: yup.string(), +// lastName: yup.string(), +// username: yup.string() +// }) const ProfileView = () => { - const { themeKey, setTheme } = useThemeSwitcher() const { addToast } = useToasts() - const { profile, updateProfile, addUsername, lookupOnUsername, toggleLookupConsent } = useUser() + const { profile, addUsername, lookupOnUsername, toggleLookupConsent } = useUser() const { publicKey } = useThresholdKey() - const [updatingProfile, setUpdatingProfile] = useState(false) const [showUsernameForm, setShowUsernameForm] = useState(false) const [username, setUsername] = useState("") const [usernameData, setUsernameData] = useState({ error: "", loading: false }) - const formik = useFormik({ - initialValues: { - firstName: profile?.firstName || "", - lastName: profile?.lastName || "" - // email: profile?.email || "" - }, - onSubmit: (values) => { - onUpdateProfile( - values.firstName || "", - values.lastName || "" - // values.email || "" - ) - }, - validationSchema: profileValidation, - validateOnChange: false - }) - const onUpdateProfile = async (firstName: string, lastName: string) => { - try { - setUpdatingProfile(true) - await updateProfile(firstName, lastName) - addToast({ title: t`Profile updated`, type: "success", testId: "profile-update-success" }) - setUpdatingProfile(false) - } catch (error) { - error instanceof Error && addToast({ title: error.message, type: "error" }) - setUpdatingProfile(false) - } - } + // const [updatingProfile, setUpdatingProfile] = useState(false) + // const formik = useFormik({ + // initialValues: { + // firstName: profile?.firstName || "", + // lastName: profile?.lastName || "" + // email: profile?.email || "" + // }, + // onSubmit: (values) => { + // onUpdateProfile( + // values.firstName || "", + // values.lastName || "" + // values.email || "" + // ) + // }, + // validationSchema: profileValidation, + // validateOnChange: false + // }) + // const onUpdateProfile = async (firstName: string, lastName: string) => { + // try { + // setUpdatingProfile(true) + // await updateProfile(firstName, lastName) + // addToast({ title: t`Profile updated`, type: "success", testId: "profile-update-success" }) + // setUpdatingProfile(false) + // } catch (error) { + // error instanceof Error && addToast({ title: error.message, type: "error" }) + // setUpdatingProfile(false) + // } + // } const classes = useStyles() @@ -321,24 +287,46 @@ const ProfileView = () => {
+ + Profile + +
- - Profile settings - - - {profile?.publicAddress && -
+ + Account visibility + +
+ + + + Allow lookup by sharing key, wallet address, username or ENS + + +
+
+ {profile?.publicAddress && +
Wallet address @@ -371,7 +359,8 @@ const ProfileView = () => { >
Files sharing key @@ -401,7 +390,8 @@ const ProfileView = () => { {profile?.username ? <> Username @@ -417,7 +407,8 @@ const ProfileView = () => { : <> Username @@ -479,16 +470,13 @@ const ProfileView = () => { } } -
- + {/*
First name @@ -510,8 +498,8 @@ const ProfileView = () => {
Last name @@ -525,7 +513,7 @@ const ProfileView = () => { data-cy="input-profile-lastname" />
- {/*
+
{ label="Email" disabled={!profile?.publicAddress} /> -
*/} - +
-
+
*/}
{/*
@@ -585,64 +570,6 @@ const ProfileView = () => {
*/} -
- - Display Settings - - - Theme - - - - - - - - - -
- - Language - -
diff --git a/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/BrowserPanel.tsx b/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/BrowserPanel.tsx index b49108aa76..7843a1075b 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/BrowserPanel.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/BrowserPanel.tsx @@ -1,9 +1,5 @@ import React, { useCallback, useState } from "react" -import { - makeStyles, - createStyles, - useThemeSwitcher -} from "@chainsafe/common-theme" +import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" import { Button, ExpansionPanel, Typography } from "@chainsafe/common-components" import clsx from "clsx" @@ -13,16 +9,15 @@ import { BrowserShare, useThresholdKey } from "../../../../../Contexts/Threshold import CustomModal from "../../../../Elements/CustomModal" import CustomButton from "../../../../Elements/CustomButton" -const useStyles = makeStyles(({ palette, constants, animation, breakpoints }: CSFTheme) => +const useStyles = makeStyles(({ palette, constants, breakpoints }: CSFTheme) => createStyles({ panelHeading: { backgroundColor: palette.additional["gray"][4], - borderRadius: "10px", + borderRadius: "16px", padding: `${constants.generalUnit}px 0 ${constants.generalUnit}px ${constants.generalUnit * 2}px`, - transition: `border-radius ${animation.transform}ms`, - "&.active": { - borderBottomLeftRadius: 0, - borderBottomRightRadius: 0 + "& > span":{ + fontWeight: 400, + fontSize: 16 } }, panelBody: { @@ -30,7 +25,7 @@ const useStyles = makeStyles(({ palette, constants, animation, breakpoints }: CS padding: 0, borderBottomLeftRadius: "10px", borderBottomRightRadius: "10px", - marginTop: `-${constants.generalUnit}px` + marginTop: constants.generalUnit }, panelContent: { marginTop: constants.generalUnit, @@ -110,7 +105,6 @@ const BrowserPanel = ({ dateAdded, shareIndex, browser, os }: BrowserShare) => { const [loadingDeleteShare, setLoadingDeleteShare] = useState(false) const [loadingDownloadKey, setLoadingDownloadKey] = useState(false) const [isModalConfirmationOpen, setIsModalConfirmationOpen] = useState(false) - const { desktop } = useThemeSwitcher() const onDeleteShare = useCallback(() => { setLoadingDeleteShare(true) @@ -216,7 +210,7 @@ const BrowserPanel = ({ dateAdded, shareIndex, browser, os }: BrowserShare) => {
setIsModalConfirmationOpen(false)} className={classes.cancelButton} disabled={loadingDeleteShare} diff --git a/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/index.tsx b/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/index.tsx index 47d41ba69b..b60809f1a0 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/index.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SecurityTab/SavedBrowsers/index.tsx @@ -1,8 +1,5 @@ import React from "react" -import { - makeStyles, - createStyles -} from "@chainsafe/common-theme" +import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" import { Loading, Typography } from "@chainsafe/common-components" import BrowserPanel from "./BrowserPanel" @@ -18,12 +15,13 @@ const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) => } }, title: { - fontSize: "16px", - lineHeight: "24px", - paddingBottom: constants.generalUnit * 2 + marginBottom: constants.generalUnit * 1.5 }, expansionContainer: { - marginBottom: constants.generalUnit * 3 + marginTop: constants.generalUnit * 0.5 + }, + loader : { + marginLeft: constants.generalUnit } }) ) @@ -35,13 +33,15 @@ const SavedBrowsers: React.FC<{isRefreshing: boolean}> = ({ isRefreshing }) => { return (
- Saved Browsers {isRefreshing && Saved Browsers + {isRefreshing && } {browserShares diff --git a/packages/files-ui/src/Components/Modules/Settings/SecurityTab/index.tsx b/packages/files-ui/src/Components/Modules/Settings/SecurityTab/index.tsx index 6bf3951fac..1296e14064 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SecurityTab/index.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SecurityTab/index.tsx @@ -13,7 +13,6 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography, zIn createStyles({ root: { paddingBottom: constants.generalUnit * 3, - maxWidth: breakpoints.values["md"], [breakpoints.down("md")]: { padding: constants.generalUnit * 2 } @@ -118,6 +117,18 @@ const useStyles = makeStyles(({ constants, breakpoints, palette, typography, zIn divider: { zIndex: zIndex?.layer1, marginTop: constants.generalUnit * 3 + }, + mainHeader: { + fontSize: 28, + marginBottom: constants.generalUnit * 2, + [breakpoints.up("md")]: { + paddingLeft: constants.generalUnit * 2 + } }, + settingsSection: { + maxWidth: breakpoints.values["md"], + [breakpoints.up("md")]: { + padding: `0 ${constants.generalUnit * 2}px` + } } }) ) @@ -161,163 +172,174 @@ const Security = ({ className }: SecurityProps) => { - Sign-in methods + Security - {showWarning && ( +
- - Hey! You only have two sign-in methods. If you lose that and have only one left, - you will be locked out of your account forever. - + Sign-in methods - )} - { - !!loggedinAs && ( -
-
- - - Social Sign-in Wallet - - - { - desktop && ( - - { loggedinAs } - - ) - } -
-
- ) - } -
-
- - - Saved Browser - - - - {browserShares.length} Saved{" "} - -
-
- {showWarning && ( -
+ {showWarning && ( - Add at least one more authentication method to protect your account. - You’d only need any two to sign in to Files from any device. + Hey! You only have two sign-in methods. If you lose that and have only one left, + you will be locked out of your account forever. -
- )} - { isChangingPassword - ? ( -
- + )} + { + !!loggedinAs && ( +
+
+ + + Social Sign-in Wallet + + + { + desktop && ( + + { loggedinAs } + + ) + } +
+
+ ) + } +
+
+ + + Saved Browser + + + + {browserShares.length} Saved{" "} + +
+
+ {showWarning && ( +
- Change password + Add at least one more authentication method to protect your account. + You’d only need any two to sign in to Files from any device. - -
- ) - : ( -
+
+ )} + { isChangingPassword + ? ( +
+ + + + Change password + + + +
+ ) + : ( +
+
+ + + Password + + + + { + {setIsChangingPassword(true)}} + > + Change Password + + } + +
+
+ )} + { isSettingBackupPhrase + ? ( +
+ + + Generate backup secret phrase + + + + + A backup secret phrase will be generated and used for your account.
+ We do not store it and it can only be displayed once. Save it somewhere safe! +
+
+ setIsSettingBackupPhrase(false)} + /> +
+ ) + : (
- Password + Backup secret phrase - { - {setIsChangingPassword(true)}} - > - Change Password - + { !hasMnemonicShare + ? ( + + {setIsSettingBackupPhrase(true)}} + > + Generate backup secret phrase + + + ) + : ( + + Generated + + ) }
-
- )} - { isSettingBackupPhrase - ? ( -
- - - Generate backup secret phrase - - - - - A backup secret phrase will be generated and used for your account.
- We do not store it and it can only be displayed once. Save it somewhere safe! -
-
- setIsSettingBackupPhrase(false)} - /> -
- ) - : (
-
- - - Backup secret phrase - - - - { !hasMnemonicShare - ? ( - - {setIsSettingBackupPhrase(true)}} - > - Generate backup secret phrase - - - ) - : ( - - Generated - - ) - } - -
-
) - } + ) + } +
- +
+ +
) } diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/BillingHistory.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/BillingHistory.tsx index 53b84c059e..1396b1d3ec 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/BillingHistory.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/BillingHistory.tsx @@ -1,59 +1,74 @@ import React, { useState } from "react" -import { Typography, Link } from "@chainsafe/common-components" +import { Link, Typography } from "@chainsafe/common-components" import { makeStyles, ITheme, createStyles } from "@chainsafe/common-theme" import { Trans } from "@lingui/macro" -import { ROUTE_LINKS } from "../../../FilesRoutes" import InvoiceLines from "../../../Elements/InvoiceLines" import PayInvoiceModal from "./PayInvoice/PayInvoiceModal" import { useBilling } from "../../../../Contexts/BillingContext" +import { ROUTE_LINKS } from "../../../FilesRoutes" const useStyles = makeStyles(({ constants }: ITheme) => createStyles({ container: { - padding: constants.generalUnit, + padding: `${constants.generalUnit}px 0`, margin: `${constants.generalUnit * 1.5}px 0` }, link: { textAlign: "right", - marginBottom: constants.generalUnit + fontSize: 16 + }, + spaceBetweenBox: { + display: "flex", + justifyContent: "space-between", + alignItems: "center" + }, + billingText: { + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit * 2 } }) ) const BillingHistory = () => { const classes = useStyles() - const [isPayInvoiceModalVisible, setPayInvoiceModalVisible] = useState(false) - const { isPendingInvoice } = useBilling() + const [invoiceToPay, setInvoiceToPay] = useState() + const { isPendingInvoice, openInvoice } = useBilling() return (
- - Billing history - - {isPendingInvoice && +
+ + Billing history + + + + All invoices + + +
+ {(isPendingInvoice || openInvoice) && + Please complete payment of the following outstanding invoices in order to avoid account suspension } - - - All invoices - - setPayInvoiceModalVisible(true)} + payInvoice={(invoiceId) => setInvoiceToPay(invoiceId)} /> - { - isPayInvoiceModalVisible && setPayInvoiceModalVisible(false)} - /> - } + {invoiceToPay && setInvoiceToPay(undefined)} + />}
) } diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/ChangePlanModal.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/ChangePlanModal.tsx index c9b016fbbe..939e13eea8 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/ChangePlanModal.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/ChangePlanModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" import { Modal } from "@chainsafe/common-components" @@ -8,11 +8,13 @@ import PaymentMethodSelector from "./PaymentMethodSelector" import ConfirmPlan from "../Common/ConfirmPlan" import { useBilling } from "../../../../../Contexts/BillingContext" import { Product, ProductPrice, ProductPriceRecurringInterval } from "@chainsafe/files-api-client" -import PlanSuccess from "./PlanSuccess" +import PlanSuccess from "../Common/PlanSuccess" import DowngradeDetails from "./DowngradeDetails" import { PaymentMethod } from "../../../../../Contexts/BillingContext" import CryptoPayment from "../Common/CryptoPayment" -const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) => +import { formatSubscriptionError } from "../utils/formatSubscriptionError" + +const useStyles = makeStyles(({ constants, breakpoints, palette }: CSFTheme) => createStyles({ root: { "&:before": { @@ -27,6 +29,18 @@ const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) => [breakpoints.down("sm")]: { width: "100%" } + }, + warningText: { + marginTop: constants.generalUnit * 3, + maxWidth: constants.generalUnit * 56, + color: palette.additional["gray"][7] + }, + icon : { + verticalAlign: "middle", + "& > svg": { + fill: palette.additional["gray"][7], + height: constants.generalUnit * 2.25 + } } }) ) @@ -57,8 +71,17 @@ const ChangeProductModal = ({ onClose }: IChangeProductModal) => { const [slide, setSlide] = useState() const [plans, setPlans] = useState() const [isLoadingChangeSubscription, setIsLoadingChangeSubscription] = useState(false) - const [isSubscriptionError, setIsSubscriptionError] = useState(false) + const [subscriptionErrorMessage, setSubscriptionErrorMessage] = useState() const didSelectFreePlan = useMemo(() => !!selectedPlan && getPrice(selectedPlan, "month") === 0, [selectedPlan]) + const monthlyPrice = useMemo(() => selectedPlan?.prices.find((price) => price.recurring.interval === "month"), [selectedPlan]) + const yearlyPrice = useMemo(() => selectedPlan?.prices.find((price) => price.recurring.interval === "year"), [selectedPlan]) + const [billingPeriod, setBillingPeriod] = useState() + + useEffect(() => { + if(selectedPlan && !billingPeriod){ + setBillingPeriod(monthlyPrice ? "month" : "year") + } + }, [billingPeriod, monthlyPrice, selectedPlan]) useEffect(() => { if(!slide){ @@ -77,56 +100,63 @@ const ChangeProductModal = ({ onClose }: IChangeProductModal) => { } }, [getAvailablePlans, plans]) - const handleChangeSubscription = () => { + const handleChangeSubscription = useCallback(() => { if (selectedPrice) { setIsLoadingChangeSubscription(true) + setSubscriptionErrorMessage(undefined) changeSubscription(selectedPrice.id) .then(() => { setSlide("planSuccess") }) - .catch(() => { - setIsSubscriptionError(true) + .catch((e) => { + const errorMessage = formatSubscriptionError(e) + setSubscriptionErrorMessage(errorMessage) }) .finally(() => setIsLoadingChangeSubscription(false)) } - } + }, [changeSubscription, selectedPrice]) + + const onSelectPlanPrice = useCallback(() => { + if(billingPeriod === "month" && monthlyPrice) { + setSelectedPrice(monthlyPrice) + } else if (yearlyPrice) { + setSelectedPrice(yearlyPrice) + } + setSlide("paymentMethod") + }, [billingPeriod, monthlyPrice, yearlyPrice]) + + const onSelectPlan = useCallback((plan: Product) => { + setSelectedPlan(plan) + const currentPrice = currentSubscription?.product?.price?.unit_amount + const currentRecurrence = currentSubscription?.product.price.recurring.interval + const newPrice = getPrice(plan, currentRecurrence) + const isDowngrade = (currentPrice || 0) > newPrice + + isDowngrade + ? setSlide("downgradeDetails") + : setSlide("planDetails") + }, [currentSubscription]) return ( {slide === "select" && ( { - setSelectedPlan(plan) - const currentPrice = currentSubscription?.product?.price?.unit_amount - const currentRecurrence = currentSubscription?.product.price.recurring.interval - const newPrice = getPrice(plan, currentRecurrence) - const isDowngrade = (currentPrice || 0) > newPrice - - isDowngrade - ? setSlide("downgradeDetails") - : setSlide("planDetails") - }} + onSelectPlan={onSelectPlan} plans={plans} /> )} - { slide === "downgradeDetails" && selectedPlan && ( + {slide === "downgradeDetails" && selectedPlan && ( {setSlide("select")}} goToPlanDetails={() => setSlide("planDetails")} @@ -135,14 +165,18 @@ const ChangeProductModal = ({ onClose }: IChangeProductModal) => { onClose={onClose} /> )} - {slide === "planDetails" && selectedPlan && ( + {slide === "planDetails" && selectedPlan && billingPeriod && ( setSlide("select")} - onSelectPlanPrice={(planPrice: ProductPrice) => { - setSelectedPrice(planPrice) - setSlide("paymentMethod") + goToSelectPlan={() => { + setBillingPeriod(undefined) + setSlide("select") }} + onSelectPlanPrice={onSelectPlanPrice} + onChangeBillingPeriod={setBillingPeriod} + billingPeriod={billingPeriod} + monthlyPrice={monthlyPrice} + yearlyPrice={yearlyPrice} /> )} {slide === "paymentMethod" && selectedPrice && @@ -160,21 +194,30 @@ const ChangeProductModal = ({ onClose }: IChangeProductModal) => { setSlide("select")} - goToPaymentMethod={() => setSlide("paymentMethod")} + goToSelectPlan={() => { + setSubscriptionErrorMessage(undefined) + setSlide("select")} + } + goToPaymentMethod={() => { + setSubscriptionErrorMessage(undefined) + setSlide("paymentMethod") + }} loadingChangeSubscription={isLoadingChangeSubscription} onChangeSubscription={selectedPaymentMethod === "creditCard" ? handleChangeSubscription : () => setSlide("cryptoPayment")} - isSubscriptionError={isSubscriptionError} + subscriptionErrorMessage={subscriptionErrorMessage} paymentMethod={selectedPaymentMethod} - /> - } - {slide === "cryptoPayment" && } - {slide === "planSuccess" && selectedPlan && selectedPrice && } + {slide === "cryptoPayment" && setSlide("planSuccess")} + onClose={onClose} + />} + {slide === "planSuccess" && selectedPlan && selectedPrice && selectedPaymentMethod && - } + paymentMethod={selectedPaymentMethod} + />} ) } diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx index 3f97cdbe6a..01d4a1a461 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/DowngradeDetails.tsx @@ -2,14 +2,14 @@ import React, { useCallback, useState } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" import { Product } from "@chainsafe/files-api-client" -import { Button, CrossIcon, formatBytes, Typography } from "@chainsafe/common-components" -import { Trans } from "@lingui/macro" +import { Button, CrossIcon, formatBytes, InfoCircleIcon, Typography } from "@chainsafe/common-components" +import { Trans } from "@lingui/macro" import clsx from "clsx" import { useBilling } from "../../../../../Contexts/BillingContext" const useStyles = makeStyles(({ constants, palette }: CSFTheme) => createStyles({ - root: { + root: { position: "relative", margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` }, @@ -75,7 +75,7 @@ const useStyles = makeStyles(({ constants, palette }: CSFTheme) => textLink: { color: palette.primary.background }, - checkCircleIcon: { + checkCircleIcon: { fill: palette.additional["gray"][7], marginLeft: constants.generalUnit }, @@ -87,6 +87,18 @@ const useStyles = makeStyles(({ constants, palette }: CSFTheme) => invoiceText: { marginTop: constants.generalUnit * 3, marginBottom: constants.generalUnit + }, + warningText: { + marginTop: constants.generalUnit * 3, + maxWidth: constants.generalUnit * 56, + color: palette.additional["gray"][7] + }, + icon: { + verticalAlign: "middle", + "& > svg": { + fill: palette.additional["gray"][7], + height: constants.generalUnit * 2.25 + } } }) ) @@ -99,11 +111,18 @@ interface IConfirmDowngrade { shouldCancelPlan: boolean } -const DowngradeDetails = ({ plan, goBack, goToPlanDetails, shouldCancelPlan, onClose }: IConfirmDowngrade) => { +const DowngradeDetails = ({ + plan, + goBack, + goToPlanDetails, + shouldCancelPlan, + onClose +}: IConfirmDowngrade) => { const classes = useStyles() - const { currentSubscription, cancelCurrentSubscription } = useBilling() + const { currentSubscription, cancelCurrentSubscription, invoices } = useBilling() const currentStorage = formatBytes(Number(currentSubscription?.product?.price.metadata?.storage_size_bytes), 2) const [isCancelingPlan, setIsCancellingPlan] = useState(false) + const lastInvoicePaymentMethod = invoices && invoices[invoices.length - 1].payment_method const onCancelPlan = useCallback(() => { setIsCancellingPlan(true) @@ -124,6 +143,7 @@ const DowngradeDetails = ({ plan, goBack, goToPlanDetails, shouldCancelPlan, onC Change plan @@ -133,8 +153,9 @@ const DowngradeDetails = ({ plan, goBack, goToPlanDetails, shouldCancelPlan, onC variant="body1" component="p" className={classes.featuresTitle} + data-cy="label-lost-features-summary" > - You would lose the following features: + By switching plan, you will loose access to:
@@ -142,6 +163,7 @@ const DowngradeDetails = ({ plan, goBack, goToPlanDetails, shouldCancelPlan, onC {currentStorage ? {currentStorage} of storage @@ -149,16 +171,25 @@ const DowngradeDetails = ({ plan, goBack, goToPlanDetails, shouldCancelPlan, onC }
-
- - - {currentSubscription?.product.description} - -
+ + + {lastInvoicePaymentMethod === "crypto" + ? + All crypto payments are final and ineligible for credits, + exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed. + + : + Payments are final and non-refundable. If you wish to change your plan, + any extra funds will be applied as credit towards future payments. + + } +
@@ -166,6 +197,7 @@ const DowngradeDetails = ({ plan, goBack, goToPlanDetails, shouldCancelPlan, onC onClick={goBack} variant="text" disabled={isCancelingPlan} + testId="go-back-to-plan-selection" > Go back @@ -176,12 +208,14 @@ const DowngradeDetails = ({ plan, goBack, goToPlanDetails, shouldCancelPlan, onC onClick={onCancelPlan} loading={isCancelingPlan} disabled={isCancelingPlan} + testId="switch-to-free-plan" > Switch to Free plan : diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx index 77a46ee123..b53e76df7b 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx @@ -86,7 +86,7 @@ interface IPaymentMethodProps { const PaymentMethodSelector = ({ selectedProductPrice, goBack, onSelectPaymentMethod }: IPaymentMethodProps) => { const classes = useStyles() const [paymentMethod, setPaymentMethod] = useState<"creditCard" | "crypto" | undefined>() - const [view, setView] = useState<"selectPaymentMethod" | "addCard">("selectPaymentMethod") + const [isCardFormOpen, setIsCardFormOpen] = useState(false) const { defaultCard } = useBilling() useEffect(() => { @@ -101,6 +101,7 @@ const PaymentMethodSelector = ({ selectedProductPrice, goBack, onSelectPaymentMe variant="h5" component="h4" className={classes.heading} + data-cy="header-select-payment" > Select payment method @@ -109,10 +110,10 @@ const PaymentMethodSelector = ({ selectedProductPrice, goBack, onSelectPaymentMe component="p" className={classes.subHeading} > - {view === "addCard" && This card will become your default payment method} + {isCardFormOpen && This card will become your default payment method} - {view === "selectPaymentMethod" && <> + {!isCardFormOpen && <>
setView("addCard")} + onClick={() => setIsCardFormOpen(true)} + data-cy={defaultCard ? "text-button-update-card" : "text-button-add-card"} > {defaultCard ? Update credit card @@ -142,36 +145,38 @@ const PaymentMethodSelector = ({ selectedProductPrice, goBack, onSelectPaymentMe checked={paymentMethod === "crypto"} labelClassName={classes.radioLabel} disabled={selectedProductPrice.recurring.interval !== "year"} + testId="crypto" /> } - {view === "addCard" &&
+ {isCardFormOpen &&
setView("selectPaymentMethod")} - goBack={() => setView("selectPaymentMethod")} + onCardAdd={() => setIsCardFormOpen(false)} + goBack={() => setIsCardFormOpen(false)} />
} - -
+ {!isCardFormOpen &&
-
+
} ) } diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PlanDetails.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PlanDetails.tsx index b9a4f42708..76b41b9db6 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PlanDetails.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PlanDetails.tsx @@ -1,7 +1,7 @@ -import React, { useState } from "react" +import React, { useMemo } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" -import { Product, ProductPrice } from "@chainsafe/files-api-client" +import { Product, ProductPrice, ProductPriceRecurringInterval } from "@chainsafe/files-api-client" import { Button, Divider, formatBytes, ToggleSwitch, Typography } from "@chainsafe/common-components" import { t, Trans } from "@lingui/macro" import dayjs from "dayjs" @@ -67,28 +67,27 @@ const useStyles = makeStyles(({ constants }: CSFTheme) => interface IPlanDetails { plan: Product goToSelectPlan: () => void - onSelectPlanPrice: (planPrice: ProductPrice) => void + onSelectPlanPrice: () => void + billingPeriod: ProductPriceRecurringInterval + onChangeBillingPeriod: (paymentPeriod: ProductPriceRecurringInterval) => void + monthlyPrice?: ProductPrice + yearlyPrice?: ProductPrice } -const PlanDetails = ({ plan, goToSelectPlan, onSelectPlanPrice }: IPlanDetails) => { +const PlanDetails = ({ + plan, + goToSelectPlan, + onSelectPlanPrice, + billingPeriod, + onChangeBillingPeriod, + monthlyPrice, + yearlyPrice +}: IPlanDetails) => { const classes = useStyles() - const monthlyPrice = plan.prices.find((price) => price.recurring.interval === "month") - const yearlyPrice = plan.prices.find((price) => price.recurring.interval === "year") - const currentPlanStorage = formatBytes(Number(monthlyPrice?.metadata?.storage_size_bytes), 2) - - const [billingPeriod, setBillingPeriod] = useState<"monthly" | "yearly">(monthlyPrice ? "monthly" : "yearly") - - const handleSelectPlan = () => { - if(billingPeriod === "monthly" && monthlyPrice) { - onSelectPlanPrice(monthlyPrice) - } else if (yearlyPrice) { - onSelectPlanPrice(yearlyPrice) - } - } - - const percentageOff = monthlyPrice && yearlyPrice - ? ((((monthlyPrice.unit_amount * 12) - yearlyPrice.unit_amount) * 100) / (monthlyPrice.unit_amount * 12)) - : null + const currentPlanStorage = useMemo(() => formatBytes(Number(monthlyPrice?.metadata?.storage_size_bytes), 2), [monthlyPrice]) + const percentageOff = useMemo(() => monthlyPrice && yearlyPrice && + (((monthlyPrice.unit_amount * 12) - yearlyPrice.unit_amount) * 100) / (monthlyPrice.unit_amount * 12) + , [monthlyPrice, yearlyPrice]) return (
@@ -165,10 +164,11 @@ const PlanDetails = ({ plan, goToSelectPlan, onSelectPlanPrice }: IPlanDetails)
setBillingPeriod(billingPeriod === "monthly" ? "yearly" : "monthly")} + onChange={onChangeBillingPeriod} + value={billingPeriod} />
@@ -190,11 +190,11 @@ const PlanDetails = ({ plan, goToSelectPlan, onSelectPlanPrice }: IPlanDetails) className={classes.boldText} data-cy="label-total-cost" > - {billingPeriod === "monthly" + {billingPeriod === "month" ? `${monthlyPrice?.unit_amount ? monthlyPrice?.currency : ""} ${monthlyPrice?.unit_amount}` : `${yearlyPrice?.unit_amount ? yearlyPrice?.currency : ""} ${yearlyPrice?.unit_amount}` } - {billingPeriod === "monthly" ? t`/month` : t`/year`} + {billingPeriod === "month" ? t`/month` : t`/year`}
@@ -209,7 +209,7 @@ const PlanDetails = ({ plan, goToSelectPlan, onSelectPlanPrice }: IPlanDetails)
{cardAddError && } - -
- {goBack && - - Go back - - } -
+
+ {goBack && + + } {onClose && {submitText} - +
) diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/Common/ConfirmPlan.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/Common/ConfirmPlan.tsx index 493cfc65a3..ea1f405973 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/Common/ConfirmPlan.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/Common/ConfirmPlan.tsx @@ -1,12 +1,13 @@ -import React, { useMemo } from "react" +import React, { useState, useEffect, useMemo } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" -import { Product, ProductPrice } from "@chainsafe/files-api-client" -import { Button, CreditCardIcon, Divider, formatBytes, Typography } from "@chainsafe/common-components" +import { CheckSubscriptionUpdate, Product, ProductPrice } from "@chainsafe/files-api-client" +import { Button, InfoCircleIcon, CreditCardIcon, Divider, formatBytes, Typography } from "@chainsafe/common-components" import { t, Trans } from "@lingui/macro" import dayjs from "dayjs" import { PaymentMethod, useBilling } from "../../../../../Contexts/BillingContext" import clsx from "clsx" +import { useFilesApi } from "../../../../../Contexts/FilesApiContext" const useStyles = makeStyles(({ constants, palette }: CSFTheme) => createStyles({ @@ -22,7 +23,7 @@ const useStyles = makeStyles(({ constants, palette }: CSFTheme) => marginBottom: constants.generalUnit * 3 }, boldText: { - fontWeight: "bold" + fontWeight: 600 }, normalWeightText: { fontWeight: "normal" @@ -86,6 +87,18 @@ const useStyles = makeStyles(({ constants, palette }: CSFTheme) => error: { marginTop: constants.generalUnit, color: palette.error.main + }, + warningText: { + marginTop: constants.generalUnit * 2, + maxWidth: constants.generalUnit * 56, + color: palette.additional["gray"][7] + }, + icon : { + verticalAlign: "middle", + fill: palette.additional["gray"][7], + "& > svg": { + height: constants.generalUnit * 2.25 + } } }) ) @@ -93,12 +106,12 @@ const useStyles = makeStyles(({ constants, palette }: CSFTheme) => interface IConfirmPlan { plan: Product planPrice: ProductPrice - goToSelectPlan: () => void - goToPaymentMethod: () => void + goToSelectPlan?: () => void + goToPaymentMethod?: () => void onChangeSubscription: () => void loadingChangeSubscription: boolean - isSubscriptionError: boolean paymentMethod: PaymentMethod + subscriptionErrorMessage?: string } const ConfirmPlan = ({ @@ -108,18 +121,31 @@ const ConfirmPlan = ({ goToPaymentMethod, onChangeSubscription, loadingChangeSubscription, - isSubscriptionError, - paymentMethod + paymentMethod, + subscriptionErrorMessage }: IConfirmPlan) => { const classes = useStyles() const { defaultCard } = useBilling() const { currentSubscription } = useBilling() + const { filesApiClient } = useFilesApi() + const [checkSubscriptionUpdate, setCheckSubscriptionUpdate] = useState() const newPlanStorage = formatBytes(Number(planPrice?.metadata?.storage_size_bytes), 2) + const isDowngrade = useMemo(() => { const currentPrice = currentSubscription?.product?.price?.unit_amount return currentPrice && currentPrice > planPrice.unit_amount - }, [currentSubscription, planPrice] - ) + }, [currentSubscription, planPrice]) + + useEffect(() => { + if (!currentSubscription) return + + filesApiClient.checkSubscriptionUpdate(currentSubscription?.id, { + payment_method: paymentMethod === "creditCard" ? "stripe" : "crypto", + price_id: planPrice.id + }) + .then(setCheckSubscriptionUpdate) + .catch(console.error) + }, [currentSubscription, paymentMethod, filesApiClient, planPrice]) return (
@@ -127,6 +153,7 @@ const ConfirmPlan = ({ variant="h5" component="h4" className={classes.heading} + data-cy="header-confirm-change" > { isDowngrade @@ -137,9 +164,10 @@ const ConfirmPlan = ({
+ variant="h5" + component="h5" + className={classes.boldText} + data-cy="label-selected-plan"> {plan.name}
@@ -148,6 +176,8 @@ const ConfirmPlan = ({ component="p" className={classes.textButton} onClick={goToSelectPlan} + disabled={!goToSelectPlan} + data-cy="link-edit-plan" > Edit plan @@ -157,6 +187,7 @@ const ConfirmPlan = ({ Features @@ -165,6 +196,7 @@ const ConfirmPlan = ({ component="p" variant="body1" className={classes.featureSeparator} + data-cy="label-features-summary" > {newPlanStorage} of storage @@ -175,8 +207,9 @@ const ConfirmPlan = ({ <>
Payment method @@ -186,6 +219,8 @@ const ConfirmPlan = ({ component="p" className={classes.textButton} onClick={goToPaymentMethod} + disabled={!goToPaymentMethod} + data-cy="link-edit-payment-method" > Edit payment method @@ -193,7 +228,9 @@ const ConfirmPlan = ({
- + •••• •••• •••• {defaultCard.last_four_digit}
@@ -205,6 +242,7 @@ const ConfirmPlan = ({ Pay with Crypto @@ -226,6 +264,7 @@ const ConfirmPlan = ({ Accepted currencies @@ -233,8 +272,9 @@ const ConfirmPlan = ({ - DAI, USDC, ETH or BTC + DAI, USDC, ETH or BTC
@@ -244,6 +284,7 @@ const ConfirmPlan = ({ Billing start time @@ -251,6 +292,7 @@ const ConfirmPlan = ({ {dayjs().format("DD MMM YYYY")} @@ -262,14 +304,25 @@ const ConfirmPlan = ({ component="h5" variant="h5" className={classes.boldText} + data-cy="label-total-title" > - Total + Pricing details + +
+
+ + Plan price
{planPrice.unit_amount ? planPrice.currency : ""} {planPrice.unit_amount} @@ -278,13 +331,114 @@ const ConfirmPlan = ({
- {isSubscriptionError && + {!!checkSubscriptionUpdate?.amount_from_credit && +
+ + Amount from credit + +
+ + {planPrice.currency}  + {checkSubscriptionUpdate.amount_from_credit.toFixed(2)} + +
+
+ } + {!!checkSubscriptionUpdate?.amount_unused_from_last_bill && +
+ + Amount available from last bill + +
+ + {planPrice.currency}  + {checkSubscriptionUpdate.amount_unused_from_last_bill.toFixed(2)} + +
+
+ } + + {!!checkSubscriptionUpdate &&
+ + Amount due + +
+ + {planPrice.currency} {checkSubscriptionUpdate?.amount_due.toFixed(2)} + +
+
+ } + {!!checkSubscriptionUpdate?.amount_credited &&
+ + Amount added to credit + +
+ + {planPrice.currency}  + {checkSubscriptionUpdate.amount_credited.toFixed(2)} + +
+
+ } +
+ + + {paymentMethod === "crypto" + ? + Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received + , your plan will not change. + + : + Payments are final and non-refundable. If you wish to change your plan, + any extra funds will be applied as credit towards future payments. + + } + +
+ {subscriptionErrorMessage && - Failed to change subscription + {subscriptionErrorMessage} }
@@ -293,14 +447,16 @@ const ConfirmPlan = ({ onClick={goToPaymentMethod} variant="text" disabled={loadingChangeSubscription} + testId="go-back-to-payment-method" > Go back } {selectedCurrency && selectedCurrency !== "bitcoin" && !isReady && - + } {selectedCurrency && selectedCurrency !== "bitcoin" && isReady && network !== 1 && - } @@ -488,7 +573,7 @@ const CryptoPayment = ({ planPrice }: ICryptoPayment) => { }
- + } ) } diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/Common/PlanSuccess.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/Common/PlanSuccess.tsx new file mode 100644 index 0000000000..c9de4469b1 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/Common/PlanSuccess.tsx @@ -0,0 +1,184 @@ +import React from "react" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { CSFTheme } from "../../../../../Themes/types" +import { Product, ProductPrice } from "@chainsafe/files-api-client" +import { Button, CheckCircleIcon, CheckIcon, Divider, formatBytes, Link, Typography } from "@chainsafe/common-components" +import { Trans } from "@lingui/macro" +import { ROUTE_LINKS } from "../../../../FilesRoutes" +import clsx from "clsx" +import { PaymentMethod } from "../../../../../Contexts/BillingContext" + +const useStyles = makeStyles(({ constants, palette }: CSFTheme) => + createStyles({ + root: { + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` + }, + headingBadge: { + color: palette.additional["gray"][7], + marginTop: constants.generalUnit * 3 + }, + headingBox: { + marginTop: constants.generalUnit * 2, + marginBottom: constants.generalUnit * 2 + }, + rowBox: { + display: "flex", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + middleRowBox: { + display: "flex", + alignItems: "center" + }, + featuresTitle: { + marginTop: constants.generalUnit * 2, + marginBottom: constants.generalUnit * 2 + }, + pushRightBox: { + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + marginLeft: constants.generalUnit * 4 + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + } + }, + bottomSection: { + display: "flex", + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + margin: `${constants.generalUnit * 3}px 0px` + }, + divider: { + margin: `${constants.generalUnit}px 0` + }, + featuresBox: { + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit * 2 + }, + featureTickBox: { + marginBottom: constants.generalUnit + }, + textLink: { + color: palette.primary.background + }, + checkCircleIcon: { + fill: palette.additional["gray"][7], + marginLeft: constants.generalUnit + }, + tickIcon: { + fill: palette.success.main + }, + invoiceText: { + marginTop: constants.generalUnit * 3, + marginBottom: constants.generalUnit + } + }) +) + +interface IPlanSuccess { + plan: Product + planPrice: ProductPrice + paymentMethod: PaymentMethod + onClose: () => void +} + +const PlanSuccess = ({ plan, onClose, planPrice, paymentMethod }: IPlanSuccess) => { + const classes = useStyles() + const newPlanCapacity = formatBytes(Number(planPrice?.metadata?.storage_size_bytes), 2) + + return ( +
+ + Confirmation + +
+ + Plan changed successfully + + +
+ +
+ + You now have: + +
+
+ + + {planPrice?.metadata?.storage_size_bytes + ? {newPlanCapacity} of storage + : plan.description + } + +
+
+
+
+ + Access your billing history in settings or view your   + + invoices here + + +
+ {paymentMethod === "crypto" && +
+ + + Crypto payments may take a few minutes to be processed. + The subscription update will reflect on next login. + + +
+ } +
+
+ +
+
+
+ ) +} + +export default PlanSuccess \ No newline at end of file diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/CurrentCard.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/CurrentCard.tsx index 81ddaf4049..012c54427d 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/CurrentCard.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/CurrentCard.tsx @@ -1,22 +1,31 @@ import React, { useState } from "react" import { Typography, CreditCardIcon, Button, Dialog } from "@chainsafe/common-components" -import { makeStyles, ITheme, createStyles } from "@chainsafe/common-theme" +import { makeStyles, createStyles } from "@chainsafe/common-theme" import { t, Trans } from "@lingui/macro" import { useBilling } from "../../../../Contexts/BillingContext" import AddCardModal from "./AddCard/AddCardModal" +import clsx from "clsx" +import dayjs from "dayjs" +import { CSFTheme } from "../../../../Themes/types" -const useStyles = makeStyles(({ constants, palette }: ITheme) => +const useStyles = makeStyles(({ constants, palette }: CSFTheme) => createStyles({ container: { - padding: constants.generalUnit, + padding: `${constants.generalUnit}px 0`, margin: `${constants.generalUnit * 1.5}px 0` }, - noCard: { + spaceBetweenBox: { + display: "flex", + justifyContent: "space-between" + }, + heading: { + flex: 1 + }, + cardLineMargins: { margin: `${constants.generalUnit * 2}px 0` }, cardDetailsContainer: { - display: "flex", - margin: `${constants.generalUnit * 2}px 0` + display: "flex" }, creditCardIcon: { marginRight: constants.generalUnit, @@ -25,10 +34,19 @@ const useStyles = makeStyles(({ constants, palette }: ITheme) => linkButton: { textDecoration: "underline", cursor: "pointer", - margin: `0 ${constants.generalUnit * 2}px` + margin: `0 ${constants.generalUnit * 2}px`, + color: palette.additional["gray"][7] }, confirmDeletionDialog: { top: "50%" + }, + link: { + color: constants.settingsPage.linkButton.color, + paddingRight: "0px !important", + fontSize: 16 + }, + text: { + fontSize: 16 } }) ) @@ -36,7 +54,7 @@ const useStyles = makeStyles(({ constants, palette }: ITheme) => const CurrentCard: React.FC = () => { const classes = useStyles() const [isAddCardModalOpen, setIsAddCardModalOpen ] = useState(false) - const { defaultCard, deleteCard, refreshDefaultCard } = useBilling() + const { defaultCard, deleteCard, refreshDefaultCard, currentSubscription } = useBilling() const [isDeleteCardModalOpen, setIsDeleteCardModalOpen] = useState(false) const [isDeleteCardLoading, setIsDeleteCardLoading] = useState(false) @@ -55,54 +73,85 @@ const CurrentCard: React.FC = () => { return ( <>
-
+
- Credit card saved + Payment information +
+ {!!currentSubscription?.expiry_date &&
+ + Next payment + + + {dayjs.unix(currentSubscription.expiry_date).format("MMMM DD, YYYY")} + +
+ } {defaultCard - ?
- - - •••• •••• •••• {defaultCard.last_four_digit} - + ?
+
+ + + •••• •••• •••• {defaultCard.last_four_digit} + + setIsDeleteCardModalOpen(true)} + data-cy="link-remove-card" + > + Remove + +
setIsDeleteCardModalOpen(true)} - data-cy="link-remove-card" + className={classes.text} > - Remove + expires {defaultCard.exp_month}/{defaultCard.exp_year.toString().substring(2)}
: No Card } -
+const useStyles = makeStyles(({ breakpoints, constants }: CSFTheme) => createStyles({ root: { - padding: constants.generalUnit, + padding: `${constants.generalUnit}px 0`, "& h2, & h5": { marginBottom: constants.generalUnit, fontWeight: 400 @@ -24,17 +25,26 @@ const useStyles = makeStyles(({ breakpoints, constants }: ITheme) => "& h5": { } }, + heading: { + flex: 1 + }, + headline: { + display: "flex", + justifyContent: "space-between" + }, + alignRight: { + display: "flex", + justifyContent: "flex-end" + }, spaceUsedBox: { - maxWidth: 240, [breakpoints.down("md")]: { marginBottom: constants.generalUnit, width: "inherit" } }, usageBar: { - maxWidth: "70%", + marginTop: constants.generalUnit * 1.5, marginBottom: constants.generalUnit, - marginTop: constants.generalUnit, overflow: "hidden" }, buttons: { @@ -47,9 +57,13 @@ const useStyles = makeStyles(({ breakpoints, constants }: ITheme) => } }, link: { - display: "block", - width: "100%", - textDecoration: "none" + color: constants.settingsPage.linkButton.color, + paddingRight: "0px !important", + fontSize: 16 + + }, + text: { + fontSize: 16 } }) ) @@ -65,54 +79,52 @@ const CurrentPlan = ({ className }: ICurrentProduct) => { const [isChangeProductModalVisible, setChangeProductModalVisible] = useState(false) return (
- - Your plan - - { - currentSubscription - ? + {currentSubscription?.product.name}{isPendingInvoice && ` ${t`(Awaiting payment)`}`} - : + +
+ : } {storageSummary && <>
- - {t`${formatBytes(storageSummary.used_storage, 2)} of ${formatBytes( - storageSummary.total_storage, 2 - )} used`} ({((storageSummary.used_storage / storageSummary.total_storage) * 100).toFixed(1)}%) - -
-
- +
+ + {t`${formatBytes(storageSummary.used_storage, 2)} of ${formatBytes( + storageSummary.total_storage, 2 + )} used`} ({((storageSummary.used_storage / storageSummary.total_storage) * 100).toFixed(1)}%) + +
{ isChangeProductModalVisible && ( { } } - ) } diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/PayInvoice/PayInvoiceModal.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/PayInvoice/PayInvoiceModal.tsx index fc8e4f42d3..c19dc6d992 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/PayInvoice/PayInvoiceModal.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/PayInvoice/PayInvoiceModal.tsx @@ -1,8 +1,13 @@ -import React from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" import { CSFTheme } from "../../../../../Themes/types" import { Modal } from "@chainsafe/common-components" import CryptoPayment from "../Common/CryptoPayment" +import { useBilling } from "../../../../../Contexts/BillingContext" +import { useFilesApi } from "../../../../../Contexts/FilesApiContext" +import ConfirmPlan from "../Common/ConfirmPlan" +import { formatSubscriptionError } from "../utils/formatSubscriptionError" +import PlanSuccess from "../Common/PlanSuccess" const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) => createStyles({ @@ -23,28 +28,84 @@ const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) => }) ) +type PayInvoiceModalSlides = "confirmPlan" | "planSuccess" | "cryptoPayment" + interface IChangeProductModal { + invoiceId: string onClose: () => void } -const PayInvoiceModal = ({ onClose }: IChangeProductModal) => { +const PayInvoiceModal = ({ onClose, invoiceId }: IChangeProductModal) => { const classes = useStyles() const { desktop } = useThemeSwitcher() + const { invoices, refreshInvoices } = useBilling() + const { filesApiClient } = useFilesApi() + const invoiceToPay = useMemo(() => invoices?.find(i => i.uuid === invoiceId), [invoices, invoiceId]) + const [payingInvoice, setPayingInvoice] = useState(false) + const [errorMessage, setErrorMessage] = useState() + const [slide, setSlide] = useState() + + const payInvoice = useCallback(() => { + if (!invoiceToPay) return + + try { + setPayingInvoice(true) + setErrorMessage(undefined) + filesApiClient.payInvoice(invoiceToPay.uuid) + .then(() => { + setSlide("planSuccess") + refreshInvoices() + }) + } catch (error: any) { + const errorMessage = formatSubscriptionError(error) + setErrorMessage(errorMessage) + } finally { + setPayingInvoice(false) + } + }, [filesApiClient, invoiceToPay, refreshInvoices]) + + useEffect(() => { + if (!slide) { + setSlide(invoiceToPay?.payment_method === "crypto" + ? "cryptoPayment" + : "confirmPlan" + ) + } + }, [invoiceToPay, slide]) + + if (!invoiceToPay) return null return ( - + {slide === "cryptoPayment" && setSlide("planSuccess")} + />} + {slide === "confirmPlan" && } + {slide === "planSuccess" && + } ) } diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/index.tsx b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/index.tsx index abca0c4601..53e269c624 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/index.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/index.tsx @@ -12,9 +12,20 @@ import { useBilling } from "../../../../Contexts/BillingContext" const useStyles = makeStyles(({ breakpoints, constants }: ITheme) => createStyles({ root: { - [breakpoints.down("sm")]: { - paddingLeft: constants.generalUnit, - paddingRight: constants.generalUnit + [breakpoints.down("md")]: { + padding: constants.generalUnit * 1.5 + } + }, + mainHeader: { + fontSize: 28, + marginBottom: constants.generalUnit * 2, + [breakpoints.up("md")]: { + paddingLeft: constants.generalUnit * 2 + } }, + settingsSection: { + maxWidth: breakpoints.values["md"], + [breakpoints.up("md")]: { + padding: `0 ${constants.generalUnit * 2}px` } } }) @@ -22,7 +33,7 @@ const useStyles = makeStyles(({ breakpoints, constants }: ITheme) => const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PK || "") -const PlanView: React.FC = () => { +const Subscription: React.FC = () => { const classes = useStyles() const { refreshDefaultCard, fetchCurrentSubscription } = useBilling() @@ -39,16 +50,25 @@ const PlanView: React.FC = () => { - Payment and Subscriptions + Subscription plan - - - +
+ +
+ +
+ +
+ +
+ +
) } -export default PlanView +export default Subscription diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/utils/formatSubscriptionError.ts b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/utils/formatSubscriptionError.ts new file mode 100644 index 0000000000..904921634d --- /dev/null +++ b/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/utils/formatSubscriptionError.ts @@ -0,0 +1,6 @@ +import { t } from "@lingui/macro" + +export const formatSubscriptionError = (e: any): string => + e.error.code === 400 && e.error.message.includes("declined") + ? t`The transaction was declined. Please use a different card or try again.` + : t`Failed to update the subscription. Please try again later.` \ No newline at end of file diff --git a/packages/files-ui/src/Components/Modules/Settings/index.tsx b/packages/files-ui/src/Components/Modules/Settings/index.tsx index 6f1551d70f..595152d8a4 100644 --- a/packages/files-ui/src/Components/Modules/Settings/index.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/index.tsx @@ -9,7 +9,9 @@ import { useHistory, ITabPaneProps, CaretRightIcon, - LockIcon + LockIcon, + UserIcon, + CaretLeftIcon } from "@chainsafe/common-components" import { makeStyles, ITheme, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" import { ROUTE_LINKS, SettingsPath } from "../../FilesRoutes" @@ -18,16 +20,17 @@ import SubscriptionTab from "./SubscriptionTab" import { ProfileIcon, SubscriptionPlanIcon } from "@chainsafe/common-components" import clsx from "clsx" import SecurityTab from "./SecurityTab" +import DisplayTab from "./DisplayTab" +import { useBilling } from "../../../Contexts/BillingContext" const TabPane = (props: ITabPaneProps) => TabPaneOrigin(props) const useStyles = makeStyles(({ constants, breakpoints, palette }: ITheme) => createStyles({ title: { marginTop: constants.generalUnit, - cursor: "pointer", [breakpoints.down("md")]: { - fontSize: 20, - lineHeight: "28px", + fontSize: 18, + lineHeight: "22px", margin: `${constants.generalUnit}px 0` } }, @@ -47,13 +50,23 @@ const useStyles = makeStyles(({ constants, breakpoints, palette }: ITheme) => marginTop: constants.generalUnit * 3 }, headerContainer: { + display: "flex", + alignItems: "center", marginBottom: constants.generalUnit * 4, + width: "fit-content", [breakpoints.down("md")]: { + cursor: "pointer", padding: `0 ${constants.generalUnit * 2}px`, marginTop: constants.generalUnit * 4, marginBottom: constants.generalUnit * 2 + }, + "& svg": { + fill: palette.additional["gray"][9] } }, + caretLeft: { + marginTop: "2px" + }, tabsContainer: { borderRadius: 10, backgroundColor: palette.additional["gray"][3], @@ -72,7 +85,10 @@ const useStyles = makeStyles(({ constants, breakpoints, palette }: ITheme) => }, lockIcon : { width: "1rem", - marginRight: "0.5rem" + marginRight: "0.5rem", + "& svg": { + fill: palette.additional["gray"][9] + } }, hideTabPane: { display: "none" @@ -112,7 +128,6 @@ const useStyles = makeStyles(({ constants, breakpoints, palette }: ITheme) => "&.selected": { borderBottom: "none", - fontWeight: "normal", backgroundColor: palette.additional["gray"][4], borderTopLeftRadius: 10, borderBottomLeftRadius: 10 @@ -131,6 +146,7 @@ const Settings: React.FC = () => { const { path = desktop ? "profile" : undefined } = useParams<{path: SettingsPath}>() const classes = useStyles() const { redirect, history } = useHistory() + const { isBillingEnabled } = useBilling() const onSelectTab = useCallback( (key: SettingsPath) => redirect(ROUTE_LINKS.SettingsPath(key)) @@ -138,12 +154,15 @@ const Settings: React.FC = () => { return (
-
+
!desktop && !!path && history.push(ROUTE_LINKS.SettingsDefault)} + > + {!desktop && !!path && } history.push(ROUTE_LINKS.SettingsDefault)} > Settings @@ -168,14 +187,24 @@ const Settings: React.FC = () => { > } + icon={} iconRight={} - title={t`Profile and Display`} + title={t`Profile`} tabKey="profile" testId="tab-profile" > + } + iconRight={} + > + + } @@ -186,16 +215,18 @@ const Settings: React.FC = () => { > - } - iconRight={} - > - - + {isBillingEnabled + ? } + iconRight={} + > + + + : null}
} diff --git a/packages/files-ui/src/Components/Pages/BillingHistory.tsx b/packages/files-ui/src/Components/Pages/BillingHistory.tsx index 16079ae2b5..a9cfab9fff 100644 --- a/packages/files-ui/src/Components/Pages/BillingHistory.tsx +++ b/packages/files-ui/src/Components/Pages/BillingHistory.tsx @@ -1,9 +1,10 @@ -import React from "react" +import React, { useState } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" import { CSFTheme } from "../../Themes/types" import { Typography } from "@chainsafe/common-components" import { Trans } from "@lingui/macro" import InvoiceLines from "../Elements/InvoiceLines" +import PayInvoiceModal from "../Modules/Settings/SubscriptionTab/PayInvoice/PayInvoiceModal" const useStyles = makeStyles( ({ constants, breakpoints }: CSFTheme) => @@ -30,6 +31,7 @@ const useStyles = makeStyles( const BillingHistory = () => { const classes = useStyles() + const [invoiceToPay, setInvoiceToPay] = useState() return (
@@ -40,7 +42,11 @@ const BillingHistory = () => { > Billing history - + setInvoiceToPay(invoiceId)} /> + {invoiceToPay && setInvoiceToPay(undefined)} + />}
) } diff --git a/packages/files-ui/src/Contexts/BillingContext.tsx b/packages/files-ui/src/Contexts/BillingContext.tsx index e46132329f..73dd97f7be 100644 --- a/packages/files-ui/src/Contexts/BillingContext.tsx +++ b/packages/files-ui/src/Contexts/BillingContext.tsx @@ -22,31 +22,41 @@ interface IBillingContext { refreshDefaultCard: () => void currentSubscription: CurrentSubscription | undefined changeSubscription: (newPriceId: string) => Promise - fetchCurrentSubscription: () => void + fetchCurrentSubscription: () => Promise getAvailablePlans: () => Promise deleteCard: (card: Card) => Promise updateDefaultCard: (id: StripePaymentMethod["id"]) => Promise invoices?: InvoiceResponse[] cancelCurrentSubscription: () => Promise isPendingInvoice: boolean + openInvoice?: InvoiceResponse downloadInvoice: (invoiceId: string) => Promise + refreshInvoices: () => void + isBillingEnabled: boolean } const ProductMapping: {[key: string]: { name: string - description: string }} = { + // Staging Product Ids prod_JwRu6Ph25b1f2O: { - name: t`Free plan`, - description: t`This is the free product.` + name: t`Files Free` }, prod_JwS49Qfnr6vD3K: { - name: t`Standard plan`, - description: t`Standard plan` + name: t`Files Pro` }, prod_JwSGHB8qFx7rRM: { - name: t`Premium plan`, - description: t`Premium plan` + name: t`Files Max` + }, + // Production Product Ids + prod_KRAq3CngQMKebw: { + name: t`Files Free` + }, + prod_LDXtKgrbAoZvIB: { + name: t`Files Pro` + }, + prod_LDXtBLuzjVxMzg: { + name: t`Files Max` } } @@ -63,24 +73,37 @@ const BillingProvider = ({ children }: BillingContextProps) => { const [defaultCard, setDefaultCard] = useState(undefined) const [invoices, setInvoices] = useState() const isPendingInvoice = useMemo(() => currentSubscription?.status === "pending_update", [currentSubscription]) + const openInvoice = useMemo(() => invoices?.find((i) => i.status === "open"), [invoices]) const [restrictedNotification, setRestrictedNotification] = useState() const [unpaidInvoiceNotification, setUnpaidInvoiceNotification] = useState() const [cardExpiringNotification, setCardExpiringNotification] = useState() + const [isBillingEnabled, setIsBillingEnabled] = useState(false) - useEffect(() => { + const refreshInvoices = useCallback(() => { if (!currentSubscription) return filesApiClient.getAllInvoices(currentSubscription.id, 100) .then(({ invoices }) => { - setInvoices(invoices - .filter(i => i.status !== "void") + setInvoices(invoices.filter(i => i.status !== "void") .sort((a, b) => b.period_start - a.period_start)) - }).catch((e: any) => { - console.error(e) + }).catch((err) => { + console.error(err) setInvoices([]) }) }, [currentSubscription, filesApiClient]) + useEffect(() => { + refreshInvoices() + }, [refreshInvoices]) + + useEffect(() => { + if (!isLoggedIn) return + + filesApiClient.getEligibility() + .then(res => setIsBillingEnabled(res.is_eligible)) + .catch(console.error) + }, [filesApiClient, isLoggedIn]) + useEffect(() => { if (accountRestricted && !restrictedNotification) { const notif = addNotification({ @@ -96,19 +119,18 @@ const BillingProvider = ({ children }: BillingContextProps) => { }, [accountRestricted, addNotification, redirect, removeNotification, restrictedNotification]) useEffect(() => { - const outstandingInvoice = invoices?.find(i => i.status === "open") - if (outstandingInvoice && !unpaidInvoiceNotification) { + if (!!openInvoice && !unpaidInvoiceNotification) { const notif = addNotification({ - createdAt: outstandingInvoice.period_start, + createdAt: openInvoice.period_start, title: t`Invoice outstanding`, onClick: () => redirect(ROUTE_LINKS.SettingsPath("plan")) }) setUnpaidInvoiceNotification(notif) - } else if (!outstandingInvoice && unpaidInvoiceNotification) { + } else if (!openInvoice && unpaidInvoiceNotification) { removeNotification(unpaidInvoiceNotification) setUnpaidInvoiceNotification(undefined) } - }, [addNotification, invoices, redirect, removeNotification, unpaidInvoiceNotification]) + }, [addNotification, openInvoice, redirect, removeNotification, unpaidInvoiceNotification]) useEffect(() => { if (defaultCard && currentSubscription) { @@ -149,15 +171,13 @@ const BillingProvider = ({ children }: BillingContextProps) => { }, [refreshDefaultCard, isLoggedIn, filesApiClient]) const fetchCurrentSubscription = useCallback(() => { - filesApiClient.getCurrentSubscription() + return filesApiClient.getCurrentSubscription() .then((subscription) => { subscription.product.name = ProductMapping[subscription.product.id].name - subscription.product.description = ProductMapping[subscription.product.id].description setCurrentSubscription(subscription) + return subscription }) - .catch((error: any) => { - console.error(error) - }) + .catch(console.error) }, [filesApiClient]) useEffect(() => { @@ -173,7 +193,6 @@ const BillingProvider = ({ children }: BillingContextProps) => { .then((products) => { return products.map(product => { product.name = ProductMapping[product.id].name - product.description = ProductMapping[product.id].description return product }) }) @@ -199,7 +218,7 @@ const BillingProvider = ({ children }: BillingContextProps) => { }) .catch((error) => { console.error(error) - return Promise.reject() + return Promise.reject(error) }) }, [filesApiClient, currentSubscription, fetchCurrentSubscription, refreshBuckets]) @@ -244,7 +263,10 @@ const BillingProvider = ({ children }: BillingContextProps) => { invoices, cancelCurrentSubscription, isPendingInvoice, - downloadInvoice + downloadInvoice, + refreshInvoices, + openInvoice, + isBillingEnabled }} > {children} diff --git a/packages/files-ui/src/Contexts/FilesContext.tsx b/packages/files-ui/src/Contexts/FilesContext.tsx index fce4ef4b02..cfc9f017e4 100644 --- a/packages/files-ui/src/Contexts/FilesContext.tsx +++ b/packages/files-ui/src/Contexts/FilesContext.tsx @@ -27,17 +27,6 @@ type FilesContextProps = { children: React.ReactNode | React.ReactNode[] } -export type SharedFolderUser = { - uuid: string - pubKey: string -} - -export type UpdateSharedFolderUser = { - uuid: string - pubKey?: string - encryption_key?: string -} - interface GetFileContentParams { cid: string cancelToken?: CancelToken @@ -71,13 +60,13 @@ type FilesContext = { isLoadingBuckets?: boolean createSharedFolder: ( name: string, - writers?: SharedFolderUser[], - readers?: SharedFolderUser[] + writers: LookupUser[], + readers: LookupUser[] ) => Promise editSharedFolder: ( bucket: BucketKeyPermission, - writers?: UpdateSharedFolderUser[], - readers?: UpdateSharedFolderUser[] + writers: LookupUser[], + readers: LookupUser[] ) => Promise transferFileBetweenBuckets: ( sourceBucket: BucketKeyPermission, @@ -688,7 +677,16 @@ const FilesProvider = ({ children }: FilesContextProps) => { } }, [getFileContent, addToast, updateToast]) - const createSharedFolder = useCallback(async (name: string, writerUsers?: SharedFolderUser[], readerUsers?: SharedFolderUser[]) => { + const getUsersWithEncryptionKey = useCallback(async (from: LookupUser[], bucketEncryptionKey: string) => { + return await Promise.all(from?.map(async ({ identity_pubkey, uuid }) => { + return ({ + uuid, + encryption_key: await encryptForPublicKey(identity_pubkey.slice(2), bucketEncryptionKey) || "" + }) + })) + }, [encryptForPublicKey]) + + const createSharedFolder = useCallback(async (name: string, writerUsers: LookupUser[], readerUsers: LookupUser[]) => { if (!publicKey) return const bucketEncryptionKey = Buffer.from( @@ -697,15 +695,8 @@ const FilesProvider = ({ children }: FilesContextProps) => { const ownerEncryptedEncryptionKey = await encryptForPublicKey(publicKey, bucketEncryptionKey) - const readers = readerUsers ? await Promise.all(readerUsers?.map(async u => ({ - uuid: u.uuid, - encryption_key: await encryptForPublicKey(u.pubKey, bucketEncryptionKey) - }))) : [] - - const writers = writerUsers ? await Promise.all(writerUsers?.map(async u => ({ - uuid: u.uuid, - encryption_key: await encryptForPublicKey(u.pubKey, bucketEncryptionKey) - }))) : [] + const readers = await getUsersWithEncryptionKey(readerUsers, bucketEncryptionKey) + const writers = await getUsersWithEncryptionKey(writerUsers, bucketEncryptionKey) return filesApiClient.createBucket({ name, @@ -723,24 +714,10 @@ const FilesProvider = ({ children }: FilesContextProps) => { } as BucketKeyPermission }) .catch(console.error) - }, [publicKey, encryptForPublicKey, filesApiClient, refreshBuckets, getKeyForBucket, getPermissionForBucket]) - - const getUsersWithEncryptionKey = useCallback(async (from: UpdateSharedFolderUser[], bucketEncryptionKey: string) => { - return await Promise.all(from?.map(async ({ pubKey, encryption_key, uuid }) => { - return !encryption_key && !!pubKey - ? { - uuid, - encryption_key: await encryptForPublicKey(pubKey, bucketEncryptionKey) || "" - } - : { - uuid, - encryption_key: encryption_key || "" - } - })) - }, [encryptForPublicKey]) + }, [encryptForPublicKey, filesApiClient, getKeyForBucket, getPermissionForBucket, getUsersWithEncryptionKey, publicKey, refreshBuckets]) const editSharedFolder = useCallback( - async (bucket: BucketKeyPermission, writerUsers?: UpdateSharedFolderUser[], readerUsers?: UpdateSharedFolderUser[]) => { + async (bucket: BucketKeyPermission, writerUsers: LookupUser[], readerUsers: LookupUser[]) => { if (!publicKey) return if (!readerUsers || !writerUsers) return diff --git a/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx b/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx index b9788d080c..6002aa0d29 100644 --- a/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx +++ b/packages/files-ui/src/Contexts/ThresholdKeyContext.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo, useCallback } from "react" -import DirectAuthSdk, { createHandler, ILoginHandler, LOGIN_TYPE, TorusLoginResponse } from "@toruslabs/torus-direct-web-sdk" +import DirectAuthSdk, { createHandler, ILoginHandler, LOGIN_TYPE, TorusLoginResponse } from "@toruslabs/customauth" import ThresholdKey from "@tkey/default" import WebStorageModule, { WEB_STORAGE_MODULE_NAME } from "@tkey/web-storage" import SecurityQuestionsModule, { SECURITY_QUESTIONS_MODULE_NAME } from "@tkey/security-questions" @@ -181,7 +181,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f setStatus("logging in") const tKeyJson = JSON.parse(tkeySerialized) const serviceProvider = new ServiceProviderBase({ enableLogging, postboxKey }) - const storageLayer = new TorusStorageLayer({ serviceProvider, enableLogging, hostUrl: "https://metadata.tor.us" }) + const storageLayer = new TorusStorageLayer({ enableLogging, hostUrl: "https://metadata.tor.us" }) tkey = await ThresholdKey.fromJSON(tKeyJson, { modules, serviceProvider, storageLayer }) if (tKeyJson.modules) { @@ -254,7 +254,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f // cached may be stale, resulting in a failure to reconstruct the key. This is // identified through the nonce. Manually refreshing the metadata cache solves this problem if (error.message.includes("nonce")) { - await TKeySdk.updateMetadata() + await TKeySdk._syncShareMetadata() const { privKey } = await TKeySdk.reconstructKey(false) const privKeyString = privKey.toString("hex") if (privKeyString.length < 64) { @@ -361,6 +361,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f shareEncPubKeyX = currentEncPubKeyX await shareTransferModule.startRequestStatusCheck(currentEncPubKeyX, true) const resultKey = await TKeySdk?.getKeyDetails() + await TKeySdk.syncLocalMetadataTransitions() setKeyDetails(resultKey) shareEncPubKeyX = undefined } catch (error) { @@ -442,6 +443,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f const loginResponse: TorusLoginResponse = { privateKey: torusKey.privateKey, publicAddress: torusKey.publicAddress, + typeOfUser: torusKey.typeOfUser, metadataNonce: "", userInfo: { idToken: identityToken.token, @@ -480,7 +482,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f console.log("privKey", privKey) } else { console.log("Existing key") - await TKeySdk.initialize({ input: metadata as ShareStore }) + await TKeySdk.initialize({ withShare: metadata as ShareStore }) try { console.log("Trying to load device share") const storageModule = TKeySdk.modules[WEB_STORAGE_MODULE_NAME] as WebStorageModule @@ -527,7 +529,6 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f if (!connected || !provider) throw new Error("Unable to connect to wallet.") } - const signer = provider.getSigner() if (!signer) throw new Error("Signer undefined") @@ -663,7 +664,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f try { const storageModule = TKeySdk.modules[WEB_STORAGE_MODULE_NAME] as WebStorageModule - await TKeySdk.updateMetadata() + await TKeySdk._syncShareMetadata() const newDeviceShare = await TKeySdk.generateNewShare() const newDeviceShareStore = newDeviceShare.newShareStores[newDeviceShare.newShareIndex.toString("hex")] @@ -674,6 +675,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f setKeyDetails(newKeyDetails) } catch (e) { console.error(e) + throw e } } @@ -683,7 +685,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f try { const shareTransferModule = TKeySdk.modules[SHARE_TRANSFER_MODULE_NAME] as ShareTransferModule await shareTransferModule.approveRequest(encPubKeyX) - await TKeySdk.syncShareMetadata() + await TKeySdk._syncShareMetadata() const newKeyDetails = await TKeySdk.getKeyDetails() setKeyDetails(newKeyDetails) } catch (e) { @@ -697,7 +699,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f try { const shareTransferModule = TKeySdk.modules[SHARE_TRANSFER_MODULE_NAME] as ShareTransferModule await shareTransferModule.deleteShareTransferStore(encPubKey) - await TKeySdk.syncShareMetadata() + await TKeySdk._syncShareMetadata() } catch (e) { console.error(e) } @@ -709,7 +711,7 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f try { const shareTransferModule = TKeySdk.modules[SHARE_TRANSFER_MODULE_NAME] as ShareTransferModule await shareTransferModule.resetShareTransferStore() - await TKeySdk.syncShareMetadata() + await TKeySdk._syncShareMetadata() } catch (e) { console.error(e) } @@ -805,14 +807,13 @@ const ThresholdKeyProvider = ({ children, network = "mainnet", enableLogging = f const refreshTKeyMeta = useCallback(async () => { if (!TKeySdk) return try { - await TKeySdk.syncShareMetadata() + await TKeySdk._syncShareMetadata() const newKeyDetails = await TKeySdk.getKeyDetails() setKeyDetails(newKeyDetails) return } catch (error: any) { if (error.message.includes("nonce")) { - await TKeySdk.updateMetadata() - await TKeySdk.syncShareMetadata() + await TKeySdk._syncShareMetadata() const newKeyDetails = await TKeySdk.getKeyDetails() setKeyDetails(newKeyDetails) } else { diff --git a/packages/files-ui/src/Themes/Constants.ts b/packages/files-ui/src/Themes/Constants.ts index 523d2df0d8..5ae9fd8019 100644 --- a/packages/files-ui/src/Themes/Constants.ts +++ b/packages/files-ui/src/Themes/Constants.ts @@ -74,7 +74,10 @@ export interface CsfColors extends IConstants { previewModal: { controlsBackground: string controlsColor: string - closeButtonColor: string + headerButtonColor: string + controlsButtonColor: string + controlsBackgroundColor: string + controlsBackgroundHoverColor: string fileOpsColor: string fileNameColor: string optionsBackground: string @@ -161,6 +164,9 @@ export interface CsfColors extends IConstants { border?: string borderColor?: string } + linkButton: { + color: string + } } surveyBanner: { color: string diff --git a/packages/files-ui/src/Themes/DarkTheme.ts b/packages/files-ui/src/Themes/DarkTheme.ts index 0930d4134c..17f07a86e2 100644 --- a/packages/files-ui/src/Themes/DarkTheme.ts +++ b/packages/files-ui/src/Themes/DarkTheme.ts @@ -165,11 +165,6 @@ export const darkTheme = createTheme({ main: "var(--gray10)", hover: "#000" }, - common: { - black: { - main: "var(--gray1)" - } - }, additional: { blue: { 1: "var(--blue1)", @@ -394,7 +389,10 @@ export const darkTheme = createTheme({ previewModal: { controlsBackground: "var(--gray1)", controlsColor: "var(--gray10)", - closeButtonColor: "var(--gray9)", + headerButtonColor: "#DBDBDB", + controlsButtonColor: "#141414", + controlsBackgroundColor: "#DBDBDB", + controlsBackgroundHoverColor: "#FAFAFA", fileOpsColor: "var(--gray9)", fileNameColor: "var(--gray9)", optionsBackground: "var(--gray2)", @@ -478,6 +476,9 @@ export const darkTheme = createTheme({ lightSwitch: { backgroundColor: "var(--gray9)", color: "var(--gray1)" + }, + linkButton: { + color: "var(--csf-primary)" } }, surveyBanner: { @@ -663,10 +664,33 @@ export const darkTheme = createTheme({ } }, focus: { - color: "none", - backgroundColor: "none", + backgroundColor: "var(--gray7)", + color: "var(--gray9)", + "& svg": { + fill: "var(--gray9)" + } + } + }, + secondary: { + active: { + backgroundColor: "var(--gray2)", + color: "var(--gray9)", + "& svg": { + fill: "var(--gray9)" + } + }, + hover: { + backgroundColor: "var(--gray2)", + color: "var(--gray9)", + "& svg": { + fill: "var(--gray9)" + } + }, + focus: { + backgroundColor: "var(--gray2)", + color: "var(--gray9)", "& svg": { - fill: "none" + fill: "var(--gray9)" } } }, @@ -693,10 +717,10 @@ export const darkTheme = createTheme({ } }, focus: { - color: "none", - backgroundColor: "none", + backgroundColor: "var(--gray7)", + color: "var(--gray9)", "& svg": { - fill: "none" + fill: "var(--gray9)" } } }, diff --git a/packages/files-ui/src/Themes/LightTheme.ts b/packages/files-ui/src/Themes/LightTheme.ts index 655f992286..41acea571c 100644 --- a/packages/files-ui/src/Themes/LightTheme.ts +++ b/packages/files-ui/src/Themes/LightTheme.ts @@ -12,8 +12,6 @@ export const lightTheme = createTheme({ primary: { main: "var(--csf-primary)", background: "var(--csf-primary)" - }, - secondary: { } }, constants: { @@ -81,7 +79,10 @@ export const lightTheme = createTheme({ previewModal: { controlsBackground: "var(--gray9)", controlsColor: "var(--gray8)", - closeButtonColor: "var(--gray2)", + headerButtonColor: "#DBDBDB", + controlsButtonColor: "#141414", + controlsBackgroundColor: "#DBDBDB", + controlsBackgroundHoverColor: "#FAFAFA", fileOpsColor: "var(--gray2)", fileNameColor: "var(--gray1)", optionsBackground: "var(--gray1)", @@ -165,6 +166,9 @@ export const lightTheme = createTheme({ color: "var(--gray9)", border: "1px solid", borderColor: "var(--csf-primary)" + }, + linkButton: { + color: "var(--csf-primary)" } }, surveyBanner: { @@ -212,19 +216,6 @@ export const lightTheme = createTheme({ cursor: "pointer" } }, - Button: { - variants: { - primary: { - focus: { - color: "none", - backgroundColor: "none", - "& svg": { - fill: "none" - } - } - } - } - }, ToggleHiddenText: { icon: { stroke: "var(--gray9)" diff --git a/packages/files-ui/src/Utils/getItemIcon.ts b/packages/files-ui/src/Utils/getItemIcon.ts new file mode 100644 index 0000000000..4fb9f395d3 --- /dev/null +++ b/packages/files-ui/src/Utils/getItemIcon.ts @@ -0,0 +1,15 @@ +import { FileAudioIcon, FileIcon, FileImageIcon, FilePdfIcon, FileTextIcon, FileVideoIcon, FolderIcon } from "@chainsafe/common-components" +import { FileSystemItem } from "../Contexts/FilesContext" +import { matcher } from "./MimeMatcher" + +export const getIconForItem = (item: FileSystemItem) => { + if (item.isFolder) return FolderIcon + + if (matcher(["image/*"])(item.content_type)) return FileImageIcon + if (matcher(["text/*"])(item.content_type)) return FileTextIcon + if (matcher(["application/pdf"])(item.content_type)) return FilePdfIcon + if (matcher(["audio/*"])(item.content_type)) return FileAudioIcon + if (matcher(["video/*"])(item.content_type)) return FileVideoIcon + + return FileIcon +} diff --git a/packages/files-ui/src/Utils/getUserDisplayName.ts b/packages/files-ui/src/Utils/getUserDisplayName.ts index 1f4b85aa49..96c5a8ed93 100644 --- a/packages/files-ui/src/Utils/getUserDisplayName.ts +++ b/packages/files-ui/src/Utils/getUserDisplayName.ts @@ -1,6 +1,28 @@ import { LookupUser } from "@chainsafe/files-api-client" import { t } from "@lingui/macro" import { centerEllipsis } from "./Helpers" +import { ethers } from "ethers" -export const getUserDisplayName = (user: LookupUser) => - user.username || centerEllipsis(user.public_address.toLowerCase()) || centerEllipsis(user.uuid) || t`unknown` \ No newline at end of file +export const getUserDisplayName = async (user: LookupUser) => { + + if (user.username) return user.username + + if (!user.public_address) { + return centerEllipsis(user.uuid) || t`unknown` + } + + try { + // at this point the user have no username, and a public address hence maybe an ens + const provider = new ethers.providers.InfuraProvider("mainnet") + const lookupName = await provider.lookupAddress(user.public_address) + const lookupAddress = await provider.resolveName(lookupName) + + // double check that the lookup name actually resolves to the same address + return user.public_address === lookupAddress + ? lookupName + : centerEllipsis(user.public_address.toLowerCase()) + } catch { + // there is no reverse lookup for this address + return centerEllipsis(user.public_address.toLowerCase()) + } +} \ No newline at end of file diff --git a/packages/files-ui/src/locales/de/messages.po b/packages/files-ui/src/locales/de/messages.po index 82b1e1cc9c..c2939a1729 100644 --- a/packages/files-ui/src/locales/de/messages.po +++ b/packages/files-ui/src/locales/de/messages.po @@ -34,9 +34,6 @@ msgstr "" msgid "A backup secret phrase will be generated and used for your account.<0/>We do not store it and <1>it can only be displayed once. Save it somewhere safe!" msgstr "Es wird ein Sicherungsgeheimsatz generiert und für Ihr Konto verwendet.<0/>Wir speichern ihn nicht und <1>er kann nur einmal angezeigt werden. Speichern Sie ihn an einem sicheren Ort!" -msgid "A better sharing experience is coming soon." -msgstr "Ein besseres Teilen-Erlebnis ist in Vorbereitung." - msgid "A file with the same name already exists" msgstr "Es existiert bereits eine Datei mit demselben Namen" @@ -55,7 +52,7 @@ msgstr "Konto" msgid "Account is restricted" msgstr "" -msgid "Active links" +msgid "Account visibility" msgstr "" msgid "Add Card" @@ -70,9 +67,6 @@ msgstr "Einen Benutzernamen hinzufügen" msgid "Add at least one more authentication method to protect your account. You’d only need any two to sign in to Files from any device." msgstr "Fügen Sie mindestens eine weitere Authentifizierungsmethode hinzu, um Ihr Konto zu schützen. Sie bräuchten nur zwei, um sich von jedem Gerät aus bei Files anzumelden." -msgid "Add by sharing address, username, wallet address or ENS" -msgstr "" - msgid "Add card" msgstr "" @@ -88,21 +82,33 @@ msgstr "" msgid "Adding you to the shared folder..." msgstr "" +msgid "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." +msgstr "" + msgid "All invoices" msgstr "" msgid "Allow lookup by sharing key, wallet address, username or ENS" msgstr "" +msgid "Amount added to credit" +msgstr "" + +msgid "Amount available from last bill" +msgstr "" + +msgid "Amount due" +msgstr "" + +msgid "Amount from credit" +msgstr "" + msgid "An error occurred:" msgstr "Es ist ein Fehler aufgetreten:" msgid "Annual billing{0}" msgstr "" -msgid "Anyone with the link can:" -msgstr "" - msgid "Approve" msgstr "Genehmigen" @@ -139,6 +145,9 @@ msgstr "" msgid "By forgetting this browser, you will not be able to use its associated recovery key to sign-in." msgstr "Wenn Sie diesen Browser vergessen haben, können Sie den zugehörigen Wiederherstellungsschlüssel nicht mehr zur Anmeldung verwenden." +msgid "By switching plan, you will loose access to:" +msgstr "" + msgid "CID (Content Identifier)" msgstr "IID (Inhaltsidentifikator)" @@ -244,16 +253,7 @@ msgstr "Erstellen" msgid "Create Shared Folder" msgstr "" -msgid "Create a sharing link" -msgstr "" - -msgid "Create folder & Copy over" -msgstr "" - -msgid "Create folder & Move over" -msgstr "" - -msgid "Create link" +msgid "Create a new shared folder" msgstr "" msgid "Create your public username in <0>Settings!" @@ -262,7 +262,10 @@ msgstr "" msgid "Credit Card is expiring soon" msgstr "" -msgid "Credit card saved" +msgid "Crypto payments may take a few minutes to be processed. The subscription update will reflect on next login." +msgstr "" + +msgid "DAI, USDC, ETH or BTC" msgstr "" msgid "Dark Theme" @@ -307,8 +310,8 @@ msgstr "Gerät wartet auf Bestätigung" msgid "Didn't receive the email ?" msgstr "Sie haben die E-Mail nicht erhalten?" -msgid "Display Settings" -msgstr "Anzeigeeinstellungen" +msgid "Display" +msgstr "" msgid "Download" msgstr "Herunterladen" @@ -364,9 +367,6 @@ msgstr "Geben Sie den Verifizierungscode ein:" msgid "Failed to add payment method" msgstr "" -msgid "Failed to change subscription" -msgstr "" - msgid "Failed to create a charge" msgstr "" @@ -376,6 +376,9 @@ msgstr "Signatur kann nicht abgerufen werden" msgid "Failed to migrate account, please try again." msgstr "Die Migration des Kontos ist fehlgeschlagen, bitte versuchen Sie es erneut." +msgid "Failed to update the subscription. Please try again later." +msgstr "" + msgid "" "Failed to validate signature.\n" "If you are using a contract wallet, please make \n" @@ -400,12 +403,18 @@ msgstr "Dateigröße" msgid "Files" msgstr "Dateien" +msgid "Files Free" +msgstr "" + +msgid "Files Max" +msgstr "" + +msgid "Files Pro" +msgstr "" + msgid "Files sharing key" msgstr "Dateifreigabeschlüssel" -msgid "First name" -msgstr "Vorname" - msgid "Folder name is already in use" msgstr "" @@ -421,15 +430,15 @@ msgstr "Diesen Browser vergessen" msgid "Free" msgstr "" -msgid "Free plan" -msgstr "" - msgid "General" msgstr "Allgemein" msgid "Generate backup secret phrase" msgstr "Sicherungsgeheimsatz generieren" +msgid "Generate sharing link" +msgstr "" + msgid "Generated" msgstr "" @@ -439,15 +448,6 @@ msgstr "Wird generiert …" msgid "Get Started" msgstr "Anfangen" -msgid "Give edit permission to:" -msgstr "" - -msgid "Give view-only permission to:" -msgstr "" - -msgid "Go back" -msgstr "" - msgid "Go back" msgstr "Zurück" @@ -490,15 +490,15 @@ msgstr "" msgid "I’m done saving my backup secret phrase" msgstr "Ich bin fertig mit dem Speichern meines Sixherungsgeheimsatzes" +msgid "Join our new limited-access subscription plans to upgrade to a plan with more storage." +msgstr "" + msgid "Keep original files" msgstr "" msgid "Language" msgstr "Sprache" -msgid "Last name" -msgstr "Nachname" - msgid "Learn more" msgstr "Mehr erfahren" @@ -514,15 +514,15 @@ msgstr "Los geht’s." msgid "Light Theme" msgstr "Helles Farbschema" -msgid "Loading" -msgstr "" - msgid "Loading preview" msgstr "Vorschau wird geladen" msgid "Loading your shared folders…" msgstr "" +msgid "Loading..." +msgstr "" + msgid "Looks like you’re signing in from a new browser. Please choose one of the following to continue:" msgstr "Sieht aus, als würden Sie sich über einen neuen Browser anmelden. Bitte wählen Sie eine der folgenden Möglichkeiten, um fortzufahren:" @@ -550,6 +550,9 @@ msgstr "Meine Dateien" msgid "Name" msgstr "Name" +msgid "Need more storage?" +msgstr "" + msgid "New folder" msgstr "Neuer Ordner" @@ -559,6 +562,9 @@ msgstr "" msgid "Next" msgstr "Nächste" +msgid "Next payment" +msgstr "" + msgid "Nice to see you again!" msgstr "Schön, Sie wiederzusehen!" @@ -589,7 +595,10 @@ msgstr "Keine Suchergebnisse für" msgid "No thanks" msgstr "Nein danke" -msgid "No user found for this query." +msgid "No users found" +msgstr "" + +msgid "Notifications" msgstr "" msgid "Number of copies (Replication Factor)" @@ -598,14 +607,17 @@ msgstr "Anzahl der Kopien (Replikationsfaktor)" msgid "OK" msgstr "" -msgid "One sec, getting files ready…" +msgid "Older notifications" +msgstr "" + +msgid "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." msgstr "" -msgid "Only send {selectedCurrency} to this address" +msgid "One sec, getting files ready…" msgstr "" -msgid "Only you can see this." -msgstr "Nur Sie können dies sehen." +msgid "Only send the exact amount of {0} to this address" +msgstr "" msgid "Oops! You need to pay for this month to upload more content." msgstr "" @@ -613,12 +625,6 @@ msgstr "" msgid "Operating system:" msgstr "Betriebssystem:" -msgid "Or Create a new shared folder" -msgstr "" - -msgid "Or Use an existing shared folder" -msgstr "" - msgid "Or confirm by signing into your Files on any browser you’ve used before." msgstr "" @@ -646,7 +652,7 @@ msgstr "Passwort:" msgid "Passwords must match" msgstr "Passwörter müssen übereinstimmen" -msgid "Pay invoice" +msgid "Pay now" msgstr "" msgid "Pay with Crypto" @@ -658,15 +664,21 @@ msgstr "" msgid "Pay with {0}" msgstr "" -msgid "Payment and Subscriptions" +msgid "Payment information" msgstr "" msgid "Payment method" msgstr "" +msgid "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." +msgstr "" + msgid "Plan changed successfully" msgstr "" +msgid "Plan price" +msgstr "" + msgid "Please complete payment of the following outstanding invoices in order to avoid account suspension" msgstr "" @@ -685,14 +697,11 @@ msgstr "Bitte geben Sie ein Passwort an" msgid "Please select a file to upload" msgstr "Bitte wählen Sie eine Datei zum Hochladen" -msgid "Premium plan" -msgstr "" - msgid "Preview" msgstr "Vorschau" -msgid "Previous" -msgstr "Vorherige" +msgid "Pricing details" +msgstr "" msgid "Privacy Policy" msgstr "Datenschutzrichtlinie" @@ -700,14 +709,8 @@ msgstr "Datenschutzrichtlinie" msgid "Proceed to payment" msgstr "" -msgid "Profile and Display" -msgstr "Profil und Anzeige" - -msgid "Profile settings" -msgstr "Profileinstellungen" - -msgid "Profile updated" -msgstr "Profil aktualisiert" +msgid "Profile" +msgstr "" msgid "Recover" msgstr "Wiederherstellen" @@ -751,6 +754,9 @@ msgstr "Datei melden" msgid "Report a bug" msgstr "Fehler melden" +msgid "Request access!" +msgstr "" + msgid "Requested from" msgstr "Angefordert von" @@ -760,9 +766,6 @@ msgstr "Ressourcen" msgid "Restore with backup secret phrase" msgstr "Mit Sicherungsgeheimsatz wiederherstellen" -msgid "Save changes" -msgstr "Änderungen speichern" - msgid "Save this browser for next time?" msgstr "Diesen Browser für das nächste Mal speichern?" @@ -793,6 +796,9 @@ msgstr "Sicherheit" msgid "See payment info" msgstr "" +msgid "Select a cryptocurrency" +msgstr "" + msgid "Select a wallet" msgstr "" @@ -841,9 +847,6 @@ msgstr "" msgid "Shared" msgstr "Geteilt" -msgid "Shared Folder Name" -msgstr "" - msgid "Shared folder name" msgstr "" @@ -868,9 +871,6 @@ msgstr "Mit einem anderen Konto anmelden" msgid "Sign in/up to access the shared folder" msgstr "" -msgid "Sign me up!" -msgstr "" - msgid "Sign-in methods" msgstr "Anmeldemethoden" @@ -895,21 +895,18 @@ msgstr "Es ist etwas schief gelaufen. Wir konnten Ihre Datei nicht hochladen" msgid "Sort By:" msgstr "" -msgid "Standard plan" -msgstr "" - msgid "Start Upload" msgstr "Hochladen starten" -msgid "Start a team" -msgstr "" - msgid "Stored by miner" msgstr "" msgid "Subscription Plan" msgstr "" +msgid "Subscription plan" +msgstr "" + msgid "Switch Network" msgstr "" @@ -922,9 +919,6 @@ msgstr "" msgid "System maintenance is scheduled to start at {0}. The system will be unavailable." msgstr "" -msgid "Teams" -msgstr "" - msgid "Technical" msgstr "Technisch" @@ -940,6 +934,12 @@ msgstr "" msgid "The link you typed in looks malformed. Please verify it." msgstr "" +msgid "The time to pay with crypto is up. Updating your plan..." +msgstr "" + +msgid "The transaction was declined. Please use a different card or try again." +msgstr "" + msgid "The username is too long" msgstr "" @@ -982,9 +982,6 @@ msgstr "" msgid "This card will become your default payment method" msgstr "" -msgid "This is the free product." -msgstr "" - msgid "This link is marlformed. Please verify that you copy/pasted it correctly." msgstr "" @@ -1000,6 +997,9 @@ msgstr "" msgid "This website uses cookies that help the website function and track interactions for analytics purposes. You have the right to decline our use of cookies. For us to provide a customizable user experience to you, please click on the Accept button below.<0>Learn more" msgstr "" +msgid "This week" +msgstr "" + msgid "Total" msgstr "" @@ -1042,10 +1042,10 @@ msgstr "Eine andere Anmeldemethode verwenden" msgid "Use a saved browser" msgstr "Einen gespeicherten Browser verwenden" -msgid "Use this card" +msgid "Use an existing shared folder" msgstr "" -msgid "User {0} is both a reader and writer" +msgid "Use this card" msgstr "" msgid "Username" @@ -1054,6 +1054,9 @@ msgstr "Benutzername" msgid "Username set successfully" msgstr "Benutzername erfolgreich gesetzt" +msgid "Username, wallet address or ENS" +msgstr "" + msgid "Usernames are public and can't be changed after creation." msgstr "Benutzernamen sind öffentlich und können nach der Erstellung nicht mehr geändert werden." @@ -1072,6 +1075,9 @@ msgstr "Verifizierungscode gesendet!" msgid "Verifying the link..." msgstr "" +msgid "View PDF" +msgstr "" + msgid "View folder" msgstr "Ordner anzeigen" @@ -1132,33 +1138,30 @@ msgstr "" msgid "You will need to sign a message in your wallet to complete sign in." msgstr "" -msgid "You would lose the following features:" -msgstr "" - msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "" msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "" -msgid "Your plan" -msgstr "" - msgid "Your recovery key can be used to restore your account in place of your backup secret phrase." msgstr "" msgid "can-edit" msgstr "" -msgid "invoices here" +msgid "expires" msgstr "" -msgid "me" -msgstr "ich" +msgid "invoices here" +msgstr "" msgid "on" msgstr "am" +msgid "or" +msgstr "" + msgid "unknown" msgstr "unbekannt" diff --git a/packages/files-ui/src/locales/en/messages.po b/packages/files-ui/src/locales/en/messages.po index 3092b52e03..3dc3a8ff09 100644 --- a/packages/files-ui/src/locales/en/messages.po +++ b/packages/files-ui/src/locales/en/messages.po @@ -34,9 +34,6 @@ msgstr "<0>{planStorageCapacity} of storage" msgid "A backup secret phrase will be generated and used for your account.<0/>We do not store it and <1>it can only be displayed once. Save it somewhere safe!" msgstr "A backup secret phrase will be generated and used for your account.<0/>We do not store it and <1>it can only be displayed once. Save it somewhere safe!" -msgid "A better sharing experience is coming soon." -msgstr "A better sharing experience is coming soon." - msgid "A file with the same name already exists" msgstr "A file with the same name already exists" @@ -55,8 +52,8 @@ msgstr "Account" msgid "Account is restricted" msgstr "Account is restricted" -msgid "Active links" -msgstr "Active links" +msgid "Account visibility" +msgstr "Account visibility" msgid "Add Card" msgstr "Add Card" @@ -70,9 +67,6 @@ msgstr "Add a username" msgid "Add at least one more authentication method to protect your account. You’d only need any two to sign in to Files from any device." msgstr "Add at least one more authentication method to protect your account. You’d only need any two to sign in to Files from any device." -msgid "Add by sharing address, username, wallet address or ENS" -msgstr "Add by sharing address, username, wallet address or ENS" - msgid "Add card" msgstr "Add card" @@ -88,21 +82,33 @@ msgstr "Add viewers and editors by username, sharing id or Ethereum address." msgid "Adding you to the shared folder..." msgstr "Adding you to the shared folder..." +msgid "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." +msgstr "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." + msgid "All invoices" msgstr "All invoices" msgid "Allow lookup by sharing key, wallet address, username or ENS" msgstr "Allow lookup by sharing key, wallet address, username or ENS" +msgid "Amount added to credit" +msgstr "Amount added to credit" + +msgid "Amount available from last bill" +msgstr "Amount available from last bill" + +msgid "Amount due" +msgstr "Amount due" + +msgid "Amount from credit" +msgstr "Amount from credit" + msgid "An error occurred:" msgstr "An error occurred:" msgid "Annual billing{0}" msgstr "Annual billing{0}" -msgid "Anyone with the link can:" -msgstr "Anyone with the link can:" - msgid "Approve" msgstr "Approve" @@ -139,6 +145,9 @@ msgstr "By connecting your wallet, you agree to our <0>Terms of Service and msgid "By forgetting this browser, you will not be able to use its associated recovery key to sign-in." msgstr "By forgetting this browser, you will not be able to use its associated recovery key to sign-in." +msgid "By switching plan, you will loose access to:" +msgstr "By switching plan, you will loose access to:" + msgid "CID (Content Identifier)" msgstr "CID (Content Identifier)" @@ -244,17 +253,8 @@ msgstr "Create" msgid "Create Shared Folder" msgstr "Create Shared Folder" -msgid "Create a sharing link" -msgstr "Create a sharing link" - -msgid "Create folder & Copy over" -msgstr "Create folder & Copy over" - -msgid "Create folder & Move over" -msgstr "Create folder & Move over" - -msgid "Create link" -msgstr "Create link" +msgid "Create a new shared folder" +msgstr "Create a new shared folder" msgid "Create your public username in <0>Settings!" msgstr "Create your public username in <0>Settings!" @@ -262,8 +262,11 @@ msgstr "Create your public username in <0>Settings!" msgid "Credit Card is expiring soon" msgstr "Credit Card is expiring soon" -msgid "Credit card saved" -msgstr "Credit card saved" +msgid "Crypto payments may take a few minutes to be processed. The subscription update will reflect on next login." +msgstr "Crypto payments may take a few minutes to be processed. The subscription update will reflect on next login." + +msgid "DAI, USDC, ETH or BTC" +msgstr "DAI, USDC, ETH or BTC" msgid "Dark Theme" msgstr "Dark Theme" @@ -307,8 +310,8 @@ msgstr "Device awaiting confirmation" msgid "Didn't receive the email ?" msgstr "Didn't receive the email ?" -msgid "Display Settings" -msgstr "Display Settings" +msgid "Display" +msgstr "Display" msgid "Download" msgstr "Download" @@ -364,9 +367,6 @@ msgstr "Enter the verification code:" msgid "Failed to add payment method" msgstr "Failed to add payment method" -msgid "Failed to change subscription" -msgstr "Failed to change subscription" - msgid "Failed to create a charge" msgstr "Failed to create a charge" @@ -376,6 +376,9 @@ msgstr "Failed to get signature" msgid "Failed to migrate account, please try again." msgstr "Failed to migrate account, please try again." +msgid "Failed to update the subscription. Please try again later." +msgstr "Failed to update the subscription. Please try again later." + msgid "" "Failed to validate signature.\n" "If you are using a contract wallet, please make \n" @@ -403,12 +406,18 @@ msgstr "File size" msgid "Files" msgstr "Files" +msgid "Files Free" +msgstr "Files Free" + +msgid "Files Max" +msgstr "Files Max" + +msgid "Files Pro" +msgstr "Files Pro" + msgid "Files sharing key" msgstr "Files sharing key" -msgid "First name" -msgstr "First name" - msgid "Folder name is already in use" msgstr "Folder name is already in use" @@ -424,15 +433,15 @@ msgstr "Forget this browser" msgid "Free" msgstr "Free" -msgid "Free plan" -msgstr "Free plan" - msgid "General" msgstr "General" msgid "Generate backup secret phrase" msgstr "Generate backup secret phrase" +msgid "Generate sharing link" +msgstr "Generate sharing link" + msgid "Generated" msgstr "Generated" @@ -442,15 +451,6 @@ msgstr "Generating…" msgid "Get Started" msgstr "Get Started" -msgid "Give edit permission to:" -msgstr "Give edit permission to:" - -msgid "Give view-only permission to:" -msgstr "Give view-only permission to:" - -msgid "Go back" -msgstr "Go back" - msgid "Go back" msgstr "Go back" @@ -493,15 +493,15 @@ msgstr "Invoice outstanding" msgid "I’m done saving my backup secret phrase" msgstr "I’m done saving my backup secret phrase" +msgid "Join our new limited-access subscription plans to upgrade to a plan with more storage." +msgstr "Join our new limited-access subscription plans to upgrade to a plan with more storage." + msgid "Keep original files" msgstr "Keep original files" msgid "Language" msgstr "Language" -msgid "Last name" -msgstr "Last name" - msgid "Learn more" msgstr "Learn more" @@ -517,15 +517,15 @@ msgstr "Let's get you set up." msgid "Light Theme" msgstr "Light Theme" -msgid "Loading" -msgstr "Loading" - msgid "Loading preview" msgstr "Loading preview" msgid "Loading your shared folders…" msgstr "Loading your shared folders…" +msgid "Loading..." +msgstr "Loading..." + msgid "Looks like you’re signing in from a new browser. Please choose one of the following to continue:" msgstr "Looks like you’re signing in from a new browser. Please choose one of the following to continue:" @@ -553,6 +553,9 @@ msgstr "My Files" msgid "Name" msgstr "Name" +msgid "Need more storage?" +msgstr "Need more storage?" + msgid "New folder" msgstr "New folder" @@ -562,6 +565,9 @@ msgstr "New shared folder name" msgid "Next" msgstr "Next" +msgid "Next payment" +msgstr "Next payment" + msgid "Nice to see you again!" msgstr "Nice to see you again!" @@ -592,8 +598,11 @@ msgstr "No search results for" msgid "No thanks" msgstr "No thanks" -msgid "No user found for this query." -msgstr "No user found for this query." +msgid "No users found" +msgstr "No users found" + +msgid "Notifications" +msgstr "Notifications" msgid "Number of copies (Replication Factor)" msgstr "Number of copies (Replication Factor)" @@ -601,14 +610,17 @@ msgstr "Number of copies (Replication Factor)" msgid "OK" msgstr "OK" +msgid "Older notifications" +msgstr "Older notifications" + +msgid "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." +msgstr "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." + msgid "One sec, getting files ready…" msgstr "One sec, getting files ready…" -msgid "Only send {selectedCurrency} to this address" -msgstr "Only send {selectedCurrency} to this address" - -msgid "Only you can see this." -msgstr "Only you can see this." +msgid "Only send the exact amount of {0} to this address" +msgstr "Only send the exact amount of {0} to this address" msgid "Oops! You need to pay for this month to upload more content." msgstr "Oops! You need to pay for this month to upload more content." @@ -616,12 +628,6 @@ msgstr "Oops! You need to pay for this month to upload more content." msgid "Operating system:" msgstr "Operating system:" -msgid "Or Create a new shared folder" -msgstr "Or Create a new shared folder" - -msgid "Or Use an existing shared folder" -msgstr "Or Use an existing shared folder" - msgid "Or confirm by signing into your Files on any browser you’ve used before." msgstr "Or confirm by signing into your Files on any browser you’ve used before." @@ -649,8 +655,8 @@ msgstr "Password:" msgid "Passwords must match" msgstr "Passwords must match" -msgid "Pay invoice" -msgstr "Pay invoice" +msgid "Pay now" +msgstr "Pay now" msgid "Pay with Crypto" msgstr "Pay with Crypto" @@ -661,15 +667,21 @@ msgstr "Pay with crypto" msgid "Pay with {0}" msgstr "Pay with {0}" -msgid "Payment and Subscriptions" -msgstr "Payment and Subscriptions" +msgid "Payment information" +msgstr "Payment information" msgid "Payment method" msgstr "Payment method" +msgid "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." +msgstr "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." + msgid "Plan changed successfully" msgstr "Plan changed successfully" +msgid "Plan price" +msgstr "Plan price" + msgid "Please complete payment of the following outstanding invoices in order to avoid account suspension" msgstr "Please complete payment of the following outstanding invoices in order to avoid account suspension" @@ -688,14 +700,11 @@ msgstr "Please provide a password" msgid "Please select a file to upload" msgstr "Please select a file to upload" -msgid "Premium plan" -msgstr "Premium plan" - msgid "Preview" msgstr "Preview" -msgid "Previous" -msgstr "Previous" +msgid "Pricing details" +msgstr "Pricing details" msgid "Privacy Policy" msgstr "Privacy Policy" @@ -703,14 +712,8 @@ msgstr "Privacy Policy" msgid "Proceed to payment" msgstr "Proceed to payment" -msgid "Profile and Display" -msgstr "Profile and Display" - -msgid "Profile settings" -msgstr "Profile settings" - -msgid "Profile updated" -msgstr "Profile updated" +msgid "Profile" +msgstr "Profile" msgid "Recover" msgstr "Recover" @@ -754,6 +757,9 @@ msgstr "Report a File" msgid "Report a bug" msgstr "Report a bug" +msgid "Request access!" +msgstr "Request access!" + msgid "Requested from" msgstr "Requested from" @@ -763,9 +769,6 @@ msgstr "Resources" msgid "Restore with backup secret phrase" msgstr "Restore with backup secret phrase" -msgid "Save changes" -msgstr "Save changes" - msgid "Save this browser for next time?" msgstr "Save this browser for next time?" @@ -796,6 +799,9 @@ msgstr "Security" msgid "See payment info" msgstr "See payment info" +msgid "Select a cryptocurrency" +msgstr "Select a cryptocurrency" + msgid "Select a wallet" msgstr "Select a wallet" @@ -844,9 +850,6 @@ msgstr "Share selected" msgid "Shared" msgstr "Shared" -msgid "Shared Folder Name" -msgstr "Shared Folder Name" - msgid "Shared folder name" msgstr "Shared folder name" @@ -871,9 +874,6 @@ msgstr "Sign in with a different account" msgid "Sign in/up to access the shared folder" msgstr "Sign in/up to access the shared folder" -msgid "Sign me up!" -msgstr "Sign me up!" - msgid "Sign-in methods" msgstr "Sign-in methods" @@ -898,21 +898,18 @@ msgstr "Something went wrong. We couldn't upload your file" msgid "Sort By:" msgstr "Sort By:" -msgid "Standard plan" -msgstr "Standard plan" - msgid "Start Upload" msgstr "Start Upload" -msgid "Start a team" -msgstr "Start a team" - msgid "Stored by miner" msgstr "Stored by miner" msgid "Subscription Plan" msgstr "Subscription Plan" +msgid "Subscription plan" +msgstr "Subscription plan" + msgid "Switch Network" msgstr "Switch Network" @@ -925,9 +922,6 @@ msgstr "Switch to Free plan" msgid "System maintenance is scheduled to start at {0}. The system will be unavailable." msgstr "System maintenance is scheduled to start at {0}. The system will be unavailable." -msgid "Teams" -msgstr "Teams" - msgid "Technical" msgstr "Technical" @@ -943,6 +937,12 @@ msgstr "The files are already in this folder" msgid "The link you typed in looks malformed. Please verify it." msgstr "The link you typed in looks malformed. Please verify it." +msgid "The time to pay with crypto is up. Updating your plan..." +msgstr "The time to pay with crypto is up. Updating your plan..." + +msgid "The transaction was declined. Please use a different card or try again." +msgstr "The transaction was declined. Please use a different card or try again." + msgid "The username is too long" msgstr "The username is too long" @@ -985,9 +985,6 @@ msgstr "There was an error when setting username." msgid "This card will become your default payment method" msgstr "This card will become your default payment method" -msgid "This is the free product." -msgstr "This is the free product." - msgid "This link is marlformed. Please verify that you copy/pasted it correctly." msgstr "This link is marlformed. Please verify that you copy/pasted it correctly." @@ -1003,6 +1000,9 @@ msgstr "This website uses cookies" msgid "This website uses cookies that help the website function and track interactions for analytics purposes. You have the right to decline our use of cookies. For us to provide a customizable user experience to you, please click on the Accept button below.<0>Learn more" msgstr "This website uses cookies that help the website function and track interactions for analytics purposes. You have the right to decline our use of cookies. For us to provide a customizable user experience to you, please click on the Accept button below.<0>Learn more" +msgid "This week" +msgstr "This week" + msgid "Total" msgstr "Total" @@ -1045,18 +1045,21 @@ msgstr "Use a different login method" msgid "Use a saved browser" msgstr "Use a saved browser" +msgid "Use an existing shared folder" +msgstr "Use an existing shared folder" + msgid "Use this card" msgstr "Use this card" -msgid "User {0} is both a reader and writer" -msgstr "User {0} is both a reader and writer" - msgid "Username" msgstr "Username" msgid "Username set successfully" msgstr "Username set successfully" +msgid "Username, wallet address or ENS" +msgstr "Username, wallet address or ENS" + msgid "Usernames are public and can't be changed after creation." msgstr "Usernames are public and can't be changed after creation." @@ -1075,6 +1078,9 @@ msgstr "Verification code sent!" msgid "Verifying the link..." msgstr "Verifying the link..." +msgid "View PDF" +msgstr "View PDF" + msgid "View folder" msgstr "View folder" @@ -1135,33 +1141,30 @@ msgstr "You were added to the shared folder ({0}): {1}" msgid "You will need to sign a message in your wallet to complete sign in." msgstr "You will need to sign a message in your wallet to complete sign in." -msgid "You would lose the following features:" -msgstr "You would lose the following features:" - msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "Your content exceeds the {planStorageCapacity} storage capacity for this plan." -msgid "Your plan" -msgstr "Your plan" - msgid "Your recovery key can be used to restore your account in place of your backup secret phrase." msgstr "Your recovery key can be used to restore your account in place of your backup secret phrase." msgid "can-edit" msgstr "can-edit" +msgid "expires" +msgstr "expires" + msgid "invoices here" msgstr "invoices here" -msgid "me" -msgstr "me" - msgid "on" msgstr "on" +msgid "or" +msgstr "or" + msgid "unknown" msgstr "unknown" diff --git a/packages/files-ui/src/locales/es/messages.po b/packages/files-ui/src/locales/es/messages.po index fc2b7fa469..e1d921f8ea 100644 --- a/packages/files-ui/src/locales/es/messages.po +++ b/packages/files-ui/src/locales/es/messages.po @@ -35,9 +35,6 @@ msgstr "" msgid "A backup secret phrase will be generated and used for your account.<0/>We do not store it and <1>it can only be displayed once. Save it somewhere safe!" msgstr "" -msgid "A better sharing experience is coming soon." -msgstr "" - msgid "A file with the same name already exists" msgstr "Ya existe un archivo con el mismo nombre" @@ -56,7 +53,7 @@ msgstr "Cuenta" msgid "Account is restricted" msgstr "" -msgid "Active links" +msgid "Account visibility" msgstr "" msgid "Add Card" @@ -71,9 +68,6 @@ msgstr "" msgid "Add at least one more authentication method to protect your account. You’d only need any two to sign in to Files from any device." msgstr "Agregue al menos un método de autenticación más para proteger su cuenta. Solo necesitaría dos para iniciar sesión en Archivos desde cualquier dispositivo." -msgid "Add by sharing address, username, wallet address or ENS" -msgstr "" - msgid "Add card" msgstr "" @@ -89,19 +83,31 @@ msgstr "" msgid "Adding you to the shared folder..." msgstr "" +msgid "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." +msgstr "" + msgid "All invoices" msgstr "" msgid "Allow lookup by sharing key, wallet address, username or ENS" msgstr "" -msgid "An error occurred:" +msgid "Amount added to credit" msgstr "" -msgid "Annual billing{0}" +msgid "Amount available from last bill" +msgstr "" + +msgid "Amount due" +msgstr "" + +msgid "Amount from credit" +msgstr "" + +msgid "An error occurred:" msgstr "" -msgid "Anyone with the link can:" +msgid "Annual billing{0}" msgstr "" msgid "Approve" @@ -140,6 +146,9 @@ msgstr "Al conectar su billetera, acepta nuestras <0> Condiciones de servicio Settings!" @@ -263,7 +263,10 @@ msgstr "" msgid "Credit Card is expiring soon" msgstr "" -msgid "Credit card saved" +msgid "Crypto payments may take a few minutes to be processed. The subscription update will reflect on next login." +msgstr "" + +msgid "DAI, USDC, ETH or BTC" msgstr "" msgid "Dark Theme" @@ -308,8 +311,8 @@ msgstr "Dispositivo pendiente de confirmación" msgid "Didn't receive the email ?" msgstr "" -msgid "Display Settings" -msgstr "Configuración de pantalla" +msgid "Display" +msgstr "" msgid "Download" msgstr "Descargar" @@ -365,9 +368,6 @@ msgstr "" msgid "Failed to add payment method" msgstr "" -msgid "Failed to change subscription" -msgstr "" - msgid "Failed to create a charge" msgstr "" @@ -377,6 +377,9 @@ msgstr "No se pudo obtener la firma" msgid "Failed to migrate account, please try again." msgstr "No se pudo migrar la cuenta. Vuelve a intentarlo." +msgid "Failed to update the subscription. Please try again later." +msgstr "" + msgid "" "Failed to validate signature.\n" "If you are using a contract wallet, please make \n" @@ -404,11 +407,17 @@ msgstr "Tamaño del archivo" msgid "Files" msgstr "Archivos" -msgid "Files sharing key" +msgid "Files Free" +msgstr "" + +msgid "Files Max" msgstr "" -msgid "First name" -msgstr "Primer Nombre" +msgid "Files Pro" +msgstr "" + +msgid "Files sharing key" +msgstr "" msgid "Folder name is already in use" msgstr "" @@ -425,15 +434,15 @@ msgstr "Olvida este navegador" msgid "Free" msgstr "" -msgid "Free plan" -msgstr "" - msgid "General" msgstr "General" msgid "Generate backup secret phrase" msgstr "" +msgid "Generate sharing link" +msgstr "" + msgid "Generated" msgstr "" @@ -443,15 +452,6 @@ msgstr "" msgid "Get Started" msgstr "Empezar" -msgid "Give edit permission to:" -msgstr "" - -msgid "Give view-only permission to:" -msgstr "" - -msgid "Go back" -msgstr "" - msgid "Go back" msgstr "Regresar" @@ -494,15 +494,15 @@ msgstr "" msgid "I’m done saving my backup secret phrase" msgstr "" +msgid "Join our new limited-access subscription plans to upgrade to a plan with more storage." +msgstr "" + msgid "Keep original files" msgstr "" msgid "Language" msgstr "Idioma" -msgid "Last name" -msgstr "Apellido" - msgid "Learn more" msgstr "Aprende Mas" @@ -518,15 +518,15 @@ msgstr "Vamos a configurarlo." msgid "Light Theme" msgstr "Tema Claro" -msgid "Loading" -msgstr "" - msgid "Loading preview" msgstr "Cargando vista previa" msgid "Loading your shared folders…" msgstr "" +msgid "Loading..." +msgstr "" + msgid "Looks like you’re signing in from a new browser. Please choose one of the following to continue:" msgstr "Parece que está iniciando sesión desde un nuevo navegador. Elija una de las siguientes opciones para continuar:" @@ -554,6 +554,9 @@ msgstr "Mis Archivos" msgid "Name" msgstr "Nombre" +msgid "Need more storage?" +msgstr "" + msgid "New folder" msgstr "Nuevo Folder" @@ -563,6 +566,9 @@ msgstr "" msgid "Next" msgstr "Próximo" +msgid "Next payment" +msgstr "" + msgid "Nice to see you again!" msgstr "Encantado de verte de nuevo!" @@ -593,7 +599,10 @@ msgstr "No hay resultados de búsqueda para" msgid "No thanks" msgstr "No gracias" -msgid "No user found for this query." +msgid "No users found" +msgstr "" + +msgid "Notifications" msgstr "" msgid "Number of copies (Replication Factor)" @@ -602,13 +611,16 @@ msgstr "Número de copias (factor de replicación)" msgid "OK" msgstr "" -msgid "One sec, getting files ready…" +msgid "Older notifications" +msgstr "" + +msgid "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." msgstr "" -msgid "Only send {selectedCurrency} to this address" +msgid "One sec, getting files ready…" msgstr "" -msgid "Only you can see this." +msgid "Only send the exact amount of {0} to this address" msgstr "" msgid "Oops! You need to pay for this month to upload more content." @@ -617,12 +629,6 @@ msgstr "" msgid "Operating system:" msgstr "Sistema operativo:" -msgid "Or Create a new shared folder" -msgstr "" - -msgid "Or Use an existing shared folder" -msgstr "" - msgid "Or confirm by signing into your Files on any browser you’ve used before." msgstr "O confirme iniciando sesión en sus Archivos en cualquier navegador que haya usado antes." @@ -650,7 +656,7 @@ msgstr "Contraseña:" msgid "Passwords must match" msgstr "Las contraseñas deben coincidir" -msgid "Pay invoice" +msgid "Pay now" msgstr "" msgid "Pay with Crypto" @@ -662,15 +668,21 @@ msgstr "" msgid "Pay with {0}" msgstr "" -msgid "Payment and Subscriptions" +msgid "Payment information" msgstr "" msgid "Payment method" msgstr "" +msgid "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." +msgstr "" + msgid "Plan changed successfully" msgstr "" +msgid "Plan price" +msgstr "" + msgid "Please complete payment of the following outstanding invoices in order to avoid account suspension" msgstr "" @@ -689,14 +701,11 @@ msgstr "Por favor ingrese una contraseña" msgid "Please select a file to upload" msgstr "" -msgid "Premium plan" -msgstr "" - msgid "Preview" msgstr "Avance" -msgid "Previous" -msgstr "Anterior" +msgid "Pricing details" +msgstr "" msgid "Privacy Policy" msgstr "Política de privacidad" @@ -704,15 +713,9 @@ msgstr "Política de privacidad" msgid "Proceed to payment" msgstr "" -msgid "Profile and Display" -msgstr "Perfil y pantalla" - -msgid "Profile settings" +msgid "Profile" msgstr "" -msgid "Profile updated" -msgstr "Perfil actualizado" - msgid "Recover" msgstr "Recuperar" @@ -755,6 +758,9 @@ msgstr "" msgid "Report a bug" msgstr "" +msgid "Request access!" +msgstr "" + msgid "Requested from" msgstr "Solicitado a" @@ -764,9 +770,6 @@ msgstr "Recursos" msgid "Restore with backup secret phrase" msgstr "" -msgid "Save changes" -msgstr "Guardar cambios" - msgid "Save this browser for next time?" msgstr "Guardar este navegador para la próxima vez?" @@ -797,6 +800,9 @@ msgstr "Seguridad" msgid "See payment info" msgstr "" +msgid "Select a cryptocurrency" +msgstr "" + msgid "Select a wallet" msgstr "Seleccione una billetera" @@ -845,9 +851,6 @@ msgstr "" msgid "Shared" msgstr "" -msgid "Shared Folder Name" -msgstr "" - msgid "Shared folder name" msgstr "" @@ -872,9 +875,6 @@ msgstr "Inicie sesión con una cuenta diferente" msgid "Sign in/up to access the shared folder" msgstr "" -msgid "Sign me up!" -msgstr "" - msgid "Sign-in methods" msgstr "Métodos de inicio de sesión" @@ -899,21 +899,18 @@ msgstr "Algo salió mal. No pudimos subir tu archivo" msgid "Sort By:" msgstr "" -msgid "Standard plan" -msgstr "" - msgid "Start Upload" msgstr "Iniciar la subida" -msgid "Start a team" -msgstr "" - msgid "Stored by miner" msgstr "Almacenado por el minero" msgid "Subscription Plan" msgstr "" +msgid "Subscription plan" +msgstr "" + msgid "Switch Network" msgstr "" @@ -926,9 +923,6 @@ msgstr "" msgid "System maintenance is scheduled to start at {0}. The system will be unavailable." msgstr "" -msgid "Teams" -msgstr "" - msgid "Technical" msgstr "Técnico" @@ -944,6 +938,12 @@ msgstr "" msgid "The link you typed in looks malformed. Please verify it." msgstr "" +msgid "The time to pay with crypto is up. Updating your plan..." +msgstr "" + +msgid "The transaction was declined. Please use a different card or try again." +msgstr "" + msgid "The username is too long" msgstr "" @@ -986,9 +986,6 @@ msgstr "" msgid "This card will become your default payment method" msgstr "" -msgid "This is the free product." -msgstr "" - msgid "This link is marlformed. Please verify that you copy/pasted it correctly." msgstr "" @@ -1004,6 +1001,9 @@ msgstr "" msgid "This website uses cookies that help the website function and track interactions for analytics purposes. You have the right to decline our use of cookies. For us to provide a customizable user experience to you, please click on the Accept button below.<0>Learn more" msgstr "" +msgid "This week" +msgstr "" + msgid "Total" msgstr "" @@ -1046,10 +1046,10 @@ msgstr "Utilice un método de inicio de sesión diferente" msgid "Use a saved browser" msgstr "Utilice un navegador guardado" -msgid "Use this card" +msgid "Use an existing shared folder" msgstr "" -msgid "User {0} is both a reader and writer" +msgid "Use this card" msgstr "" msgid "Username" @@ -1058,6 +1058,9 @@ msgstr "" msgid "Username set successfully" msgstr "" +msgid "Username, wallet address or ENS" +msgstr "" + msgid "Usernames are public and can't be changed after creation." msgstr "" @@ -1076,6 +1079,9 @@ msgstr "" msgid "Verifying the link..." msgstr "" +msgid "View PDF" +msgstr "" + msgid "View folder" msgstr "Utilice un navegador guardado" @@ -1136,33 +1142,30 @@ msgstr "" msgid "You will need to sign a message in your wallet to complete sign in." msgstr "Deberá firmar un mensaje en su billetera para completar el inicio de sesión." -msgid "You would lose the following features:" -msgstr "" - msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "" msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "" -msgid "Your plan" -msgstr "" - msgid "Your recovery key can be used to restore your account in place of your backup secret phrase." msgstr "" msgid "can-edit" msgstr "" -msgid "invoices here" +msgid "expires" msgstr "" -msgid "me" +msgid "invoices here" msgstr "" msgid "on" msgstr "en" +msgid "or" +msgstr "" + msgid "unknown" msgstr "" diff --git a/packages/files-ui/src/locales/fr/messages.po b/packages/files-ui/src/locales/fr/messages.po index e4b3ee657e..081d449eae 100644 --- a/packages/files-ui/src/locales/fr/messages.po +++ b/packages/files-ui/src/locales/fr/messages.po @@ -3,15 +3,15 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-04-23 11:05+0200\n" -"PO-Revision-Date: 2022-01-20 22:53+0000\n" -"Last-Translator: J. Lavoie \n" +"PO-Revision-Date: 2022-02-28 19:31+0000\n" +"Last-Translator: Thibaut \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.11-dev\n" +"X-Generator: Weblate 4.11.1-dev\n" "Mime-Version: 1.0\n" msgid "(Awaiting payment)" @@ -35,9 +35,6 @@ msgstr "<0>{planStorageCapacity} de stockage" msgid "A backup secret phrase will be generated and used for your account.<0/>We do not store it and <1>it can only be displayed once. Save it somewhere safe!" msgstr "Une phrase secrète de sauvegarde sera générée et utilisé pour ce compte.<0/>Nous ne la sauvergardons pas <1>elle ne peut être affichée qu’une seule fois. Gardez-la dans un endroit sûr !" -msgid "A better sharing experience is coming soon." -msgstr "Une meilleure expérience de partage sera bientôt disponible." - msgid "A file with the same name already exists" msgstr "Un fichier avec ce nom existe déjà" @@ -56,8 +53,8 @@ msgstr "Compte" msgid "Account is restricted" msgstr "Ce compte est limité" -msgid "Active links" -msgstr "Liens actifs" +msgid "Account visibility" +msgstr "" msgid "Add Card" msgstr "Ajouter une carte" @@ -71,9 +68,6 @@ msgstr "Ajouter un nom d’utilisateur" msgid "Add at least one more authentication method to protect your account. You’d only need any two to sign in to Files from any device." msgstr "Ajoutez au moins une méthode d’authentification pour protéger ce compte. Vous avez besoin de deux méthode pour accéder à Files depuis n’importe quel appareil." -msgid "Add by sharing address, username, wallet address or ENS" -msgstr "" - msgid "Add card" msgstr "Ajouter une carte" @@ -89,20 +83,32 @@ msgstr "Ajoutez des personnes pouvant visualiser ou afficher par nom d'utilisate msgid "Adding you to the shared folder..." msgstr "Je vous ajoute au dossier partagé…" +msgid "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." +msgstr "Tous les paiements via crypto-monnaie sont définitifs et ne peuvent être crédités, échangés ou remboursés. Si vous souhaitez changer votre formule, vos fonds ne seront pas remboursés." + msgid "All invoices" msgstr "Toutes les factures" msgid "Allow lookup by sharing key, wallet address, username or ENS" -msgstr "" +msgstr "Autoriser la recherche par la clé partagée, l'adresse du portefeuille, le nom d'utilisateur, ou l'ENS" + +msgid "Amount added to credit" +msgstr "Montant ajouté à la balance" + +msgid "Amount available from last bill" +msgstr "La montant disponible de la dernière facture" + +msgid "Amount due" +msgstr "Montant dû" + +msgid "Amount from credit" +msgstr "Montant du crédit" msgid "An error occurred:" msgstr "Une erreur s'est produite :" msgid "Annual billing{0}" -msgstr "" - -msgid "Anyone with the link can:" -msgstr "Toute personne ayant le lien peut le faire :" +msgstr "Facturation annuelle{0}" msgid "Approve" msgstr "Accepter" @@ -140,6 +146,9 @@ msgstr "En connectant votre portefeuille, vous acceptez nos <0>conditions de ser msgid "By forgetting this browser, you will not be able to use its associated recovery key to sign-in." msgstr "En oubliant ce navigateur, vous ne pourrez pas utiliser la clé de récupération qui lui est associée pour vous connecter." +msgid "By switching plan, you will loose access to:" +msgstr "En changeant de formule, vous perdrez l'accès à :" + msgid "CID (Content Identifier)" msgstr "CID (Identifiant de contenu)" @@ -245,17 +254,8 @@ msgstr "Créer" msgid "Create Shared Folder" msgstr "Créer un dossier partagé" -msgid "Create a sharing link" -msgstr "Créer un lien de partage" - -msgid "Create folder & Copy over" -msgstr "Créer un dossier et le copier" - -msgid "Create folder & Move over" -msgstr "Créer un dossier et le déplacer" - -msgid "Create link" -msgstr "Créer un lien" +msgid "Create a new shared folder" +msgstr "Créer un nouveau dossier partagé" msgid "Create your public username in <0>Settings!" msgstr "Créez votre nom d'utilisateur public dans <0>Paramètres !" @@ -263,8 +263,11 @@ msgstr "Créez votre nom d'utilisateur public dans <0>Paramètres !" msgid "Credit Card is expiring soon" msgstr "La carte de crédit va bientôt expirer" -msgid "Credit card saved" -msgstr "Carte de crédit enregistrée" +msgid "Crypto payments may take a few minutes to be processed. The subscription update will reflect on next login." +msgstr "" + +msgid "DAI, USDC, ETH or BTC" +msgstr "DAI, USDC, ETH ou BTC" msgid "Dark Theme" msgstr "Thème sombre" @@ -306,10 +309,10 @@ msgid "Device awaiting confirmation" msgstr "Appareil en attente de confirmation" msgid "Didn't receive the email ?" -msgstr "Vous n’avez pas reçu de courriel ?" +msgstr "Vous n’avez pas reçu de courriel ?" -msgid "Display Settings" -msgstr "Paramètres d’affichage" +msgid "Display" +msgstr "Afficher" msgid "Download" msgstr "Télécharger" @@ -365,11 +368,8 @@ msgstr "Entrez le code de vérification :" msgid "Failed to add payment method" msgstr "Échec de l'ajout d'une méthode de paiement" -msgid "Failed to change subscription" -msgstr "Échec de changement de souscription" - msgid "Failed to create a charge" -msgstr "" +msgstr "Échec de création de la charge" msgid "Failed to get signature" msgstr "Échec de l’obtention de la signature" @@ -377,6 +377,9 @@ msgstr "Échec de l’obtention de la signature" msgid "Failed to migrate account, please try again." msgstr "Échec de la migration du compte, veuillez réessayer." +msgid "Failed to update the subscription. Please try again later." +msgstr "Échec de la mise à jour de la souscription. Veuillez réessayer plus tard." + msgid "" "Failed to validate signature.\n" "If you are using a contract wallet, please make \n" @@ -404,12 +407,18 @@ msgstr "Taille" msgid "Files" msgstr "Fichiers" +msgid "Files Free" +msgstr "" + +msgid "Files Max" +msgstr "" + +msgid "Files Pro" +msgstr "" + msgid "Files sharing key" msgstr "Clé de partage des fichiers" -msgid "First name" -msgstr "Prénom" - msgid "Folder name is already in use" msgstr "Le nom du dossier est déjà utilisé" @@ -425,15 +434,15 @@ msgstr "Oublier ce navigateur" msgid "Free" msgstr "Gratuit" -msgid "Free plan" -msgstr "Formule gratuite" - msgid "General" msgstr "Général" msgid "Generate backup secret phrase" msgstr "Créer une phrase de sauvegarde secrète" +msgid "Generate sharing link" +msgstr "Générer un lien de partage" + msgid "Generated" msgstr "Généré" @@ -443,15 +452,6 @@ msgstr "Génération…" msgid "Get Started" msgstr "Commencer" -msgid "Give edit permission to:" -msgstr "Donner l’autorisation de modifier à :" - -msgid "Give view-only permission to:" -msgstr "Donner l’accès en lecture seule à :" - -msgid "Go back" -msgstr "Retour" - msgid "Go back" msgstr "Retour" @@ -489,20 +489,20 @@ msgid "Insufficient balance" msgstr "Solde insuffisant" msgid "Invoice outstanding" -msgstr "" +msgstr "Facture non réglée" msgid "I’m done saving my backup secret phrase" msgstr "Phrase de sauvegarde secrète enregistrée" +msgid "Join our new limited-access subscription plans to upgrade to a plan with more storage." +msgstr "Inscrivez-vous à nos nouvelles offres d'abonnement à accès limité pour passer à une offre contenant plus de stockage." + msgid "Keep original files" msgstr "Conserver les fichiers originaux" msgid "Language" msgstr "Langue" -msgid "Last name" -msgstr "Nom" - msgid "Learn more" msgstr "En savoir plus" @@ -518,15 +518,15 @@ msgstr "C’est parti." msgid "Light Theme" msgstr "Thème clair" -msgid "Loading" -msgstr "Chargement" - msgid "Loading preview" msgstr "Chargement de l’aperçu" msgid "Loading your shared folders…" msgstr "Chargement de vos dossiers partagés…" +msgid "Loading..." +msgstr "Chargement..." + msgid "Looks like you’re signing in from a new browser. Please choose one of the following to continue:" msgstr "Il semble que vous vous connectiez à partir d’un nouveau navigateur. Veuillez choisir une des options suivantes pour continuer :" @@ -554,6 +554,9 @@ msgstr "Mes Fichiers" msgid "Name" msgstr "Nom" +msgid "Need more storage?" +msgstr "Plus d'espace de stockage ?" + msgid "New folder" msgstr "Nouveau dossier" @@ -563,6 +566,9 @@ msgstr "Nouveau nom de dossier partagé" msgid "Next" msgstr "Suivant" +msgid "Next payment" +msgstr "Prochain paiement" + msgid "Nice to see you again!" msgstr "Ravi de te revoir !" @@ -593,8 +599,11 @@ msgstr "Aucun fichier à afficher" msgid "No thanks" msgstr "Non merci" -msgid "No user found for this query." -msgstr "Aucun utilisateur n'a été trouvé pour cette requête." +msgid "No users found" +msgstr "Aucun utilisateur trouvé" + +msgid "Notifications" +msgstr "Notifications" msgid "Number of copies (Replication Factor)" msgstr "Nombre de copies (facteur de réplication)" @@ -602,14 +611,17 @@ msgstr "Nombre de copies (facteur de réplication)" msgid "OK" msgstr "OK" +msgid "Older notifications" +msgstr "Autre notifications" + +msgid "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." +msgstr "Une fois activé, il est attendu que votre compte émette un paiement dans les 60 prochaines minutes. Si aucun paiement n'est reçu, votre formule ne changera pas." + msgid "One sec, getting files ready…" msgstr "Les fichiers sont presque prêts…" -msgid "Only send {selectedCurrency} to this address" -msgstr "N'envoyer que des {selectedCurrency} à cette adresse" - -msgid "Only you can see this." -msgstr "Vous seul(e) pouvez voir ceci." +msgid "Only send the exact amount of {0} to this address" +msgstr "N'envoyez que la somme exacte de {0} à cette adresse" msgid "Oops! You need to pay for this month to upload more content." msgstr "Oups ! Vous devez payer pour ce mois-ci pour ajouter plus de contenu." @@ -617,12 +629,6 @@ msgstr "Oups ! Vous devez payer pour ce mois-ci pour ajouter plus de contenu." msgid "Operating system:" msgstr "Système d’exploitation :" -msgid "Or Create a new shared folder" -msgstr "Ou créer un nouveau dossier partagé" - -msgid "Or Use an existing shared folder" -msgstr "Ou utiliser un dossier partagé existant" - msgid "Or confirm by signing into your Files on any browser you’ve used before." msgstr "Ou accepte la requête de connexion depuis n’importe quel appareil ou navigateur utilisé auparavant." @@ -650,8 +656,8 @@ msgstr "Mot de passe :" msgid "Passwords must match" msgstr "Les mots de passes de correspondent pas" -msgid "Pay invoice" -msgstr "Payer la facture" +msgid "Pay now" +msgstr "Payer maintenant" msgid "Pay with Crypto" msgstr "Payer en cryptomonnaie" @@ -662,17 +668,23 @@ msgstr "Payer en cryptomonnaie" msgid "Pay with {0}" msgstr "Payer via {0}" -msgid "Payment and Subscriptions" -msgstr "Paiements et souscriptions" +msgid "Payment information" +msgstr "Informations de paiement" msgid "Payment method" msgstr "Méthode de paiement" +msgid "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." +msgstr "Les paiements sont finaux et non remboursables. Si vous souhaitez changer de formule, les prochains fonds seront crédités pour les futurs paiements." + msgid "Plan changed successfully" msgstr "Formule changée" +msgid "Plan price" +msgstr "Prix de la formule" + msgid "Please complete payment of the following outstanding invoices in order to avoid account suspension" -msgstr "" +msgstr "Veuillez régler les factures non réglées suivantes pour ne pas perdre l'accès à votre compte" msgid "Please enter a file name" msgstr "Veuillez entrer un nom de fichier" @@ -689,14 +701,11 @@ msgstr "Merci de donner un mot de passe" msgid "Please select a file to upload" msgstr "Merci de sélectionner un fichier à téléverser" -msgid "Premium plan" -msgstr "Formule premium" - msgid "Preview" msgstr "Aperçu" -msgid "Previous" -msgstr "Précédent" +msgid "Pricing details" +msgstr "Détails du prix" msgid "Privacy Policy" msgstr "Politique de confidentialité" @@ -704,14 +713,8 @@ msgstr "Politique de confidentialité" msgid "Proceed to payment" msgstr "Procéder au paiement" -msgid "Profile and Display" -msgstr "Profil et affichage" - -msgid "Profile settings" -msgstr "Paramètres du profil" - -msgid "Profile updated" -msgstr "Profile mis à jour" +msgid "Profile" +msgstr "Profil" msgid "Recover" msgstr "Récupérer" @@ -755,6 +758,9 @@ msgstr "Signaler un fichier" msgid "Report a bug" msgstr "Signaler une erreur" +msgid "Request access!" +msgstr "Demandez l'accès !" + msgid "Requested from" msgstr "Envoyé par" @@ -764,9 +770,6 @@ msgstr "Ressources" msgid "Restore with backup secret phrase" msgstr "Récupérer avec la phrase de sauvegarde secrète" -msgid "Save changes" -msgstr "Enregistrer" - msgid "Save this browser for next time?" msgstr "Enregistrer ce navigateur ?" @@ -797,6 +800,9 @@ msgstr "Sécurité" msgid "See payment info" msgstr "Voir les détails du paiement" +msgid "Select a cryptocurrency" +msgstr "Sélectionner une crypto-devise" + msgid "Select a wallet" msgstr "Sélectionner un wallet" @@ -845,9 +851,6 @@ msgstr "Partager la sélection" msgid "Shared" msgstr "Partagé" -msgid "Shared Folder Name" -msgstr "Nom du dossier partagé" - msgid "Shared folder name" msgstr "Nom du dossier partagé" @@ -872,9 +875,6 @@ msgstr "Se connecter avec un autre compte" msgid "Sign in/up to access the shared folder" msgstr "Se connecter/s'inscrire pour accéder au dossier partagé" -msgid "Sign me up!" -msgstr "S'inscrire !" - msgid "Sign-in methods" msgstr "Méthodes de connexion" @@ -899,21 +899,18 @@ msgstr "Un problème est survenu. Nous n’avons pas pu téléverser votre fichi msgid "Sort By:" msgstr "Trier par :" -msgid "Standard plan" -msgstr "Formule standard" - msgid "Start Upload" msgstr "Démarrer le téléversement" -msgid "Start a team" -msgstr "Créer une équipe" - msgid "Stored by miner" msgstr "Sauvegardé par le mineur" msgid "Subscription Plan" msgstr "Formule souscrite" +msgid "Subscription plan" +msgstr "Formule de souscription" + msgid "Switch Network" msgstr "Changer de réseau" @@ -926,9 +923,6 @@ msgstr "Passer à la formule gratuite" msgid "System maintenance is scheduled to start at {0}. The system will be unavailable." msgstr "Une maintenance du système est prévue pour démarrer à {0}. Le système sera indisponible." -msgid "Teams" -msgstr "Équipes" - msgid "Technical" msgstr "Technique" @@ -944,6 +938,12 @@ msgstr "Les fichiers sont déjà dans ce dossier" msgid "The link you typed in looks malformed. Please verify it." msgstr "Le lien que vous avez tapé semble malformé. Veuillez le vérifier." +msgid "The time to pay with crypto is up. Updating your plan..." +msgstr "Temps imparti pour payer en crypto dépassé. Mise à jour votre offre…" + +msgid "The transaction was declined. Please use a different card or try again." +msgstr "La transaction a été refusée. Veuillez utiliser une autre carte, ou réessayer." + msgid "The username is too long" msgstr "Le nom d'utilisateur est trop long" @@ -954,7 +954,7 @@ msgid "There are no notifications!" msgstr "Il n'y a aucune notification !" msgid "There was a problem creating a charge {error}" -msgstr "" +msgstr "Échec de la création de la charge {error}" msgid "There was an error authenticating" msgstr "Une erreur s’est produite lors de l’authentification" @@ -986,9 +986,6 @@ msgstr "Une erreur s'est produite lors de la définition du nom d'utilisateur." msgid "This card will become your default payment method" msgstr "Cette carte deviendra votre méthode de paiement par défaut" -msgid "This is the free product." -msgstr "Ceci est la version gratuite." - msgid "This link is marlformed. Please verify that you copy/pasted it correctly." msgstr "Ce lien est marlformé. Veuillez vérifier que vous l'avez copié/collé correctement." @@ -1004,6 +1001,9 @@ msgstr "Ce site web utilise des cookies" msgid "This website uses cookies that help the website function and track interactions for analytics purposes. You have the right to decline our use of cookies. For us to provide a customizable user experience to you, please click on the Accept button below.<0>Learn more" msgstr "Ce site web utilise des cookies qui l'aident à fonctionner et à suivre les interactions à des fins d'analyse. Vous avez le droit de refuser notre utilisation des cookies. Pour que nous puissions vous offrir une expérience utilisateur personnalisable, veuillez cliquer sur le bouton Accepter ci-dessous.<0>En savoir plus" +msgid "This week" +msgstr "Cette semaine" + msgid "Total" msgstr "Total" @@ -1046,18 +1046,21 @@ msgstr "Utilisez une méthode de connexion différente" msgid "Use a saved browser" msgstr "Utiliser un navigateur enregistré" +msgid "Use an existing shared folder" +msgstr "Utiliser un dossier partagé existant" + msgid "Use this card" msgstr "Utiliser cette carte" -msgid "User {0} is both a reader and writer" -msgstr "L'utilisateur {0} est dans les auteurs et lecteurs" - msgid "Username" msgstr "Nom d’utilisateur" msgid "Username set successfully" msgstr "Nom d’utilisateur défini avec succès" +msgid "Username, wallet address or ENS" +msgstr "Nom d'utilisateur, adresse du portefeuille, ou ENS" + msgid "Usernames are public and can't be changed after creation." msgstr "Les noms d’utilisateur sont publics et ne peuvent pas être modifiés après leur création." @@ -1076,6 +1079,9 @@ msgstr "Code de vérification envoyé !" msgid "Verifying the link..." msgstr "Vérification du lien…" +msgid "View PDF" +msgstr "Voir le PDF" + msgid "View folder" msgstr "Voir le dossier" @@ -1136,33 +1142,30 @@ msgstr "Vous avez été ajouté(e) au dossier partagé ({0}) : {1}" msgid "You will need to sign a message in your wallet to complete sign in." msgstr "Vous devrez signer un message avec votre wallet pour terminer la procédure connexion." -msgid "You would lose the following features:" -msgstr "Vous perdriez ces fonctionnalités :" - msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "Vous avez un paiement en retard. Tant qu'il n'a pas été réglé, l'accès à votre compte a été restreint" msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "Votre contenu dépasse la capacité de stockage de {planStorageCapacity} de cette formule." -msgid "Your plan" -msgstr "Votre formule" - msgid "Your recovery key can be used to restore your account in place of your backup secret phrase." msgstr "Votre clé de récupération vous permet de récupérer l'accès à votre compte à la place de la phrase secrète." msgid "can-edit" msgstr "peut-modifier" +msgid "expires" +msgstr "expire" + msgid "invoices here" msgstr "factures ici" -msgid "me" -msgstr "moi" - msgid "on" msgstr "le" +msgid "or" +msgstr "ou" + msgid "unknown" msgstr "inconnu" diff --git a/packages/files-ui/src/locales/no/messages.po b/packages/files-ui/src/locales/no/messages.po index 47b6d62f8b..751527e4dd 100644 --- a/packages/files-ui/src/locales/no/messages.po +++ b/packages/files-ui/src/locales/no/messages.po @@ -34,9 +34,6 @@ msgstr "" msgid "A backup secret phrase will be generated and used for your account.<0/>We do not store it and <1>it can only be displayed once. Save it somewhere safe!" msgstr "" -msgid "A better sharing experience is coming soon." -msgstr "" - msgid "A file with the same name already exists" msgstr "" @@ -55,7 +52,7 @@ msgstr "Konto" msgid "Account is restricted" msgstr "" -msgid "Active links" +msgid "Account visibility" msgstr "" msgid "Add Card" @@ -70,9 +67,6 @@ msgstr "Legg til et brukernavn" msgid "Add at least one more authentication method to protect your account. You’d only need any two to sign in to Files from any device." msgstr "" -msgid "Add by sharing address, username, wallet address or ENS" -msgstr "" - msgid "Add card" msgstr "" @@ -88,19 +82,31 @@ msgstr "" msgid "Adding you to the shared folder..." msgstr "" +msgid "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." +msgstr "" + msgid "All invoices" msgstr "" msgid "Allow lookup by sharing key, wallet address, username or ENS" msgstr "" -msgid "An error occurred:" +msgid "Amount added to credit" msgstr "" -msgid "Annual billing{0}" +msgid "Amount available from last bill" +msgstr "" + +msgid "Amount due" +msgstr "" + +msgid "Amount from credit" +msgstr "" + +msgid "An error occurred:" msgstr "" -msgid "Anyone with the link can:" +msgid "Annual billing{0}" msgstr "" msgid "Approve" @@ -139,6 +145,9 @@ msgstr "" msgid "By forgetting this browser, you will not be able to use its associated recovery key to sign-in." msgstr "" +msgid "By switching plan, you will loose access to:" +msgstr "" + msgid "CID (Content Identifier)" msgstr "" @@ -244,16 +253,7 @@ msgstr "Opprett" msgid "Create Shared Folder" msgstr "" -msgid "Create a sharing link" -msgstr "" - -msgid "Create folder & Copy over" -msgstr "" - -msgid "Create folder & Move over" -msgstr "" - -msgid "Create link" +msgid "Create a new shared folder" msgstr "" msgid "Create your public username in <0>Settings!" @@ -262,7 +262,10 @@ msgstr "" msgid "Credit Card is expiring soon" msgstr "" -msgid "Credit card saved" +msgid "Crypto payments may take a few minutes to be processed. The subscription update will reflect on next login." +msgstr "" + +msgid "DAI, USDC, ETH or BTC" msgstr "" msgid "Dark Theme" @@ -307,7 +310,7 @@ msgstr "" msgid "Didn't receive the email ?" msgstr "Fikk du ikke e-posten?" -msgid "Display Settings" +msgid "Display" msgstr "" msgid "Download" @@ -364,9 +367,6 @@ msgstr "Skriv inn bekreftelseskoden:" msgid "Failed to add payment method" msgstr "" -msgid "Failed to change subscription" -msgstr "" - msgid "Failed to create a charge" msgstr "" @@ -376,6 +376,9 @@ msgstr "" msgid "Failed to migrate account, please try again." msgstr "" +msgid "Failed to update the subscription. Please try again later." +msgstr "" + msgid "" "Failed to validate signature.\n" "If you are using a contract wallet, please make \n" @@ -400,11 +403,17 @@ msgstr "Filstørrelse" msgid "Files" msgstr "Filer" -msgid "Files sharing key" +msgid "Files Free" +msgstr "" + +msgid "Files Max" +msgstr "" + +msgid "Files Pro" msgstr "" -msgid "First name" -msgstr "Fornavn" +msgid "Files sharing key" +msgstr "" msgid "Folder name is already in use" msgstr "" @@ -421,15 +430,15 @@ msgstr "" msgid "Free" msgstr "" -msgid "Free plan" -msgstr "" - msgid "General" msgstr "" msgid "Generate backup secret phrase" msgstr "" +msgid "Generate sharing link" +msgstr "" + msgid "Generated" msgstr "" @@ -439,15 +448,6 @@ msgstr "" msgid "Get Started" msgstr "Begynn" -msgid "Give edit permission to:" -msgstr "" - -msgid "Give view-only permission to:" -msgstr "" - -msgid "Go back" -msgstr "" - msgid "Go back" msgstr "Tilbake" @@ -490,15 +490,15 @@ msgstr "" msgid "I’m done saving my backup secret phrase" msgstr "" +msgid "Join our new limited-access subscription plans to upgrade to a plan with more storage." +msgstr "" + msgid "Keep original files" msgstr "" msgid "Language" msgstr "Språk" -msgid "Last name" -msgstr "Etternavn" - msgid "Learn more" msgstr "Lær mer" @@ -514,15 +514,15 @@ msgstr "" msgid "Light Theme" msgstr "Lys drakt" -msgid "Loading" -msgstr "" - msgid "Loading preview" msgstr "" msgid "Loading your shared folders…" msgstr "" +msgid "Loading..." +msgstr "" + msgid "Looks like you’re signing in from a new browser. Please choose one of the following to continue:" msgstr "" @@ -550,6 +550,9 @@ msgstr "Mine filer" msgid "Name" msgstr "Navn" +msgid "Need more storage?" +msgstr "" + msgid "New folder" msgstr "Ny mappe" @@ -559,6 +562,9 @@ msgstr "" msgid "Next" msgstr "Neste" +msgid "Next payment" +msgstr "" + msgid "Nice to see you again!" msgstr "" @@ -589,7 +595,10 @@ msgstr "" msgid "No thanks" msgstr "Nei takk" -msgid "No user found for this query." +msgid "No users found" +msgstr "" + +msgid "Notifications" msgstr "" msgid "Number of copies (Replication Factor)" @@ -598,14 +607,17 @@ msgstr "" msgid "OK" msgstr "" -msgid "One sec, getting files ready…" +msgid "Older notifications" +msgstr "" + +msgid "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." msgstr "" -msgid "Only send {selectedCurrency} to this address" +msgid "One sec, getting files ready…" msgstr "" -msgid "Only you can see this." -msgstr "Kun du kan se dette." +msgid "Only send the exact amount of {0} to this address" +msgstr "" msgid "Oops! You need to pay for this month to upload more content." msgstr "" @@ -613,12 +625,6 @@ msgstr "" msgid "Operating system:" msgstr "Operativsystem:" -msgid "Or Create a new shared folder" -msgstr "" - -msgid "Or Use an existing shared folder" -msgstr "" - msgid "Or confirm by signing into your Files on any browser you’ve used before." msgstr "" @@ -646,7 +652,7 @@ msgstr "Passord:" msgid "Passwords must match" msgstr "Passordene må samsvare" -msgid "Pay invoice" +msgid "Pay now" msgstr "" msgid "Pay with Crypto" @@ -658,15 +664,21 @@ msgstr "" msgid "Pay with {0}" msgstr "" -msgid "Payment and Subscriptions" +msgid "Payment information" msgstr "" msgid "Payment method" msgstr "" +msgid "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." +msgstr "" + msgid "Plan changed successfully" msgstr "" +msgid "Plan price" +msgstr "" + msgid "Please complete payment of the following outstanding invoices in order to avoid account suspension" msgstr "" @@ -685,14 +697,11 @@ msgstr "Angi et passord" msgid "Please select a file to upload" msgstr "Velg en fil å laste opp" -msgid "Premium plan" -msgstr "" - msgid "Preview" msgstr "Forhåndsvis" -msgid "Previous" -msgstr "Forrige" +msgid "Pricing details" +msgstr "" msgid "Privacy Policy" msgstr "Personvernspraksis" @@ -700,14 +709,8 @@ msgstr "Personvernspraksis" msgid "Proceed to payment" msgstr "" -msgid "Profile and Display" -msgstr "Profil og visning" - -msgid "Profile settings" -msgstr "Profilinnstillinger" - -msgid "Profile updated" -msgstr "Profil oppdatert" +msgid "Profile" +msgstr "" msgid "Recover" msgstr "Gjenopprett" @@ -751,6 +754,9 @@ msgstr "" msgid "Report a bug" msgstr "" +msgid "Request access!" +msgstr "" + msgid "Requested from" msgstr "Forespurt fra" @@ -760,9 +766,6 @@ msgstr "Ressurser" msgid "Restore with backup secret phrase" msgstr "" -msgid "Save changes" -msgstr "Lagre endringer" - msgid "Save this browser for next time?" msgstr "" @@ -793,6 +796,9 @@ msgstr "" msgid "See payment info" msgstr "" +msgid "Select a cryptocurrency" +msgstr "" + msgid "Select a wallet" msgstr "" @@ -841,9 +847,6 @@ msgstr "" msgid "Shared" msgstr "Delt" -msgid "Shared Folder Name" -msgstr "" - msgid "Shared folder name" msgstr "" @@ -868,9 +871,6 @@ msgstr "Logg inn med en annen konto" msgid "Sign in/up to access the shared folder" msgstr "" -msgid "Sign me up!" -msgstr "" - msgid "Sign-in methods" msgstr "Innloggingsmetoder" @@ -895,21 +895,18 @@ msgstr "" msgid "Sort By:" msgstr "" -msgid "Standard plan" -msgstr "" - msgid "Start Upload" msgstr "" -msgid "Start a team" -msgstr "" - msgid "Stored by miner" msgstr "" msgid "Subscription Plan" msgstr "" +msgid "Subscription plan" +msgstr "" + msgid "Switch Network" msgstr "" @@ -922,9 +919,6 @@ msgstr "" msgid "System maintenance is scheduled to start at {0}. The system will be unavailable." msgstr "" -msgid "Teams" -msgstr "" - msgid "Technical" msgstr "Teknisk" @@ -940,6 +934,12 @@ msgstr "" msgid "The link you typed in looks malformed. Please verify it." msgstr "" +msgid "The time to pay with crypto is up. Updating your plan..." +msgstr "" + +msgid "The transaction was declined. Please use a different card or try again." +msgstr "" + msgid "The username is too long" msgstr "" @@ -982,9 +982,6 @@ msgstr "" msgid "This card will become your default payment method" msgstr "" -msgid "This is the free product." -msgstr "" - msgid "This link is marlformed. Please verify that you copy/pasted it correctly." msgstr "" @@ -1000,6 +997,9 @@ msgstr "" msgid "This website uses cookies that help the website function and track interactions for analytics purposes. You have the right to decline our use of cookies. For us to provide a customizable user experience to you, please click on the Accept button below.<0>Learn more" msgstr "" +msgid "This week" +msgstr "" + msgid "Total" msgstr "" @@ -1042,10 +1042,10 @@ msgstr "Bruk en annen innloggingsmetode" msgid "Use a saved browser" msgstr "" -msgid "Use this card" +msgid "Use an existing shared folder" msgstr "" -msgid "User {0} is both a reader and writer" +msgid "Use this card" msgstr "" msgid "Username" @@ -1054,6 +1054,9 @@ msgstr "Brukernavn" msgid "Username set successfully" msgstr "Brukernavn satt" +msgid "Username, wallet address or ENS" +msgstr "" + msgid "Usernames are public and can't be changed after creation." msgstr "" @@ -1072,6 +1075,9 @@ msgstr "" msgid "Verifying the link..." msgstr "" +msgid "View PDF" +msgstr "" + msgid "View folder" msgstr "Vis mappe" @@ -1132,33 +1138,30 @@ msgstr "" msgid "You will need to sign a message in your wallet to complete sign in." msgstr "" -msgid "You would lose the following features:" -msgstr "" - msgid "You've got a payment due. Until you've settled up, we've placed your account in restricted mode" msgstr "" msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." msgstr "" -msgid "Your plan" -msgstr "" - msgid "Your recovery key can be used to restore your account in place of your backup secret phrase." msgstr "" msgid "can-edit" msgstr "" -msgid "invoices here" +msgid "expires" msgstr "" -msgid "me" +msgid "invoices here" msgstr "" msgid "on" msgstr "" +msgid "or" +msgstr "" + msgid "unknown" msgstr "" diff --git a/packages/storage-ui/cypress/support/commands.ts b/packages/storage-ui/cypress/support/commands.ts index 74a8ab2ce8..9d7203a494 100644 --- a/packages/storage-ui/cypress/support/commands.ts +++ b/packages/storage-ui/cypress/support/commands.ts @@ -33,26 +33,31 @@ import { testPrivateKey, localHost } from "../fixtures/loginData" import { CustomizedBridge } from "./utils/CustomBridge" import "cypress-file-upload" import "cypress-pipe" +import { BucketType } from "@chainsafe/files-api-client" +import { navigationMenu } from "./page-objects/navigationMenu" export type Storage = Record[]; export interface Web3LoginOptions { url?: string - apiUrlBase?: string - saveBrowser?: boolean + withNewUser?: boolean clearPins?: boolean + deleteFpsBuckets?: boolean } -Cypress.Commands.add("clearPins", (apiUrlBase: string) => { - apiTestHelper.clearPins(apiUrlBase) +Cypress.Commands.add("clearPins", apiTestHelper.clearPins) + +Cypress.Commands.add("deleteBuckets", (type: BucketType | BucketType[]) => { + apiTestHelper.deleteBuckets(type) }) Cypress.Commands.add( "web3Login", ({ url = localHost, - apiUrlBase = "https://stage.imploy.site/api/v1", - clearPins = false + clearPins = false, + withNewUser = true, + deleteFpsBuckets = false }: Web3LoginOptions = {}) => { cy.on("window:before:load", (win) => { @@ -60,16 +65,18 @@ Cypress.Commands.add( "https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847", 4 ) - const signer = new Wallet(testPrivateKey, provider) + const signer = withNewUser + ? Wallet.createRandom() + : new Wallet(testPrivateKey, provider) // inject ethereum object in the global window Object.defineProperty(win, "ethereum", { - get: () => new CustomizedBridge(signer as any, provider as any) + get: () => new CustomizedBridge(signer as any, signer.address, provider as any) }) }) // with nothing in localstorage (and in session storage) // the whole login flow should kick in - cy.session("web3login", () => { + cy.session("web3loginNewUser", () => { cy.visit(url) authenticationPage.web3Button().click() authenticationPage.showMoreButton().click() @@ -82,12 +89,18 @@ Cypress.Commands.add( bucketsPage.bucketsHeaderLabel().should("be.visible") if(clearPins){ - cy.clearPins(apiUrlBase) + cy.clearPins() + navigationMenu.bucketsNavButton().click() + navigationMenu.cidsNavButton().click() + } + + if (deleteFpsBuckets) { + apiTestHelper.deleteBuckets("fps") } } ) -Cypress.Commands.add("safeClick", { prevSubject: "element" }, $element => { +Cypress.Commands.add("safeClick", { prevSubject: "element" }, ($element?: JQuery) => { const click = ($el: JQuery) => $el.trigger("click") return cy .wrap($element) @@ -105,16 +118,15 @@ declare global { /** * Login using Metamask to an instance of Storage. * @param {String} options.url - (default: "http://localhost:3000") - what url to visit. - * @param {String} apiUrlBase - (default: "https://stage.imploy.site/api/v1") - what url to call for the api. + * @param {Boolean} options.withNewUser - (default: true) - whether to create a new user for this session. + * @param {Boolean} options.clearCSFBucket - (default: false) - whether any file in the csf bucket should be deleted. */ - web3Login: (options?: Web3LoginOptions) => Chainable + web3Login: (options?: Web3LoginOptions) => void /** * Remove all "queued", "pinning", "pinned", "failed" pins - * @param {String} apiUrlBase - what url to call for the api. - * @example cy.clearPins("https://stage.imploy.site/api/v1") */ - clearPins: (apiUrlBase: string) => Chainable + clearPins: () => void /** * Use this when encountering race condition issues resulting in @@ -128,7 +140,15 @@ declare global { * https://github.com/cypress-io/cypress/issues/7306 * */ - safeClick: () => Chainable + safeClick: ($element?: JQuery) => Chainable + + /** + * Clear a bucket. + * @param {BucketType} - what bucket type to clear for this user. + * @example cy.deleteBuckets("fps") + * @example cy.deleteBuckets(["fps","csf"]) + */ + deleteBuckets: (type: BucketType | BucketType[]) => void } } } diff --git a/packages/storage-ui/cypress/support/index.ts b/packages/storage-ui/cypress/support/index.ts index cb8233c617..764ed85986 100644 --- a/packages/storage-ui/cypress/support/index.ts +++ b/packages/storage-ui/cypress/support/index.ts @@ -28,5 +28,16 @@ Cypress.on("uncaught:exception", (err) => { } }) -// Alternatively you can use CommonJS syntax: -// require('./commands') +// Hide fetch/XHR requests +// interim solution until cypress adds configuration support +// source https://gist.github.com/simenbrekken/3d2248f9e50c1143bf9dbe02e67f5399 +const app = window.top + +if(app != null && !app.document.head.querySelector("[data-hide-command-log-request]")) { + const style = app.document.createElement("style") + style.innerHTML = + ".command-name-request, .command-name-xhr { display: none }" + style.setAttribute("data-hide-command-log-request", "") + + app.document.head.appendChild(style) +} diff --git a/packages/storage-ui/cypress/support/page-objects/bucketsPage.ts b/packages/storage-ui/cypress/support/page-objects/bucketsPage.ts index 7d29141024..650a44b123 100644 --- a/packages/storage-ui/cypress/support/page-objects/bucketsPage.ts +++ b/packages/storage-ui/cypress/support/page-objects/bucketsPage.ts @@ -14,7 +14,7 @@ export const bucketsPage = { nameTableHeader: () => cy.get("[data-cy=table-header-name]"), sizeTableHeader: () => cy.get("[data-cy=table-header-size]"), bucketItemName: () => cy.get("[data-cy=cell-bucket-name]"), - bucketRowKebabButton: () => cy.get("[data-testid=dropdown-title-bucket-kebab]"), + bucketRowKebabButton: () => cy.get("[data-testid=dropdown-title-bucket-kebab]", { timeout: 10000 }), // create bucket modal elements createBucketForm: () => cy.get("[data-testid=form-create-bucket]", { timeout: 10000 }), diff --git a/packages/storage-ui/cypress/support/utils/CustomBridge.ts b/packages/storage-ui/cypress/support/utils/CustomBridge.ts index 4924ecbbff..ff1fd4c19c 100644 --- a/packages/storage-ui/cypress/support/utils/CustomBridge.ts +++ b/packages/storage-ui/cypress/support/utils/CustomBridge.ts @@ -1,8 +1,15 @@ import { Eip1193Bridge } from "@ethersproject/experimental/lib/eip1193-bridge" +import { ethers } from "ethers" import { toUtf8String } from "ethers/lib/utils" -import { testAddress } from "../../fixtures/loginData" export class CustomizedBridge extends Eip1193Bridge { + expectedAddress = "" + + constructor(signer: ethers.Signer, address: string, provider?: ethers.providers.Provider) { + super(signer as any, provider as any) + this.expectedAddress = address + } + async sendAsync(...args: Array) { return this.send(...args) } @@ -30,17 +37,17 @@ export class CustomizedBridge extends Eip1193Bridge { const message = params[0] if ( - (addr as string).toLowerCase() !== testAddress.toLowerCase() + (addr as string).toLowerCase() !== this.expectedAddress.toLowerCase() ) { return Promise.reject( - `Wrong address, expected ${testAddress}, but got ${addr}` + `Wrong address, expected ${this.expectedAddress}, but got ${addr}` ) } try { const sig = await this.signer.signMessage(toUtf8String(message)) return sig - } catch (e) { + } catch (e: any) { return Promise.reject( `Error in CustomizedBridge for personal_sign: ${e.message}` ) @@ -49,9 +56,9 @@ export class CustomizedBridge extends Eip1193Bridge { if (method === "eth_requestAccounts" || method === "eth_accounts") { if (isCallbackForm) { - callback({ result: [testAddress] }) + callback({ result: [this.expectedAddress] }) } else { - return Promise.resolve([testAddress]) + return Promise.resolve([this.expectedAddress]) } } diff --git a/packages/storage-ui/cypress/support/utils/apiTestHelper.ts b/packages/storage-ui/cypress/support/utils/apiTestHelper.ts index a227ebd030..a7762eab81 100644 --- a/packages/storage-ui/cypress/support/utils/apiTestHelper.ts +++ b/packages/storage-ui/cypress/support/utils/apiTestHelper.ts @@ -1,15 +1,22 @@ import axios from "axios" import { FilesApiClient } from "@chainsafe/files-api-client" +import { BucketType } from "@chainsafe/files-api-client" const REFRESH_TOKEN_KEY = "css.refreshToken" +const API_BASE_URL = "https://stage.imploy.site/api/v1" + +const getApiClient = () => { + // Disable the internal Axios JSON deserialization as this is handled by the client + const axiosInstance = axios.create({ transformResponse: [] }) + const apiClient = new FilesApiClient({}, API_BASE_URL, axiosInstance) + + return apiClient +} export const apiTestHelper = { - clearPins(apiUrlBase: string) { - const axiosInstance = axios.create({ - // Disable the internal Axios JSON de serialization as this is handled by the client - transformResponse: [] - }) - const apiClient = new FilesApiClient({}, apiUrlBase, axiosInstance) + clearPins() { + const apiClient = getApiClient() + cy.window().then((win) => { apiClient .getRefreshToken({ @@ -17,10 +24,31 @@ export const apiTestHelper = { }) .then((tokens) => { apiClient.setToken(tokens.access_token.token) - apiClient.listPins(undefined, undefined, ["queued", "pinning", "pinned", "failed"]) - .then((pins) => + // The ones in "queued" and "pinning" status can't be deleted + apiClient.listPins(undefined, undefined, ["pinned", "failed"]) + .then((pins) => pins.results?.forEach(ps => apiClient.deletePin(ps.requestid) - )) + )) + }) + }) + }, + deleteBuckets(type: BucketType | BucketType[]) { + const apiClient = getApiClient() + const typeToDelete = Array.isArray(type) ? type : [type] + + return new Cypress.Promise(async (resolve) => { + cy.window() + .then(async (win) => { + const tokens = await apiClient.getRefreshToken({ refresh: win.localStorage.getItem(REFRESH_TOKEN_KEY) || "" }) + + await apiClient.setToken(tokens.access_token.token) + const buckets = await apiClient.listBuckets(typeToDelete) + buckets.forEach(async (bucket) => { + cy.log(`Deleting fps bucket: "${bucket.name}""`) + await apiClient.removeBucket(bucket.id) + }) + cy.log("Done deleting fps buckets.") + resolve() }) }) } diff --git a/packages/storage-ui/cypress/tests/bucket-management-spec.ts b/packages/storage-ui/cypress/tests/bucket-management-spec.ts index 987d5a32a8..e72fa7ad98 100644 --- a/packages/storage-ui/cypress/tests/bucket-management-spec.ts +++ b/packages/storage-ui/cypress/tests/bucket-management-spec.ts @@ -7,8 +7,8 @@ describe("Bucket management", () => { context("desktop", () => { - it.skip("can create a bucket", () => { - cy.web3Login({ clearPins: true }) + it("can create a bucket", () => { + cy.web3Login({ clearPins: true, deleteFpsBuckets: true }) // create a bucket and see it in the bucket table navigationMenu.bucketsNavButton().click() @@ -24,16 +24,17 @@ describe("Bucket management", () => { bucketsPage.createBucketForm().should("not.exist") }) - it.skip("can delete a bucket", () => { - cy.web3Login({ clearPins: true }) + it("can delete a bucket", () => { + cy.web3Login({ clearPins: true, deleteFpsBuckets: true }) - // delete a bucket and ensure it's row is removed + // delete a bucket and ensure its row is removed navigationMenu.bucketsNavButton().click() - bucketsPage.createBucket(bucketName) + // creating a bucket with a unique name + bucketsPage.createBucket(`${bucketName}_${Date.now()}`) bucketsPage.bucketRowKebabButton().first().click() bucketsPage.deleteBucketMenuOption().first().click() - bucketsPage.bucketItemRow().should("not.be.visible") - bucketsPage.bucketItemName().should("not.be.visible") + bucketsPage.bucketItemRow().should("not.exist") + bucketsPage.bucketItemName().should("not.exist") }) }) }) diff --git a/packages/storage-ui/cypress/tests/cid-management-spec.ts b/packages/storage-ui/cypress/tests/cid-management-spec.ts index 5c8dccc369..9247d9911b 100644 --- a/packages/storage-ui/cypress/tests/cid-management-spec.ts +++ b/packages/storage-ui/cypress/tests/cid-management-spec.ts @@ -6,7 +6,7 @@ describe("CID management", () => { context("desktop", () => { - it.skip("can pin a CID", () => { + it("can pin a CID", () => { cy.web3Login({ clearPins: true }) navigationMenu.cidsNavButton().click() @@ -22,6 +22,8 @@ describe("CID management", () => { cidsPage.pinCidForm().should("not.exist") }) + // this is unreliable since the pin from the previous + // test is still in the "queued" state while being unpinned. it.skip("can unpin a cid", () => { cy.web3Login({ clearPins: true }) @@ -29,7 +31,7 @@ describe("CID management", () => { cidsPage.addPinnedCid() cidsPage.cidRowKebabButton().click() cidsPage.unpinMenuOption().click() - cidsPage.cidItemRow().should("not.exist") + cidsPage.cidItemRow().should("contain.text", "queued") }) }) }) \ No newline at end of file diff --git a/packages/storage-ui/package.json b/packages/storage-ui/package.json index fdcb40ec2b..0246fb4fd5 100644 --- a/packages/storage-ui/package.json +++ b/packages/storage-ui/package.json @@ -6,7 +6,7 @@ "@babel/core": "^7.12.10", "@babel/runtime": "^7.0.0", "@chainsafe/browser-storage-hooks": "^1.0.1", - "@chainsafe/files-api-client": "1.18.25", + "@chainsafe/files-api-client": "1.18.29", "@chainsafe/web3-context": "1.3.0", "@lingui/core": "^3.7.2", "@lingui/react": "^3.7.2", diff --git a/packages/storage-ui/src/App.tsx b/packages/storage-ui/src/App.tsx index cd1eeb9672..7998d74a44 100644 --- a/packages/storage-ui/src/App.tsx +++ b/packages/storage-ui/src/App.tsx @@ -14,6 +14,7 @@ import { useLocalStorage } from "@chainsafe/browser-storage-hooks" import { StorageApiProvider } from "./Contexts/StorageApiContext" import { StorageProvider } from "./Contexts/StorageContext" import { UserProvider } from "./Contexts/UserContext" +import { BillingProvider } from "./Contexts/BillingContext" if ( process.env.NODE_ENV === "production" && @@ -117,11 +118,13 @@ const App = () => { > - - - - - + + + + + + + diff --git a/packages/storage-ui/src/Components/CustomButton.tsx b/packages/storage-ui/src/Components/CustomButton.tsx new file mode 100644 index 0000000000..3d9de50746 --- /dev/null +++ b/packages/storage-ui/src/Components/CustomButton.tsx @@ -0,0 +1,54 @@ +import { Button, IButtonProps } from "@chainsafe/common-components" +import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" +import React, { ReactNode } from "react" +import clsx from "clsx" + +const useStyles = makeStyles(({ palette }: ITheme) => + createStyles({ + root: { + "&.gray": { + backgroundColor: palette.additional["gray"][3], + color: palette.additional["gray"][9] + } + } + }) +) + +const CUSTOM_VARIANTS = ["gray"] + +const temp = [...CUSTOM_VARIANTS] +type customVariant = typeof temp[0] + +type buttonVariant = IButtonProps["variant"] | customVariant + +interface ICustomButton extends Omit { + children: ReactNode + variant?: buttonVariant + className?: string +} + +const CustomButton: React.FC = ({ + className, + children, + variant, + ...rest +}: ICustomButton) => { + const classes = useStyles() + + const setVariant = + variant && CUSTOM_VARIANTS.includes(variant) + ? "primary" + : variant || "primary" + + return ( + + ) +} + +export default CustomButton diff --git a/packages/storage-ui/src/Components/Elements/InvoiceLines.tsx b/packages/storage-ui/src/Components/Elements/InvoiceLines.tsx new file mode 100644 index 0000000000..7de0249601 --- /dev/null +++ b/packages/storage-ui/src/Components/Elements/InvoiceLines.tsx @@ -0,0 +1,165 @@ +import React, { useMemo } from "react" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { CSSTheme } from "../../Themes/types" +import { Typography, Loading, Button } from "@chainsafe/common-components" +import { Trans } from "@lingui/macro" +import dayjs from "dayjs" +import { useBilling } from "../../Contexts/BillingContext" +import clsx from "clsx" + +const useStyles = makeStyles( + ({ constants, breakpoints, palette, typography }: CSSTheme) => + createStyles({ + heading: { + fontSize: 16, + marginBottom: constants.generalUnit * 4, + marginTop: constants.generalUnit * 2, + [breakpoints.down("md")]: { + marginBottom: constants.generalUnit * 2 + } + }, + loader: { + marginTop: constants.generalUnit + }, + centered: { + textAlign: "center" + }, + root: { + [breakpoints.down("md")]: { + padding: `0 ${constants.generalUnit}px` + } + }, + invoiceLine: { + width: "100%", + backgroundColor: palette.additional["gray"][4], + color: palette.additional["gray"][9], + padding: constants.generalUnit * 1.5, + borderRadius: 10, + marginTop: constants.generalUnit * 1.5, + "& > div": { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + "& > *": { + width: "100%", + lineHeight: "16px", + fontWeight: typography.fontWeight.regular, + marginLeft: constants.generalUnit, + marginRight: constants.generalUnit + }, + "& > button": { + maxWidth: 100 + } + } + }, + unpaidInvoice: { + border: `1px solid ${palette.additional["volcano"][7]}`, + color: palette.additional["volcano"][7] + }, + price: { + fontWeight: "bold !important" as "bold", + fontSize: 16 + }, + link: { + color: palette.primary.main, + paddingRight: "0px !important", + fontSize: 16 + }, + text: { + fontSize: 16 + } + }) +) + +interface IInvoiceProps { + lineNumber?: number + payInvoice: (invoiceId: string) => void +} + +const InvoiceLines = ({ lineNumber, payInvoice }: IInvoiceProps) => { + const classes = useStyles() + const { invoices, downloadInvoice } = useBilling() + const invoicesToShow = useMemo(() => { + if (!invoices) return + + return lineNumber + ? invoices.slice(0, lineNumber) + : invoices + }, [invoices, lineNumber]) + + return ( + <> + {!invoicesToShow && ( +
+ +
+ )} + {invoicesToShow && !invoicesToShow.length && ( +
+ + No invoice found + +
+ )} + {!!invoicesToShow?.length && ( + invoicesToShow.map(({ amount, currency, uuid, period_start, status, product }) => +
+
+ + {product.name} {product.price.recurring.interval_count} {product.price.recurring.interval} + + + {dayjs.unix(period_start).format("MMM D, YYYY")} + + + {amount.toFixed(2)} {currency.toUpperCase()} + + {(status === "paid") && ( + + )} + {(status === "open") && ( + + )} +
+
+ ) + )} + + ) +} + +export default InvoiceLines \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/AddCard/AddCardModal.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/AddCard/AddCardModal.tsx new file mode 100644 index 0000000000..c27f2328f8 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/AddCard/AddCardModal.tsx @@ -0,0 +1,85 @@ +import React, { useMemo } from "react" +import { createStyles, makeStyles } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import CustomModal from "../../../Elements/CustomModal" +import { t, Trans } from "@lingui/macro" +import AddCard from "../Common/AddCard" +import { Typography } from "@chainsafe/common-components" +import { useBilling } from "../../../../Contexts/BillingContext" + +const useStyles = makeStyles( + ({ breakpoints, constants, zIndex, typography }: CSSTheme) => { + return createStyles({ + root: { + padding: constants.generalUnit * 4 + }, + modalRoot: { + zIndex: zIndex?.blocker, + [breakpoints.down("md")]: {} + }, + modalInner: { + backgroundColor: constants.createFolder.backgroundColor, + color: constants.createFolder.color, + width: "100%", + [breakpoints.down("md")]: { + maxWidth: `${breakpoints.width("md")}px !important` + } + }, + heading: { + color: constants.createFolder.color, + fontWeight: typography.fontWeight.semibold, + textAlign: "left", + marginBottom: constants.generalUnit * 3 + }, + footer: { + marginTop: constants.generalUnit * 4 + } + }) + } +) + +interface IAddCardModalProps { + isModalOpen: boolean + onClose: () => void +} + +const AddCardModal = ({ isModalOpen, onClose }: IAddCardModalProps) => { + const classes = useStyles() + const { defaultCard } = useBilling() + const isUpdate = useMemo(() => !!defaultCard, [defaultCard]) + + return ( + +
+ + {isUpdate + ? Update your credit card + : Add a credit card + } + + +
+
+ ) +} + +export default AddCardModal diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/BillingHistory.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/BillingHistory.tsx new file mode 100644 index 0000000000..70d3cb526e --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/BillingHistory.tsx @@ -0,0 +1,76 @@ +import React, { useState } from "react" +import { Link, Typography } from "@chainsafe/common-components" +import { makeStyles, ITheme, createStyles } from "@chainsafe/common-theme" +import { Trans } from "@lingui/macro" +import InvoiceLines from "../../Elements/InvoiceLines" +import PayInvoiceModal from "./PayInvoice/PayInvoiceModal" +import { useBilling } from "../../../Contexts/BillingContext" +import { ROUTE_LINKS } from "../../StorageRoutes" + +const useStyles = makeStyles(({ constants }: ITheme) => + createStyles({ + container: { + padding: `${constants.generalUnit}px 0`, + margin: `${constants.generalUnit * 1.5}px 0` + }, + link: { + textAlign: "right", + fontSize: 16 + }, + spaceBetweenBox: { + display: "flex", + justifyContent: "space-between", + alignItems: "center" + }, + billingText: { + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit * 2 + } + }) +) + +const BillingHistory = () => { + const classes = useStyles() + const [invoiceToPay, setInvoiceToPay] = useState() + const { isPendingInvoice, openInvoice } = useBilling() + + return ( +
+
+ + Billing history + + + + All invoices + + +
+ {(isPendingInvoice || openInvoice) && + + Please complete payment of the following outstanding invoices in order to avoid account suspension + } + setInvoiceToPay(invoiceId)} + /> + {invoiceToPay && setInvoiceToPay(undefined)} + />} +
+ ) +} + +export default BillingHistory diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/ChangePlanModal.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/ChangePlanModal.tsx new file mode 100644 index 0000000000..204a9f8994 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/ChangePlanModal.tsx @@ -0,0 +1,223 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react" +import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import { Modal } from "@chainsafe/common-components" +import SelectPlan from "./SelectPlan" +import PlanDetails from "./PlanDetails" +import PaymentMethodSelector from "./PaymentMethodSelector" +import ConfirmPlan from "../Common/ConfirmPlan" +import { useBilling } from "../../../../Contexts/BillingContext" +import { Product, ProductPrice, ProductPriceRecurringInterval } from "@chainsafe/files-api-client" +import PlanSuccess from "./PlanSuccess" +import DowngradeDetails from "./DowngradeDetails" +import { PaymentMethod } from "../../../../Contexts/BillingContext" +import CryptoPayment from "../Common/CryptoPayment" +import { formatSubscriptionError } from "../utils/formatSubscriptionError" + +const useStyles = makeStyles(({ constants, breakpoints, palette }: CSSTheme) => + createStyles({ + root: { + "&:before": { + backgroundColor: constants.modalDefault.fadeBackground + } + }, + inner: { + borderRadius: `${constants.generalUnit / 2}px`, + [breakpoints.up("sm")]: { + minWidth: 480 + }, + [breakpoints.down("sm")]: { + width: "100%" + } + }, + warningText: { + marginTop: constants.generalUnit * 3, + maxWidth: constants.generalUnit * 56, + color: palette.additional["gray"][7] + }, + icon : { + verticalAlign: "middle", + "& > svg": { + fill: palette.additional["gray"][7], + height: constants.generalUnit * 2.25 + } + } + }) +) + +type ChangeModalSlides = "select" | +"planDetails" | +"paymentMethod" | +"confirmPlan" | +"planSuccess" | +"downgradeDetails" | +"cryptoPayment" + +const getPrice = (plan: Product, recurrence?: ProductPriceRecurringInterval) => { + return plan.prices.find(price => price?.recurring?.interval === recurrence)?.unit_amount || 0 +} + +interface IChangeProductModal { + onClose: () => void +} + +const ChangeProductModal = ({ onClose }: IChangeProductModal) => { + const classes = useStyles() + const { desktop } = useThemeSwitcher() + const { getAvailablePlans, changeSubscription, currentSubscription, isPendingInvoice } = useBilling() + const [selectedPlan, setSelectedPlan] = useState() + const [selectedPrice, setSelectedPrice] = useState() + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() + const [slide, setSlide] = useState() + const [plans, setPlans] = useState() + const [isLoadingChangeSubscription, setIsLoadingChangeSubscription] = useState(false) + const [subscriptionErrorMessage, setSubscriptionErrorMessage] = useState() + const didSelectFreePlan = useMemo(() => !!selectedPlan && getPrice(selectedPlan, "month") === 0, [selectedPlan]) + const monthlyPrice = useMemo(() => selectedPlan?.prices.find((price) => price.recurring.interval === "month"), [selectedPlan]) + const yearlyPrice = useMemo(() => selectedPlan?.prices.find((price) => price.recurring.interval === "year"), [selectedPlan]) + const [billingPeriod, setBillingPeriod] = useState() + + useEffect(() => { + if(selectedPlan && !billingPeriod){ + setBillingPeriod(monthlyPrice ? "month" : "year") + } + }, [billingPeriod, monthlyPrice, selectedPlan]) + + useEffect(() => { + if(!slide){ + setSlide(isPendingInvoice + ? "cryptoPayment" + : "select" + ) + } + }, [isPendingInvoice, slide]) + + useEffect(() => { + if(!plans) { + getAvailablePlans() + .then((plans) => setPlans(plans)) + .catch(console.error) + } + }, [getAvailablePlans, plans]) + + const handleChangeSubscription = useCallback(() => { + if (selectedPrice) { + setIsLoadingChangeSubscription(true) + setSubscriptionErrorMessage(undefined) + changeSubscription(selectedPrice.id) + .then(() => { + setSlide("planSuccess") + }) + .catch((e) => { + const errorMessage = formatSubscriptionError(e) + setSubscriptionErrorMessage(errorMessage) + }) + .finally(() => setIsLoadingChangeSubscription(false)) + } + }, [changeSubscription, selectedPrice]) + + const onSelectPlanPrice = useCallback(() => { + if(billingPeriod === "month" && monthlyPrice) { + setSelectedPrice(monthlyPrice) + } else if (yearlyPrice) { + setSelectedPrice(yearlyPrice) + } + setSlide("paymentMethod") + }, [billingPeriod, monthlyPrice, yearlyPrice]) + + const onSelectPlan = useCallback((plan: Product) => { + setSelectedPlan(plan) + const currentPrice = currentSubscription?.product?.price?.unit_amount + const currentRecurrence = currentSubscription?.product.price.recurring.interval + const newPrice = getPrice(plan, currentRecurrence) + const isDowngrade = (currentPrice || 0) > newPrice + + isDowngrade + ? setSlide("downgradeDetails") + : setSlide("planDetails") + }, [currentSubscription]) + + return ( + + {slide === "select" && ( + + )} + { slide === "downgradeDetails" && selectedPlan && ( + {setSlide("select")}} + goToPlanDetails={() => setSlide("planDetails")} + shouldCancelPlan={didSelectFreePlan} + plan={selectedPlan} + onClose={onClose} + /> + )} + {slide === "planDetails" && selectedPlan && billingPeriod && ( + { + setBillingPeriod(undefined) + setSlide("select") + }} + onSelectPlanPrice={onSelectPlanPrice} + onChangeBillingPeriod={setBillingPeriod} + billingPeriod={billingPeriod} + monthlyPrice={monthlyPrice} + yearlyPrice={yearlyPrice} + /> + )} + {slide === "paymentMethod" && selectedPrice && + setSlide("planDetails")} + onSelectPaymentMethod={(paymentMethod) => { + setSelectedPaymentMethod(paymentMethod) + setSlide("confirmPlan") + }} + /> + } + {slide === "confirmPlan" && selectedPlan && selectedPrice && selectedPaymentMethod && + { + setSubscriptionErrorMessage(undefined) + setSlide("select")} + } + goToPaymentMethod={() => { + setSubscriptionErrorMessage(undefined) + setSlide("paymentMethod") + }} + loadingChangeSubscription={isLoadingChangeSubscription} + onChangeSubscription={selectedPaymentMethod === "creditCard" ? handleChangeSubscription : () => setSlide("cryptoPayment")} + subscriptionErrorMessage={subscriptionErrorMessage} + paymentMethod={selectedPaymentMethod} + />} + {slide === "cryptoPayment" && } + {slide === "planSuccess" && selectedPlan && selectedPrice && } + + ) +} + +export default ChangeProductModal \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/DowngradeDetails.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/DowngradeDetails.tsx new file mode 100644 index 0000000000..ec72b950f4 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/DowngradeDetails.tsx @@ -0,0 +1,239 @@ +import React, { useCallback, useState } from "react" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import { Product } from "@chainsafe/files-api-client" +import { Button, CrossIcon, formatBytes, InfoCircleIcon, Typography } from "@chainsafe/common-components" +import { Trans } from "@lingui/macro" +import clsx from "clsx" +import { useBilling } from "../../../../Contexts/BillingContext" + +const useStyles = makeStyles(({ constants, palette }: CSSTheme) => + createStyles({ + root: { + position: "relative", + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` + }, + header: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginTop: constants.generalUnit * 2 + }, + headingBadge: { + color: palette.additional["gray"][7], + marginTop: constants.generalUnit * 3 + }, + headingBox: { + marginTop: constants.generalUnit * 2, + marginBottom: constants.generalUnit * 2 + }, + rowBox: { + display: "flex", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + middleRowBox: { + display: "flex", + alignItems: "center" + }, + featuresTitle: { + marginTop: constants.generalUnit * 2, + marginBottom: constants.generalUnit * 2 + }, + pushRightBox: { + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + marginLeft: constants.generalUnit * 4 + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + } + }, + bottomSection: { + display: "flex", + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + margin: `${constants.generalUnit * 3}px 0px` + }, + divider: { + margin: `${constants.generalUnit}px 0` + }, + featuresBox: { + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit * 2 + }, + featureTickBox: { + marginBottom: constants.generalUnit + }, + textLink: { + color: palette.primary.main + }, + checkCircleIcon: { + fill: palette.additional["gray"][7], + marginLeft: constants.generalUnit + }, + crossIcon: { + fill: palette.error.main, + fontSize: constants.generalUnit * 2, + marginRight: constants.generalUnit + }, + invoiceText: { + marginTop: constants.generalUnit * 3, + marginBottom: constants.generalUnit + }, + warningText: { + marginTop: constants.generalUnit * 3, + maxWidth: constants.generalUnit * 56, + color: palette.additional["gray"][7] + }, + icon : { + verticalAlign: "middle", + "& > svg": { + fill: palette.additional["gray"][7], + height: constants.generalUnit * 2.25 + } + } + }) +) + +interface IConfirmDowngrade { + plan: Product + onClose: () => void + goBack: () => void + goToPlanDetails: () => void + shouldCancelPlan: boolean +} + +const DowngradeDetails = ({ + plan, + goBack, + goToPlanDetails, + shouldCancelPlan, + onClose +}: IConfirmDowngrade) => { + const classes = useStyles() + const { currentSubscription, cancelCurrentSubscription, invoices } = useBilling() + const currentStorage = formatBytes(Number(currentSubscription?.product?.price.metadata?.storage_size_bytes), 2) + const [isCancelingPlan, setIsCancellingPlan] = useState(false) + const lastInvoicePaymentMethod = invoices && invoices[invoices.length - 1].payment_method + + const onCancelPlan = useCallback(() => { + setIsCancellingPlan(true) + cancelCurrentSubscription() + .catch(console.error) + .finally(() => { + setIsCancellingPlan(true) + onClose() + }) + }, [cancelCurrentSubscription, onClose]) + + if (!currentSubscription) + return null + + return ( +
+
+ + Change plan + +
+
+ + By switching plan, you will loose access to: + +
+
+ + + {currentStorage + ? {currentStorage} of storage + : plan.description + } + +
+
+ + + {currentSubscription?.product.description} + +
+
+ + + {lastInvoicePaymentMethod === "crypto" + ? + All crypto payments are final and ineligible for credits, + exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed. + + : + Payments are final and non-refundable. If you wish to change your plan, + any extra funds will be applied as credit towards future payments. + + } + +
+
+
+ + { + shouldCancelPlan + ? + : + } +
+
+
+ ) +} + +export default DowngradeDetails \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx new file mode 100644 index 0000000000..c74b4db895 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PaymentMethodSelector.tsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState } from "react" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import { Button, Divider, RadioInput, Typography } from "@chainsafe/common-components" +import { t, Trans } from "@lingui/macro" +import AddCard from "../Common/AddCard" +import { PaymentMethod, useBilling } from "../../../../Contexts/BillingContext" +import { ProductPrice } from "@chainsafe/files-api-client" + +const useStyles = makeStyles(({ constants, palette }: CSSTheme) => + createStyles({ + root: { + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` + }, + heading: { + marginTop: constants.generalUnit * 3, + marginBottom: constants.generalUnit * 2 + }, + subHeading: { + marginBottom: constants.generalUnit * 2, + color: palette.additional["gray"][8] + }, + boldText: { + fontWeight: "bold" + }, + normalWeightText: { + fontWeight: "normal" + }, + rowBox: { + display: "flex", + justifyContent: "space-between" + }, + pushRightBox: { + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + flex: 1 + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + } + }, + bottomSection: { + display: "flex", + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + marginTop: constants.generalUnit * 4, + marginBottom: constants.generalUnit * 3 + }, + divider: { + margin: `${constants.generalUnit}px 0` + }, + radioLabel: { + fontSize: 14 + }, + textButton: { + color: palette.primary.main, + cursor: "pointer" + }, + linkButton: { + textDecoration: "underline", + cursor: "pointer" + }, + addCardWrapper: { + padding: `${constants.generalUnit * 2}px 0px` + }, + footer: { + marginTop: constants.generalUnit * 2 + } + }) +) + +interface IPaymentMethodProps { + selectedProductPrice: ProductPrice + onClose: () => void + goBack: () => void + onSelectPaymentMethod: (paymentMethod: PaymentMethod) => void +} + +const PaymentMethodSelector = ({ selectedProductPrice, goBack, onSelectPaymentMethod }: IPaymentMethodProps) => { + const classes = useStyles() + const [paymentMethod, setPaymentMethod] = useState<"creditCard" | "crypto" | undefined>() + const [isCardFormOpen, setIsCardFormOpen] = useState(false) + const { defaultCard } = useBilling() + + useEffect(() => { + if (defaultCard) { + setPaymentMethod("creditCard") + } + }, [defaultCard]) + + return ( +
+ + Select payment method + + + {isCardFormOpen && This card will become your default payment method} + + + {!isCardFormOpen && <> +
+ setPaymentMethod("creditCard")} + checked={paymentMethod === "creditCard"} + labelClassName={classes.radioLabel} + disabled={!defaultCard} + testId="credit-card" + /> + setIsCardFormOpen(true)} + data-cy={defaultCard ? "text-button-update-card" : "text-button-add-card"} + > + {defaultCard + ? Update credit card + : Add credit card + } + +
+ + setPaymentMethod("crypto")} + checked={paymentMethod === "crypto"} + labelClassName={classes.radioLabel} + disabled={selectedProductPrice.recurring.interval !== "year"} + testId="crypto" + /> + + } + {isCardFormOpen &&
+ setIsCardFormOpen(false)} + goBack={() => setIsCardFormOpen(false)} + /> +
+ } + {!isCardFormOpen &&
+
+ + +
+
} +
+ ) +} + +export default PaymentMethodSelector \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PlanDetails.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PlanDetails.tsx new file mode 100644 index 0000000000..6f44be24ce --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PlanDetails.tsx @@ -0,0 +1,223 @@ +import React, { useMemo } from "react" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import { Product, ProductPrice, ProductPriceRecurringInterval } from "@chainsafe/files-api-client" +import { Button, Divider, formatBytes, ToggleSwitch, Typography } from "@chainsafe/common-components" +import { t, Trans } from "@lingui/macro" +import dayjs from "dayjs" + +const useStyles = makeStyles(({ constants }: CSSTheme) => + createStyles({ + root: { + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` + }, + heading: { + marginTop: constants.generalUnit * 3, + marginBottom: constants.generalUnit + }, + subheading: { + marginBottom: constants.generalUnit * 3 + }, + boldText: { + fontWeight: "bold" + }, + normalWeightText: { + fontWeight: "normal" + }, + featureSeparator: { + marginBottom: constants.generalUnit + }, + rowBox: { + display: "flex", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + middleRowBox: { + display: "flex", + alignItems: "center", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + pushRightBox: { + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + flex: 1 + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + } + }, + bottomSection: { + display: "flex", + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + margin: `${constants.generalUnit * 3}px 0px` + }, + divider: { + margin: `${constants.generalUnit}px 0` + } + }) +) + +interface IPlanDetails { + plan: Product + goToSelectPlan: () => void + onSelectPlanPrice: () => void + billingPeriod: ProductPriceRecurringInterval + onChangeBillingPeriod: (paymentPeriod: ProductPriceRecurringInterval) => void + monthlyPrice?: ProductPrice + yearlyPrice?: ProductPrice +} + +const PlanDetails = ({ + plan, + goToSelectPlan, + onSelectPlanPrice, + billingPeriod, + onChangeBillingPeriod, + monthlyPrice, + yearlyPrice +}: IPlanDetails) => { + const classes = useStyles() + const currentPlanStorage = useMemo(() => formatBytes(Number(monthlyPrice?.metadata?.storage_size_bytes), 2), [monthlyPrice]) + const percentageOff = useMemo(() => monthlyPrice && yearlyPrice && + (((monthlyPrice.unit_amount * 12) - yearlyPrice.unit_amount) * 100) / (monthlyPrice.unit_amount * 12) + , [monthlyPrice, yearlyPrice]) + + return ( +
+ + {plan.name} + + + You get access to these features right now. + + +
+ + Features + +
+ + {currentPlanStorage} of storage + +
+
+ +
+ + Billing start time + +
+ + {dayjs().format("DD MMM YYYY")} + +
+
+ + {monthlyPrice && yearlyPrice && + <> +
+ + Annual billing + {percentageOff && percentageOff > 0 && ` (${Math.round(percentageOff)}% off)`} + + +
+ +
+
+ + + } +
+ + Total + +
+ + {billingPeriod === "month" + ? `${monthlyPrice?.unit_amount ? monthlyPrice?.currency : ""} ${monthlyPrice?.unit_amount}` + : `${yearlyPrice?.unit_amount ? yearlyPrice?.currency : ""} ${yearlyPrice?.unit_amount}` + } + {billingPeriod === "month" ? t`/month` : t`/year`} + +
+
+
+
+ + +
+
+
+ ) +} + +export default PlanDetails \ No newline at end of file diff --git a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PlanSuccess.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PlanSuccess.tsx similarity index 86% rename from packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PlanSuccess.tsx rename to packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PlanSuccess.tsx index b55c30d97f..9754f11445 100644 --- a/packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/ChangePlan/PlanSuccess.tsx +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/PlanSuccess.tsx @@ -1,15 +1,15 @@ import React from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" -import { CSFTheme } from "../../../../../Themes/types" +import { CSSTheme } from "../../../../Themes/types" import { Product, ProductPrice } from "@chainsafe/files-api-client" -import { Button, CheckCircleIcon, CheckIcon, Divider, formatBytes, Link, Typography } from "@chainsafe/common-components" +import { Button, CheckCircleIcon, CheckIcon, Divider, formatBytes, Link, Typography } from "@chainsafe/common-components" import { Trans } from "@lingui/macro" -import { ROUTE_LINKS } from "../../../../FilesRoutes" +import { ROUTE_LINKS } from "../../../StorageRoutes" import clsx from "clsx" -const useStyles = makeStyles(({ constants, palette }: CSFTheme) => +const useStyles = makeStyles(({ constants, palette }: CSSTheme) => createStyles({ - root: { + root: { margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` }, headingBadge: { @@ -65,9 +65,9 @@ const useStyles = makeStyles(({ constants, palette }: CSFTheme) => marginBottom: constants.generalUnit }, textLink: { - color: palette.primary.background + color: palette.primary.main }, - checkCircleIcon: { + checkCircleIcon: { fill: palette.additional["gray"][7], marginLeft: constants.generalUnit }, @@ -97,6 +97,7 @@ const PlanSuccess = ({ plan, onClose, planPrice }: IPlanSuccess) => { variant="body1" component="p" className={classes.headingBadge} + data-cy="header-plan-change-success" > Confirmation @@ -104,6 +105,7 @@ const PlanSuccess = ({ plan, onClose, planPrice }: IPlanSuccess) => { Plan changed successfully @@ -115,6 +117,7 @@ const PlanSuccess = ({ plan, onClose, planPrice }: IPlanSuccess) => { variant="body1" component="p" className={classes.featuresTitle} + data-cy="label-features-summary-title" > You now have: @@ -124,6 +127,7 @@ const PlanSuccess = ({ plan, onClose, planPrice }: IPlanSuccess) => { {planPrice?.metadata?.storage_size_bytes ? {newPlanCapacity} of storage @@ -131,15 +135,6 @@ const PlanSuccess = ({ plan, onClose, planPrice }: IPlanSuccess) => { }
-
- - - {plan.description} - -
@@ -147,11 +142,13 @@ const PlanSuccess = ({ plan, onClose, planPrice }: IPlanSuccess) => { component="p" variant="body1" className={classes.invoiceText} + data-cy="label-billing-history-notice" > Access your billing history in settings or view your   invoices here @@ -162,6 +159,7 @@ const PlanSuccess = ({ plan, onClose, planPrice }: IPlanSuccess) => { diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/SelectPlan.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/SelectPlan.tsx new file mode 100644 index 0000000000..a385834736 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/ChangePlan/SelectPlan.tsx @@ -0,0 +1,353 @@ +import React, { useState } from "react" +import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" +import clsx from "clsx" +import { Button, formatBytes, Loading, Typography } from "@chainsafe/common-components" +import { t, Trans } from "@lingui/macro" +import { CSSTheme } from "../../../../Themes/types" +import { useBilling } from "../../../../Contexts/BillingContext" +import { Product } from "@chainsafe/files-api-client" +import { ROUTE_LINKS } from "../../../StorageRoutes" + +const useStyles = makeStyles(({ breakpoints, constants, palette, typography }: CSSTheme) => + createStyles({ + root: { + position: "relative", + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 3}px`, + [breakpoints.down("md")]: { + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` + } + }, + header: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginTop: constants.generalUnit * 2 + }, + loadingContainer: { + margin: `${constants.generalUnit * 4}px 0`, + display: "flex", + justifyContent: "center" + }, + panels: { + display: "grid", + gridColumnGap: constants.generalUnit * 1.5, + gridRowGap: constants.generalUnit * 1.5, + gridTemplateColumns: "1fr 1fr 1fr", + marginTop: constants.generalUnit * 2, + marginBottom: constants.generalUnit * 3, + [breakpoints.down("md")]: { + gridTemplateColumns: "1fr", + marginTop: constants.generalUnit * 3 + } + }, + planBox: { + border: `2px solid ${palette.additional["gray"][5]}`, + padding: `${constants.generalUnit * 3}px ${constants.generalUnit * 4}px `, + display: "flex", + flexDirection: "column", + alignItems: "center", + borderRadius: 5, + [breakpoints.down("md")]: { + padding: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px `, + justifyContent: "space-between" + }, + "&.active": { + borderColor: palette.primary.background + } + }, + priceSpace: { + height: 22 + }, + link: { + display: "flex", + justifyContent: "flex-start", + alignItems: "center", + "& svg": { + marginLeft: constants.generalUnit, + stroke: palette.additional.gray[10], + width: constants.generalUnit * 2, + height: constants.generalUnit * 2 + } + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + }, + [breakpoints.down("md")]: { + flex: 1 + } + }, + bottomSection: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center" + }, + planTitle: { + fontWeight: "bold", + marginBottom: constants.generalUnit + }, + priceSubtitle: { + ...typography.body2, + color: palette.additional["gray"][7] + }, + description: { + margin: `${constants.generalUnit * 3}px 0`, + [breakpoints.down("md")]: { + margin: 0 + } + }, + priceYearlyTitle: { + fontWeight: "bold", + [breakpoints.down("md")]: { + fontWeight: "normal", + marginTop: constants.generalUnit + } + }, + mobilePriceBox: { + display: "flex", + flexDirection: "column", + alignItems: "end", + height: "100%" + }, + selectButton: { + marginLeft: constants.generalUnit + }, + cannotUpdate: { + color: palette.error.main, + marginTop: "1rem", + textAlign: "center" + }, + loader: { + "& > svg" : { + marginRight: constants.generalUnit + }, + [breakpoints.down("md")]: { + width: "100%", + marginTop: constants.generalUnit + } + }, + priceAndDescription: { + flexDirection: "row", + justifyContent: "space-between", + display: "flex", + alignItems: "center", + width: "100%" + } + }) +) + +interface ISelectPlan { + className?: string + plans?: Product[] + onSelectPlan: (plan: Product) => void +} + +const SelectPlan = ({ className, onSelectPlan, plans }: ISelectPlan) => { + const classes = useStyles() + const { currentSubscription, isPendingInvoice } = useBilling() + const { desktop } = useThemeSwitcher() + const [tempSelectedPlan, setTempSelectedPlan] = useState() + + return ( +
+
+ + Switch Plan + +
+ {!plans && ( +
+ +
+ )} +
+ {plans && plans.map((plan) => { + const monthly = plan.prices.find((price) => price.recurring.interval === "month") + const yearly = plan.prices.find((price) => price.recurring.interval === "year") + const isUpdateAllowed = !!monthly?.is_update_allowed || !!yearly?.is_update_allowed + const isCurrentPlan = plan.id === currentSubscription?.product.id + const planStorageCapacity = formatBytes(Number(monthly?.metadata?.storage_size_bytes), 2) + + return desktop + ? ( +
+ + {plan.name} + + {monthly && ( + + {monthly.unit_amount + ? <> + {monthly.currency.toUpperCase()} {monthly.unit_amount} + + /month + + + : t`Free`} + + )} + {monthly && yearly + ? ( + + {yearly.currency.toUpperCase()} {yearly.unit_amount} + + /year + + + ) + :
+ } + {!isUpdateAllowed && !isCurrentPlan && ( + + Your content exceeds the {planStorageCapacity} storage capacity for this plan. + + )} + + { + monthly?.metadata?.storage_size_bytes + ? {planStorageCapacity} of storage + : plan.description + } + + +
+ ) + : ( +
!isPendingInvoice && isUpdateAllowed && !isCurrentPlan && setTempSelectedPlan(plan)} + key={`plan-${plan.id}`} + > +
+
+ + {plan.name} + + + { + monthly?.metadata?.storage_size_bytes + ? {planStorageCapacity} of storage + : plan.description + } + +
+
+ {monthly && ( + + {monthly.unit_amount + ? <> + {monthly.currency.toUpperCase()} {monthly.unit_amount} + + /month + + + : t`Free`} + + )} + {monthly && yearly + ? ( + + {yearly.currency.toUpperCase()} {yearly.unit_amount} + + /year + + + ) + :
+ } +
+
+
+ )})} +
+
+ {desktop && ( + + Have a question?  + + Contact us + + + )} + {!desktop && ( +
+ +
+ )} +
+
+ ) +} + +export default SelectPlan \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/AddCard.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/AddCard.tsx new file mode 100644 index 0000000000..3ad3ce80ec --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/AddCard.tsx @@ -0,0 +1,267 @@ +import React, { FormEvent, useMemo, useState } from "react" +import { Button, Typography, useToasts } from "@chainsafe/common-components" +import { createStyles, makeStyles, useTheme, useThemeSwitcher } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import CustomButton from "../../../Elements/CustomButton" +import { t, Trans } from "@lingui/macro" +import { + useStripe, + useElements, + CardNumberElement, + CardExpiryElement, + CardCvcElement +} from "@stripe/react-stripe-js" +import { useStorageApi } from "../../../../Contexts/StorageApiContext" +import { useBilling } from "../../../../Contexts/BillingContext" +import clsx from "clsx" + +const useStyles = makeStyles( + ({ breakpoints, constants, palette, animation }: CSSTheme) => { + return createStyles({ + root: { + flexDirection: "column" + }, + okButton: { + marginLeft: constants.generalUnit + }, + label: { + fontSize: 14, + lineHeight: "22px" + }, + cardNumberInputs: { + marginBottom: constants.generalUnit * 2, + [breakpoints.down("md")]: { + marginTop: constants.generalUnit * 2, + marginBottom: constants.generalUnit * 2 + } + }, + cardInputs: { + border: `1px solid ${palette.additional["gray"][6]}`, + borderRadius: 2, + padding: constants.generalUnit * 1.5, + transitionDuration: `${animation.transform}ms`, + "&:hover": { + borderColor: palette.primary.border + } + }, + cardInputsFocus: { + borderColor: palette.primary.border, + boxShadow: constants.addCard.shadow + }, + expiryCvcContainer: { + display: "grid", + gridTemplateColumns: "1fr 1fr", + marginTop: constants.generalUnit, + gridColumnGap: constants.generalUnit, + [breakpoints.down("md")]: { + gridTemplateColumns: "1fr", + gridRowGap: constants.generalUnit * 2 + } + }, + error: { + marginTop: constants.generalUnit * 2, + color: palette.error.main + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + } + } + }) + } +) + +interface IAddCardProps { + submitText: string + onCardAdd?: () => void + onClose?: () => void + goBack?: () => void + footerClassName?: string +} + + +const AddCard = ({ onClose, onCardAdd, footerClassName, submitText, goBack }: IAddCardProps) => { + const classes = useStyles() + const stripe = useStripe() + const elements = useElements() + const { addToast } = useToasts() + const { storageApiClient } = useStorageApi() + const { refreshDefaultCard, deleteCard, updateDefaultCard, defaultCard } = useBilling() + const [focusElement, setFocusElement] = useState<"number" | "expiry" | "cvc" | undefined>(undefined) + const [cardAddError, setCardAddError] = useState(undefined) + const theme: CSSTheme = useTheme() + const isUpdate = useMemo(() => !!defaultCard, [defaultCard]) + const { desktop } = useThemeSwitcher() + + const [loadingPaymentMethodAdd, setLoadingPaymentMethodAdd] = useState(false) + + const handleSubmitPaymentMethod = async (event: FormEvent) => { + event.preventDefault() + setCardAddError(undefined) + if (!stripe || !elements) return + try { + const cardNumberElement = elements.getElement(CardNumberElement) + if (!cardNumberElement) return + + setLoadingPaymentMethodAdd(true) + const { error, paymentMethod } = await stripe.createPaymentMethod({ + type: "card", + card: cardNumberElement + }) + + if (error || !paymentMethod) { + console.error(error) + setLoadingPaymentMethodAdd(false) + setCardAddError(t`Card inputs invalid`) + return + } + + const setupIntent = await storageApiClient.createSetupIntent() + const setupIntentResult = await stripe.confirmCardSetup(setupIntent.secret, { + payment_method: paymentMethod.id + }) + + if (setupIntentResult.error || !setupIntentResult.setupIntent) { + console.error(error) + setLoadingPaymentMethodAdd(false) + setCardAddError(t`Failed to add payment method`) + return + } + isUpdate && defaultCard && await deleteCard(defaultCard) + await updateDefaultCard(paymentMethod.id) + refreshDefaultCard() + onCardAdd && onCardAdd() + setLoadingPaymentMethodAdd(false) + addToast({ + title: isUpdate ? t`Card updated` : t`Card added`, + type: "success", + testId: isUpdate ? "card-updated" : "card-added" + }) + } catch (error) { + console.error(error) + setLoadingPaymentMethodAdd(false) + setCardAddError(t`Failed to add payment method`) + } + } + + return ( +
+
+ setFocusElement("number")} + onBlur={() => setFocusElement(undefined)} + onChange={() => setCardAddError(undefined)} + /> +
+ setFocusElement("expiry")} + onBlur={() => setFocusElement(undefined)} + onChange={() => setCardAddError(undefined)} + options={{ + style: { + base: { + color: theme.constants.addCard.color, + "::placeholder": { + color: theme.constants.addCard.placeholderColor + } + } + } + }} + /> + setFocusElement("cvc")} + onBlur={() => setFocusElement(undefined)} + onChange={() => setCardAddError(undefined)} + options={{ + style: { + base: { + color: theme.constants.addCard.color, + "::placeholder": { + color: theme.constants.addCard.placeholderColor + } + } + } + }} + /> +
+ {cardAddError && + {cardAddError} + + } +
+ {goBack && + + } + {onClose && + + Cancel + + } + +
+
+
+ ) +} + +export default AddCard diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/ConfirmPlan.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/ConfirmPlan.tsx new file mode 100644 index 0000000000..48760ef766 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/ConfirmPlan.tsx @@ -0,0 +1,469 @@ +import React, { useState, useEffect, useMemo } from "react" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import { CheckSubscriptionUpdate, Product, ProductPrice } from "@chainsafe/files-api-client" +import { Button, InfoCircleIcon, CreditCardIcon, Divider, formatBytes, Typography } from "@chainsafe/common-components" +import { t, Trans } from "@lingui/macro" +import dayjs from "dayjs" +import { PaymentMethod, useBilling } from "../../../../Contexts/BillingContext" +import clsx from "clsx" +import { useStorageApi } from "../../../../Contexts/StorageApiContext" + +const useStyles = makeStyles(({ constants, palette }: CSSTheme) => + createStyles({ + root: { + position: "relative", + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` + }, + heading: { + marginTop: constants.generalUnit * 3, + marginBottom: constants.generalUnit * 2 + }, + subheading: { + marginBottom: constants.generalUnit * 3 + }, + boldText: { + fontWeight: 600 + }, + normalWeightText: { + fontWeight: "normal" + }, + rowBox: { + display: "flex", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + middleRowBox: { + display: "flex", + alignItems: "center", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + pushRightBox: { + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + flex: 1 + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + } + }, + bottomSection: { + display: "flex", + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + margin: `${constants.generalUnit * 3}px 0px` + }, + divider: { + margin: `${constants.generalUnit}px 0` + }, + textButton: { + color: palette.primary.main, + cursor: "pointer", + textDecoration: "underline" + }, + creditCardIcon: { + marginRight: constants.generalUnit, + fill: palette.additional["gray"][9] + }, + featuresBox: { + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit * 2 + }, + creditCardRow: { + display: "flex", + alignItems: "center", + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit + }, + featureSeparator: { + marginBottom: constants.generalUnit + }, + error: { + marginTop: constants.generalUnit, + color: palette.error.main + }, + warningText: { + marginTop: constants.generalUnit * 2, + maxWidth: constants.generalUnit * 56, + color: palette.additional["gray"][7] + }, + icon : { + verticalAlign: "middle", + fill: palette.additional["gray"][7], + "& > svg": { + height: constants.generalUnit * 2.25 + } + } + }) +) + +interface IConfirmPlan { + plan: Product + planPrice: ProductPrice + goToSelectPlan: () => void + goToPaymentMethod: () => void + onChangeSubscription: () => void + loadingChangeSubscription: boolean + paymentMethod: PaymentMethod + subscriptionErrorMessage?: string +} + +const ConfirmPlan = ({ + plan, + planPrice, + goToSelectPlan, + goToPaymentMethod, + onChangeSubscription, + loadingChangeSubscription, + paymentMethod, + subscriptionErrorMessage +}: IConfirmPlan) => { + const classes = useStyles() + const { defaultCard } = useBilling() + const { currentSubscription } = useBilling() + const { storageApiClient } = useStorageApi() + const [checkSubscriptionUpdate, setCheckSubscriptionUpdate] = useState() + const newPlanStorage = formatBytes(Number(planPrice?.metadata?.storage_size_bytes), 2) + + const isDowngrade = useMemo(() => { + const currentPrice = currentSubscription?.product?.price?.unit_amount + return currentPrice && currentPrice > planPrice.unit_amount + }, [currentSubscription, planPrice]) + + useEffect(() => { + if (!currentSubscription) return + + storageApiClient.checkSubscriptionUpdate(currentSubscription?.id, { + payment_method: paymentMethod === "creditCard" ? "stripe" : "crypto", + price_id: planPrice.id + }) + .then(setCheckSubscriptionUpdate) + .catch(console.error) + }, [currentSubscription, paymentMethod, storageApiClient, planPrice]) + + return ( +
+ + { + isDowngrade + ? Confirm plan downgrade + : Confirm plan change + } + + +
+ + {plan.name} + +
+ + Edit plan + +
+
+
+ + Features + +
+ + {newPlanStorage} of storage + +
+
+ + {paymentMethod === "creditCard" && defaultCard && + <> +
+ + Payment method + +
+ + Edit payment method + +
+
+
+ + + •••• •••• •••• {defaultCard.last_four_digit} + +
+ + } + {paymentMethod === "crypto" && + <> +
+ + Pay with Crypto + +
+ + Edit payment + +
+
+
+
+
+
+ + Accepted currencies + +
+ + DAI, USDC, ETH or BTC + +
+
+ + } +
+ + Billing start time + +
+ + {dayjs().format("DD MMM YYYY")} + +
+
+ +
+ + Pricing details + +
+
+ + Plan price + +
+ + {planPrice.unit_amount ? planPrice.currency : ""} {planPrice.unit_amount} + + {planPrice.recurring.interval === "month" ? t`/month` : t`/year`} + + +
+
+ {!!checkSubscriptionUpdate?.amount_from_credit && +
+ + Amount from credit + +
+ + {planPrice.currency}  + {checkSubscriptionUpdate.amount_from_credit.toFixed(2)} + +
+
+ } + {!!checkSubscriptionUpdate?.amount_unused_from_last_bill && +
+ + Amount available from last bill + +
+ + {planPrice.currency}  + {checkSubscriptionUpdate.amount_unused_from_last_bill.toFixed(2)} + +
+
+ } + + {!!checkSubscriptionUpdate &&
+ + Amount due + +
+ + {planPrice.currency} {checkSubscriptionUpdate?.amount_due.toFixed(2)} + +
+
+ } + {!!checkSubscriptionUpdate?.amount_credited &&
+ + Amount added to credit + +
+ + {planPrice.currency}  + {checkSubscriptionUpdate.amount_credited.toFixed(2)} + +
+
+ } +
+ + + {paymentMethod === "crypto" + ? + Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received + , your plan will not change. + + : + Payments are final and non-refundable. If you wish to change your plan, + any extra funds will be applied as credit towards future payments. + + } + +
+ {subscriptionErrorMessage && + + {subscriptionErrorMessage} + + } +
+
+ + +
+
+
+ ) +} + +export default ConfirmPlan \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/CryptoPayment.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/CryptoPayment.tsx new file mode 100644 index 0000000000..0e8d75a0d2 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/Common/CryptoPayment.tsx @@ -0,0 +1,577 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { makeStyles, createStyles, debounce } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import { ProductPrice, InvoiceResponse } from "@chainsafe/files-api-client" +import { + BitcoinIcon, + Button, + CircularProgressBar, + CopyIcon, + DaiIcon, + Divider, + EthereumIcon, + InfoCircleIcon, + Loading, + Typography, + UsdcIcon +} from "@chainsafe/common-components" +import { t, Trans } from "@lingui/macro" +import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" +import { useBilling } from "../../../../Contexts/BillingContext" +import { useStorageApi } from "../../../../Contexts/StorageApiContext" +import QRCode from "react-qr-code" +import { useWeb3 } from "@chainsafe/web3-context" +import { utils } from "ethers" +import clsx from "clsx" + +dayjs.extend(duration) + +const useStyles = makeStyles(({ constants, palette, zIndex, animation, breakpoints }: CSSTheme) => + createStyles({ + root: { + margin: `${constants.generalUnit * 2}px ${constants.generalUnit * 2}px` + }, + heading: { + marginTop: constants.generalUnit * 3, + marginBottom: constants.generalUnit * 2 + }, + subheading: { + marginBottom: constants.generalUnit * 3 + }, + boldText: { + fontWeight: "bold" + }, + normalWeightText: { + fontWeight: "normal" + }, + rowBox: { + display: "flex", + alignItems: "center", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + middleRowBox: { + display: "flex", + alignItems: "center", + padding: `${constants.generalUnit * 0.5}px 0px` + }, + pushRightBox: { + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + flex: 1 + }, + buttons: { + display: "flex", + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-end", + "& > *": { + marginLeft: constants.generalUnit + } + }, + bottomSection: { + display: "flex", + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + margin: `${constants.generalUnit * 3}px 0px` + }, + divider: { + margin: `${constants.generalUnit}px 0` + }, + textButton: { + color: palette.primary.main, + cursor: "pointer", + textDecoration: "underline" + }, + creditCardIcon: { + marginRight: constants.generalUnit, + fill: palette.additional["gray"][9] + }, + featuresBox: { + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit * 2 + }, + creditCardRow: { + display: "flex", + alignItems: "center", + marginTop: constants.generalUnit, + marginBottom: constants.generalUnit + }, + featureSeparator: { + marginBottom: constants.generalUnit + }, + error: { + marginTop: constants.generalUnit, + color: palette.error.main + }, + qrCode: { + display: "flex", + justifyContent: "center" + }, + qrCodeInside: { + padding: 2, + backgroundColor: "white", + height: 132 + }, + qrCodeLabel: { + display: "flex", + justifyContent: "center", + textAlign: "center", + color: palette.additional["gray"][8] + }, + availableCurrencies: { + display: "flex", + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "space-between" + }, + currencyButton: { + width: "calc(50% - 8px)", + backgroundColor: palette.additional["gray"][4], + color: palette.additional["gray"][10], + borderRadius: 10, + marginTop: 4, + marginBottom: 4, + "&:hover": { + backgroundColor: palette.additional["gray"][4], + color: palette.additional["gray"][10] + } + }, + currencyIcon: { + "& > svg": { + fill: palette.additional["gray"][10], + height: 16 + } + }, + copiedFlag: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + left: "50%", + bottom: "calc(100% + 8px)", + position: "absolute", + transform: "translate(-50%, 0%)", + zIndex: zIndex?.layer1, + transitionDuration: `${animation.transform}ms`, + opacity: 0, + visibility: "hidden", + backgroundColor: palette.additional["gray"][9], + color: palette.additional["gray"][1], + padding: `${constants.generalUnit / 2}px ${constants.generalUnit}px`, + borderRadius: 2, + "&:after": { + transitionDuration: `${animation.transform}ms`, + content: "''", + position: "absolute", + top: "100%", + left: "50%", + transform: "translate(-50%,0)", + width: 0, + height: 0, + borderLeft: "5px solid transparent", + borderRight: "5px solid transparent", + borderTop: `5px solid ${palette.additional["gray"][9]}` + }, + "&.active": { + opacity: 1, + visibility: "visible" + } + }, + copyIcon: { + fontSize: "16px", + fill: palette.additional["gray"][9], + [breakpoints.down("md")]: { + fontSize: "18px", + fill: palette.additional["gray"][9] + } + }, + copyRow: { + position: "relative", + cursor: "pointer", + borderRadius: 10, + backgroundColor: palette.additional["gray"][4], + padding: "5px 10px", + "& > span": { + width: "100%", + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis" + } + }, + loadingContainer: { + margin: `${constants.generalUnit * 4}px 0`, + display: "flex", + justifyContent: "center", + flexDirection: "column", + alignItems: "center" + }, + warningText: { + marginTop: constants.generalUnit * 3, + maxWidth: constants.generalUnit * 56, + color: palette.additional["gray"][8] + }, + icon : { + verticalAlign: "middle", + "& > svg": { + fill: palette.additional["gray"][7], + height: constants.generalUnit * 2.25 + } + } + }) +) + +interface ICryptoPayment { + planPrice?: ProductPrice + onClose: () => void +} + +const iconMap: { [key: string]: React.FC } = { + ethereum: EthereumIcon, + bitcoin: BitcoinIcon, + dai: DaiIcon, + usdc: UsdcIcon +} + +const symbolMap: { [key: string]: string } = { + ethereum: "ETH", + bitcoin: "BTC", + dai: "DAI", + usdc: "USDC" +} + +const CryptoPayment = ({ planPrice, onClose }: ICryptoPayment) => { + const classes = useStyles() + const { selectWallet, storageApiClient } = useStorageApi() + const { isReady, network, provider, wallet, tokens, switchNetwork, checkIsReady, ethBalance } = useWeb3() + const { currentSubscription, fetchCurrentSubscription, isPendingInvoice, invoices } = useBilling() + const [cryptoPayment, setCryptoPayment] = useState() + const [error, setError] = useState(undefined) + const [cryptoChargeLoading, setCryptoChargeLoading] = useState(false) + const [transferActive, setTransferActive] = useState(false) + const [timeRemaining, setTimeRemaining] = useState() + const pageLoadTimestamp = useRef(dayjs().unix()) + const [copiedDestinationAddress, setCopiedDestinationAddress] = useState(false) + const [copiedAmount, setCopiedAmount] = useState(false) + const debouncedSwitchCopiedDestinationAddress = debounce(() => setCopiedDestinationAddress(false), 3000) + const debouncedSwitchCopiedAmount = debounce(() => setCopiedAmount(false), 3000) + const pendingCryptoInvoice = useMemo(() => + invoices?.find((i) => i.payment_method === "crypto" && i.status === "open") + , [invoices]) + const currencies = useMemo(() => cryptoPayment?.payment_methods.map(c => c.currency), [cryptoPayment]) + const [selectedCurrency, setSelectedCurrency] = useState(undefined) + const [isFetchingSubscription, setIsFetchingSubscription] = useState(false) + const isTimeUp = useMemo(() => timeRemaining && timeRemaining?.asSeconds() <= 0, [timeRemaining]) + + useEffect(() => { + // no more time to pay, this effect will run until there is no more pending subscription + if(!isFetchingSubscription && isTimeUp){ + setIsFetchingSubscription(true) + fetchCurrentSubscription() + .then((subscription) => { + if (subscription && subscription.status !== "pending_update"){ + onClose() + } + }) + .finally(() => setIsFetchingSubscription(false)) + } + }, [fetchCurrentSubscription, isFetchingSubscription, isTimeUp, onClose, timeRemaining]) + + useEffect(() => { + if (!currentSubscription || !planPrice || isPendingInvoice || isTimeUp) return + + setCryptoChargeLoading(true) + + storageApiClient.updateSubscription(currentSubscription.id, { + price_id: planPrice.id, + payment_method: "crypto" + }).then(() => { + fetchCurrentSubscription() + }).catch((error) => { + console.error(error) + setError(t`There was a problem creating a charge ${error}`) + }).finally(() => setCryptoChargeLoading(false)) + }, [currentSubscription, fetchCurrentSubscription, storageApiClient, isPendingInvoice, isTimeUp, planPrice]) + + useEffect(() => { + if (!pendingCryptoInvoice || isTimeUp) return + + pendingCryptoInvoice?.uuid && storageApiClient.payInvoice(pendingCryptoInvoice.uuid) + .then(r => { + setCryptoPayment(r.crypto) + pageLoadTimestamp.current = dayjs().unix() + }) + .catch(console.error) + }, [storageApiClient, isTimeUp, pendingCryptoInvoice]) + + useEffect(() => { + const timer = setInterval(() => { + if (cryptoPayment) { + setTimeRemaining(dayjs.duration(cryptoPayment.expires_at - dayjs().unix(), "s")) + } + }, 1000) + + return () => { + timer && clearInterval(timer) + } + }, [cryptoPayment]) + + const selectedPaymentMethod = useMemo(() => + cryptoPayment && selectedCurrency && cryptoPayment.payment_methods.find(p => p.currency === selectedCurrency) + , [cryptoPayment, selectedCurrency]) + + const onCopyDestinationAddress = useCallback(() => { + if (!selectedPaymentMethod) return + + navigator.clipboard.writeText(selectedPaymentMethod.address) + .then(() => { + setCopiedDestinationAddress(true) + debouncedSwitchCopiedDestinationAddress() + }).catch(console.error) + }, [debouncedSwitchCopiedDestinationAddress, selectedPaymentMethod]) + + const onCopyAmount = useCallback(() => { + if (!selectedPaymentMethod) return + + navigator.clipboard.writeText(selectedPaymentMethod.amount) + .then(() => { + setCopiedAmount(true) + debouncedSwitchCopiedAmount() + }).catch(console.error) + }, [debouncedSwitchCopiedAmount, selectedPaymentMethod]) + + const isBalanceSufficient = useMemo(() => { + if (selectedCurrency === "bitcoin") { + return false + } else if (selectedCurrency === "ethereum") { + return ethBalance && selectedPaymentMethod && ethBalance > Number(selectedPaymentMethod.amount) + } else { + const token = Object.values(tokens).find(t => t.symbol?.toLowerCase() === selectedCurrency) + return token && selectedPaymentMethod && token.balance >= Number(selectedPaymentMethod.amount) + } + }, [selectedCurrency, ethBalance, selectedPaymentMethod, tokens]) + + const handlePayment = useCallback(async () => { + if (!provider || !selectedCurrency || !selectedPaymentMethod) return + + const signer = provider.getSigner() + try { + setTransferActive(true) + if (selectedCurrency === "ethereum") { + await (await signer.sendTransaction({ + to: selectedPaymentMethod.address, + value: utils.parseEther(selectedPaymentMethod.amount) + })).wait(1) + } else { + const token = Object.values(tokens).find(t => t.symbol?.toLowerCase() === selectedCurrency) + if (!token || !token.transfer) return + await (await token.transfer( + selectedPaymentMethod.address, + utils.parseUnits(selectedPaymentMethod.amount, token.decimals) + )).wait(1) + } + } catch (error) { + console.error(error) + } finally { + setTransferActive(false) + } + }, [provider, selectedCurrency, selectedPaymentMethod, tokens]) + + const handleSwitchNetwork = useCallback(async () => { + await switchNetwork(1) + await checkIsReady() + }, [checkIsReady, switchNetwork]) + + return ( +
+
+ + Pay with crypto + + {cryptoPayment && !isTimeUp &&
+ +
} +
+ {(cryptoChargeLoading || !cryptoPayment || isTimeUp) &&
+ + {isTimeUp && + The time to pay with crypto is up. Updating your plan... + } +
} + {error && + + Failed to create a charge + + } + {cryptoPayment && pendingCryptoInvoice && !isTimeUp && + <> +
+ + Total + +
+ + {pendingCryptoInvoice.currency?.toUpperCase()} {pendingCryptoInvoice.amount} + +
+
+ + {!selectedCurrency && currencies && + <> + + Select a cryptocurrency + +
+ {currencies.map(c => { + const CurrencyIcon = iconMap[c] || null + return + })} +
+ + } + {selectedCurrency && selectedPaymentMethod && + <> +
+
+ +
+
+
+ + Only send the exact amount of {symbolMap[selectedCurrency]} to this address + +
+ + + Destination Address + +
+ {selectedPaymentMethod.address} +
+ +
+ + + Copied! + + +
+
+
+ + Total Amount + +
+ + {selectedPaymentMethod.amount} {symbolMap[selectedCurrency]} + +
+ +
+ + + Copied! + + +
+
+
+ + + + All crypto payments are final and ineligible for credits, + exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed. + + + + } + + } + {!isTimeUp &&
+
+ {!!selectedCurrency && } + {selectedCurrency && selectedCurrency !== "bitcoin" && !isReady && + + } + {selectedCurrency && selectedCurrency !== "bitcoin" && isReady && network !== 1 && + + } + {selectedCurrency && selectedCurrency !== "bitcoin" && isReady && network === 1 && + + } +
+
} +
+ ) +} + +export default CryptoPayment \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/CurrentCard.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/CurrentCard.tsx new file mode 100644 index 0000000000..6be9336b8d --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/CurrentCard.tsx @@ -0,0 +1,176 @@ +import React, { useState } from "react" +import { Typography, CreditCardIcon, Button, Dialog } from "@chainsafe/common-components" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { t, Trans } from "@lingui/macro" +import { useBilling } from "../../../Contexts/BillingContext" +import AddCardModal from "./AddCard/AddCardModal" +import clsx from "clsx" +import dayjs from "dayjs" +import { CSSTheme } from "../../../Themes/types" + +const useStyles = makeStyles(({ constants, palette }: CSSTheme) => + createStyles({ + container: { + padding: `${constants.generalUnit}px 0`, + margin: `${constants.generalUnit * 1.5}px 0` + }, + spaceBetweenBox: { + display: "flex", + justifyContent: "space-between" + }, + heading: { + flex: 1 + }, + cardLineMargins: { + margin: `${constants.generalUnit * 2}px 0` + }, + cardDetailsContainer: { + display: "flex" + }, + creditCardIcon: { + marginRight: constants.generalUnit, + fill: palette.additional["gray"][9] + }, + linkButton: { + textDecoration: "underline", + cursor: "pointer", + margin: `0 ${constants.generalUnit * 2}px`, + color: palette.additional["gray"][7] + }, + confirmDeletionDialog: { + top: "50%" + }, + link: { + color: palette.primary.main, + paddingRight: "0px !important", + fontSize: 16 + }, + text: { + fontSize: 16 + } + }) +) + +const CurrentCard: React.FC = () => { + const classes = useStyles() + const [isAddCardModalOpen, setIsAddCardModalOpen ] = useState(false) + const { defaultCard, deleteCard, refreshDefaultCard, currentSubscription } = useBilling() + const [isDeleteCardModalOpen, setIsDeleteCardModalOpen] = useState(false) + const [isDeleteCardLoading, setIsDeleteCardLoading] = useState(false) + + const onRemoveCard = () => { + if (!defaultCard) return + setIsDeleteCardLoading(true) + deleteCard(defaultCard) + .then(refreshDefaultCard) + .catch(console.error) + .finally(() => { + setIsDeleteCardModalOpen(false) + setIsDeleteCardLoading(false) + }) + } + + return ( + <> +
+
+ + Payment information + + +
+ {!!currentSubscription?.expiry_date &&
+ + Next payment + + + {dayjs.unix(currentSubscription.expiry_date).format("MMMM DD, YYYY")} + +
+ } + {defaultCard + ?
+
+ + + •••• •••• •••• {defaultCard.last_four_digit} + + setIsDeleteCardModalOpen(true)} + data-cy="link-remove-card" + > + Remove + +
+ + expires {defaultCard.exp_month}/{defaultCard.exp_year.toString().substring(2)} + +
+ : + No Card + + } +
+ setIsAddCardModalOpen(false)} + /> + setIsDeleteCardModalOpen(false)} + accept={onRemoveCard} + requestMessage={t`Are you sure? This will delete your default payment method.`} + rejectText={t`Cancel`} + acceptText={t`Confirm`} + acceptButtonProps={{ loading: isDeleteCardLoading, disabled: isDeleteCardLoading, testId: "confirm-remove" }} + rejectButtonProps={{ disabled: isDeleteCardLoading, testId: "cancel-remove" }} + injectedClass={{ inner: classes.confirmDeletionDialog }} + testId="remove-card-confirmation" + /> + + ) +} + +export default CurrentCard diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/CurrentPlan.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/CurrentPlan.tsx new file mode 100644 index 0000000000..d316139097 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/CurrentPlan.tsx @@ -0,0 +1,138 @@ +import React, { useState } from "react" +import { + Button, + formatBytes, + Loading, + ProgressBar, + Typography +} from "@chainsafe/common-components" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { useStorage } from "../../../Contexts/StorageContext" +import { t, Trans } from "@lingui/macro" +import clsx from "clsx" +import { useBilling } from "../../../Contexts/BillingContext" +import ChangePlanModal from "./ChangePlan/ChangePlanModal" +import { CSSTheme } from "../../../Themes/types" + +const useStyles = makeStyles(({ breakpoints, constants, palette }: CSSTheme) => + createStyles({ + root: { + padding: `${constants.generalUnit}px 0`, + "& h2, & h5": { + marginBottom: constants.generalUnit, + fontWeight: 400 + }, + "& h5": { + } + }, + heading: { + flex: 1 + }, + headline: { + display: "flex", + justifyContent: "space-between" + }, + alignRight: { + display: "flex", + justifyContent: "flex-end" + }, + spaceUsedBox: { + [breakpoints.down("md")]: { + marginBottom: constants.generalUnit, + width: "inherit" + } + }, + usageBar: { + marginTop: constants.generalUnit * 1.5, + marginBottom: constants.generalUnit, + overflow: "hidden" + }, + buttons: { + maxWidth: 240, + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + "& > *:first-child": { + marginRight: constants.generalUnit + } + }, + link: { + color: palette.primary.main, + paddingRight: "0px !important", + fontSize: 16 + }, + text: { + fontSize: 16 + } + }) +) + +interface ICurrentProduct { + className?: string +} + +const CurrentPlan = ({ className }: ICurrentProduct) => { + const classes = useStyles() + const { storageSummary } = useStorage() + const { currentSubscription, isPendingInvoice } = useBilling() + const [isChangeProductModalVisible, setChangeProductModalVisible] = useState(false) + + return (
+ {currentSubscription + ?
+ + {currentSubscription?.product.name}{isPendingInvoice && ` ${t`(Awaiting payment)`}`} + + +
+ : + } + {storageSummary && + <> +
+ +
+ + {t`${formatBytes(storageSummary.used_storage, 2)} of ${formatBytes( + storageSummary.total_storage, 2 + )} used`} ({((storageSummary.used_storage / storageSummary.total_storage) * 100).toFixed(1)}%) + +
+
+ { + isChangeProductModalVisible && ( setChangeProductModalVisible(false)} + />) + } + + } +
) +} + +export default CurrentPlan \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/PayInvoice/PayInvoiceModal.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/PayInvoice/PayInvoiceModal.tsx new file mode 100644 index 0000000000..692fe1b303 --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/PayInvoice/PayInvoiceModal.tsx @@ -0,0 +1,91 @@ +import React, { useCallback, useMemo, useState } from "react" +import { makeStyles, createStyles, useThemeSwitcher } from "@chainsafe/common-theme" +import { CSSTheme } from "../../../../Themes/types" +import { Modal } from "@chainsafe/common-components" +import CryptoPayment from "../Common/CryptoPayment" +import { useBilling } from "../../../../Contexts/BillingContext" +import { useStorageApi } from "../../../../Contexts/StorageApiContext" +import ConfirmPlan from "../Common/ConfirmPlan" +import { formatSubscriptionError } from "../utils/formatSubscriptionError" + +const useStyles = makeStyles(({ constants, breakpoints }: CSSTheme) => + createStyles({ + root: { + "&:before": { + backgroundColor: constants.modalDefault.fadeBackground + } + }, + inner: { + borderRadius: `${constants.generalUnit / 2}px`, + [breakpoints.up("sm")]: { + minWidth: 480 + }, + [breakpoints.down("sm")]: { + width: "100%" + } + } + }) +) + +interface IChangeProductModal { + invoiceId: string + onClose: () => void +} + +const PayInvoiceModal = ({ onClose, invoiceId }: IChangeProductModal) => { + const classes = useStyles() + const { desktop } = useThemeSwitcher() + const { invoices, refreshInvoices } = useBilling() + const { storageApiClient } = useStorageApi() + const invoiceToPay = useMemo(() => invoices?.find(i => i.uuid === invoiceId), [invoices, invoiceId]) + const [payingInvoice, setPayingInvoice] = useState(false) + const [errorMessage, setErrorMessage] = useState() + + const payInvoice = useCallback(() => { + if (!invoiceToPay) return + + try { + setPayingInvoice(true) + setErrorMessage(undefined) + storageApiClient.payInvoice(invoiceToPay.uuid).then(refreshInvoices) + } catch (error: any) { + const errorMessage = formatSubscriptionError(error) + setErrorMessage(errorMessage) + } finally { + setPayingInvoice(false) + } + }, [storageApiClient, invoiceToPay, refreshInvoices]) + + if (!invoiceToPay) return null + + return ( + + {invoiceToPay?.payment_method === "crypto" + ? + : undefined } + goToPaymentMethod={() => undefined } + onChangeSubscription={payInvoice} + loadingChangeSubscription={payingInvoice} + subscriptionErrorMessage={errorMessage} + paymentMethod={invoiceToPay.payment_method === "stripe" ? "creditCard" : "crypto"} + /> + } + + ) +} + +export default PayInvoiceModal \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/index.tsx b/packages/storage-ui/src/Components/Modules/SubscriptionTab/index.tsx new file mode 100644 index 0000000000..052834d3cd --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/index.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from "react" +import CurrentCard from "./CurrentCard" +import { Divider, Typography } from "@chainsafe/common-components" +import { makeStyles, createStyles, ITheme } from "@chainsafe/common-theme" +import { Trans } from "@lingui/macro" +import BillingHistory from "./BillingHistory" +import { Elements } from "@stripe/react-stripe-js" +import { loadStripe } from "@stripe/stripe-js" +import CurrentPlan from "./CurrentPlan" +import { useBilling } from "../../../Contexts/BillingContext" + +const useStyles = makeStyles(({ breakpoints, constants }: ITheme) => + createStyles({ + root: { + [breakpoints.down("md")]: { + padding: constants.generalUnit * 1.5 + } + }, + mainHeader: { + fontSize: 28, + marginBottom: constants.generalUnit * 2, + [breakpoints.up("md")]: { + paddingLeft: constants.generalUnit * 2 + } }, + settingsSection: { + maxWidth: breakpoints.values["md"], + [breakpoints.up("md")]: { + padding: `0 ${constants.generalUnit * 2}px` + } + } + }) +) + +const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PK || "") + +const Subscription: React.FC = () => { + const classes = useStyles() + const { refreshDefaultCard, fetchCurrentSubscription } = useBilling() + + useEffect(() => { + // this is needed for testing when a card is deleted programmatically + refreshDefaultCard() + // or when a plan is changed programmatically + fetchCurrentSubscription() + }, [fetchCurrentSubscription, refreshDefaultCard]) + + return ( + +
+ + Subscription plan + + +
+ +
+ +
+ +
+ +
+ +
+
+
+ ) +} + +export default Subscription diff --git a/packages/storage-ui/src/Components/Modules/SubscriptionTab/utils/formatSubscriptionError.ts b/packages/storage-ui/src/Components/Modules/SubscriptionTab/utils/formatSubscriptionError.ts new file mode 100644 index 0000000000..904921634d --- /dev/null +++ b/packages/storage-ui/src/Components/Modules/SubscriptionTab/utils/formatSubscriptionError.ts @@ -0,0 +1,6 @@ +import { t } from "@lingui/macro" + +export const formatSubscriptionError = (e: any): string => + e.error.code === 400 && e.error.message.includes("declined") + ? t`The transaction was declined. Please use a different card or try again.` + : t`Failed to update the subscription. Please try again later.` \ No newline at end of file diff --git a/packages/storage-ui/src/Components/Pages/BillingHistory.tsx b/packages/storage-ui/src/Components/Pages/BillingHistory.tsx new file mode 100644 index 0000000000..3403099673 --- /dev/null +++ b/packages/storage-ui/src/Components/Pages/BillingHistory.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react" +import { makeStyles, createStyles } from "@chainsafe/common-theme" +import { CSSTheme } from "../../Themes/types" +import { Typography } from "@chainsafe/common-components" +import { Trans } from "@lingui/macro" +import InvoiceLines from "../Elements/InvoiceLines" +import PayInvoiceModal from "../Modules/SubscriptionTab/PayInvoice/PayInvoiceModal" + +const useStyles = makeStyles( + ({ constants, breakpoints }: CSSTheme) => + createStyles({ + heading: { + marginBottom: constants.generalUnit * 4, + [breakpoints.down("md")]: { + marginBottom: constants.generalUnit * 2 + } + }, + loader: { + marginTop: constants.generalUnit + }, + centered: { + textAlign: "center" + }, + root: { + [breakpoints.down("md")]: { + padding: `0 ${constants.generalUnit}px` + } + } + }) +) + +const BillingHistory = () => { + const classes = useStyles() + const [invoiceToPay, setInvoiceToPay] = useState() + + return ( +
+ + Billing history + + setInvoiceToPay(invoiceId)} /> + {invoiceToPay && setInvoiceToPay(undefined)} + />} +
+ ) +} + +export default BillingHistory diff --git a/packages/storage-ui/src/Components/Pages/BucketsPage.tsx b/packages/storage-ui/src/Components/Pages/BucketsPage.tsx index cfc174630b..9b81dd2528 100644 --- a/packages/storage-ui/src/Components/Pages/BucketsPage.tsx +++ b/packages/storage-ui/src/Components/Pages/BucketsPage.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react" +import React, { useEffect, useMemo, useState } from "react" import { makeStyles, createStyles } from "@chainsafe/common-theme" import { Button, @@ -114,10 +114,18 @@ const useStyles = makeStyles(({ breakpoints, animation, constants, typography }: const BucketsPage = () => { const classes = useStyles() - const { storageBuckets, createBucket } = useStorage() + const { storageBuckets, createBucket, refreshBuckets } = useStorage() const [isCreateBucketModalOpen, setIsCreateBucketModalOpen] = useState(false) + const bucketsToShow = useMemo(() => storageBuckets.filter(b => b.status === "created"), [storageBuckets]) + const bucketNameValidationSchema = useMemo( + () => bucketNameValidator(bucketsToShow.map(b => b.name)) + , [bucketsToShow] + ) - const bucketNameValidationSchema = useMemo(() => bucketNameValidator(storageBuckets.map(b => b.name)), [storageBuckets]) + useEffect(() => { + // this is needed for tests + refreshBuckets() + }, [refreshBuckets]) const formik = useFormik({ initialValues:{ @@ -194,12 +202,13 @@ const BucketsPage = () => { - {storageBuckets.map((bucket) => - - )} + {bucketsToShow + .map((bucket) => + + )} ) => TabPaneOrigin(props) const useStyles = makeStyles(({ constants, breakpoints, palette }: ITheme) => @@ -126,13 +129,14 @@ const useStyles = makeStyles(({ constants, breakpoints, palette }: ITheme) => const SettingsPage: React.FC = () => { const { desktop } = useThemeSwitcher() - const { path = desktop ? "apiKeys" : "" } = useParams<{path: SettingsPath}>() + const { path = desktop ? "apiKeys" : undefined } = useParams<{path: SettingsPath}>() const classes = useStyles() const { redirect } = useHistory() + const { isBillingEnabled } = useBilling() - + console.log("path", path) const onSelectTab = useCallback( - (path: string) => redirect(ROUTE_LINKS.Settings(path as SettingsPath)) + (path: SettingsPath) => redirect(ROUTE_LINKS.SettingsPath(path)) , [redirect]) return ( @@ -174,6 +178,18 @@ const SettingsPage: React.FC = () => { > + {isBillingEnabled + ? } + iconRight={} + > + + + : null}
} diff --git a/packages/storage-ui/src/Components/StorageRoutes.tsx b/packages/storage-ui/src/Components/StorageRoutes.tsx index ad6e53395a..ee7c9e0118 100644 --- a/packages/storage-ui/src/Components/StorageRoutes.tsx +++ b/packages/storage-ui/src/Components/StorageRoutes.tsx @@ -6,8 +6,10 @@ import CidsPage from "./Pages/CidsPage" import BucketsPage from "./Pages/BucketsPage" import SettingsPage from "./Pages/SettingsPage" import BucketPage from "./Pages/BucketPage" +import BillingHistory from "./Pages/BillingHistory" -export const SETTINGS_PATHS = ["apiKeys"] as const +export const SETTINGS_BASE = "/settings" +export const SETTINGS_PATHS = ["apiKeys", "plan"] as const export type SettingsPath = typeof SETTINGS_PATHS[number] export const ROUTE_LINKS = { @@ -15,7 +17,10 @@ export const ROUTE_LINKS = { Cids: "/cids", Buckets: "/buckets", SettingsRoot: "/settings", - Settings: (path: SettingsPath) => `/settings/${path}`, + Settings: `${SETTINGS_BASE}/:path`, + SettingsDefault: `${SETTINGS_BASE}`, + SettingsPath: (settingsPath: SettingsPath) => `${SETTINGS_BASE}/${settingsPath}`, + BillingHistory: "/billing-history", UserSurvey: "https://blocksurvey.io/survey/1K4bjDmqwtyAsehm1r4KbsdzRRDVyRCDoe/1541a8c4-275a-4e22-9547-570e94c5a55f", PrivacyPolicy: "https://storage.chainsafe.io/privacy-policy", Terms: "https://storage.chainsafe.io/terms-of-service", @@ -32,9 +37,9 @@ const StorageRoutes = () => { { redirectPath={ROUTE_LINKS.Landing} /> + void + currentSubscription: CurrentSubscription | undefined + changeSubscription: (newPriceId: string) => Promise + fetchCurrentSubscription: () => Promise + getAvailablePlans: () => Promise + deleteCard: (card: Card) => Promise + updateDefaultCard: (id: StripePaymentMethod["id"]) => Promise + invoices?: InvoiceResponse[] + cancelCurrentSubscription: () => Promise + isPendingInvoice: boolean + openInvoice?: InvoiceResponse + downloadInvoice: (invoiceId: string) => Promise + refreshInvoices: () => void + isBillingEnabled: boolean +} + +const ProductMapping: {[key: string]: { + name: string + description: string +}} = { + prod_JqS5E5vEhu95YG: { + name: t`Free plan`, + description: t`This is the free product.` + }, + prod_L7wbvnjEoUQyat: { + name: t`Storage Pro`, + description: t`Storage Pro` + }, + prod_LAqDoc8N18IOyp: { + name: t`Storage Max`, + description: t`Storage Max` + } +} + +const BillingContext = React.createContext( + undefined +) + +const BillingProvider = ({ children }: BillingContextProps) => { + const { storageApiClient, isLoggedIn + // accountRestricted + } = useStorageApi() + // const { redirect } = useHistory() + // const { addNotification, removeNotification } = useNotifications() + const { refreshBuckets } = useStorage() + const [currentSubscription, setCurrentSubscription] = useState() + const [defaultCard, setDefaultCard] = useState(undefined) + const [invoices, setInvoices] = useState() + const isPendingInvoice = useMemo(() => currentSubscription?.status === "pending_update", [currentSubscription]) + const openInvoice = useMemo(() => invoices?.find((i) => i.status === "open"), [invoices]) + // const [restrictedNotification, setRestrictedNotification] = useState() + // const [unpaidInvoiceNotification, setUnpaidInvoiceNotification] = useState() + // const [cardExpiringNotification, setCardExpiringNotification] = useState() + const [isBillingEnabled, setIsBillingEnabled] = useState(false) + + const refreshInvoices = useCallback(() => { + if (!currentSubscription) return + + storageApiClient.getAllInvoices(currentSubscription.id, 100) + .then(({ invoices }) => { + setInvoices(invoices.filter(i => i.status !== "void") + .sort((a, b) => b.period_start - a.period_start)) + }).catch((err) => { + console.error(err) + setInvoices([]) + }) + }, [currentSubscription, storageApiClient]) + + useEffect(() => { + refreshInvoices() + }, [refreshInvoices]) + + useEffect(() => { + if (!isLoggedIn) return + + storageApiClient.getEligibility() + .then(res => setIsBillingEnabled(res.is_eligible)) + .catch(console.error) + }, [storageApiClient, isLoggedIn]) + + // useEffect(() => { + // if (accountRestricted && !restrictedNotification) { + // const notif = addNotification({ + // createdAt: dayjs().unix(), + // title: t`Account is restricted`, + // onClick: () => redirect(ROUTE_LINKS.SettingsPath("plan")) + // }) + // setRestrictedNotification(notif) + // } else if (accountRestricted === false && restrictedNotification) { + // removeNotification(restrictedNotification) + // setRestrictedNotification(undefined) + // } + // }, [accountRestricted, addNotification, redirect, removeNotification, restrictedNotification]) + + // useEffect(() => { + // if (!!openInvoice && !unpaidInvoiceNotification) { + // const notif = addNotification({ + // createdAt: openInvoice.period_start, + // title: t`Invoice outstanding`, + // onClick: () => redirect(ROUTE_LINKS.SettingsPath("plan")) + // }) + // setUnpaidInvoiceNotification(notif) + // } else if (!openInvoice && unpaidInvoiceNotification) { + // removeNotification(unpaidInvoiceNotification) + // setUnpaidInvoiceNotification(undefined) + // } + // }, [addNotification, openInvoice, redirect, removeNotification, unpaidInvoiceNotification]) + + // useEffect(() => { + // if (defaultCard && currentSubscription) { + // if (!cardExpiringNotification && currentSubscription.expiry_date > + // dayjs(`${defaultCard.exp_year}-${defaultCard.exp_month}-01`, "YYYY-MM-DD").endOf("month").unix()) { + // const notif = addNotification({ + // createdAt: dayjs().unix(), + // title: t`Credit Card is expiring soon`, + // onClick: () => redirect(ROUTE_LINKS.SettingsPath("plan")) + // }) + // setCardExpiringNotification(notif) + // } else if (cardExpiringNotification && currentSubscription?.expiry_date <= + // dayjs(`${defaultCard?.exp_year}-${defaultCard?.exp_month}-01`, "YYYY-MM-DD").endOf("month").unix()) { + // removeNotification(cardExpiringNotification) + // setCardExpiringNotification(undefined) + // } + // } + // }, [addNotification, cardExpiringNotification, currentSubscription, defaultCard, redirect, removeNotification]) + + const refreshDefaultCard = useCallback(() => { + storageApiClient.getDefaultCard() + .then((card) => { + setDefaultCard(card) + }).catch((err) => { + console.error(err) + setDefaultCard(undefined) + }) + }, [storageApiClient]) + + const deleteCard = useCallback((card: Card) => + storageApiClient.deleteCard(card.id) + , [storageApiClient]) + + useEffect(() => { + if (isLoggedIn) { + refreshDefaultCard() + } + }, [refreshDefaultCard, isLoggedIn, storageApiClient]) + + const fetchCurrentSubscription = useCallback(() => { + return storageApiClient.getCurrentSubscription() + .then((subscription) => { + subscription.product.name = ProductMapping[subscription.product.id].name + subscription.product.description = ProductMapping[subscription.product.id].description + setCurrentSubscription(subscription) + return subscription + }) + .catch(console.error) + }, [storageApiClient]) + + useEffect(() => { + if (isLoggedIn && !currentSubscription) { + fetchCurrentSubscription() + } else if (!isLoggedIn) { + setCurrentSubscription(undefined) + } + }, [isLoggedIn, fetchCurrentSubscription, currentSubscription]) + + const getAvailablePlans = useCallback(() => { + return storageApiClient.getAllProducts() + .then((products) => { + return products.map(product => { + product.name = ProductMapping[product.id].name + product.description = ProductMapping[product.id].description + return product + }) + }) + .catch((error: any) => { + console.error(error) + return [] + }) + }, [storageApiClient]) + + const updateDefaultCard = useCallback((id: StripePaymentMethod["id"]) => + storageApiClient.updateDefaultCard({ id }) + , [storageApiClient]) + + const changeSubscription = useCallback((newPriceId: string) => { + if (!currentSubscription?.id) return Promise.resolve() + return storageApiClient.updateSubscription(currentSubscription.id, { + price_id: newPriceId, + payment_method: "stripe" + }) + .then(() => { + fetchCurrentSubscription() + refreshBuckets() + }) + .catch((error) => { + console.error(error) + return Promise.reject(error) + }) + }, [storageApiClient, currentSubscription, fetchCurrentSubscription, refreshBuckets]) + + const cancelCurrentSubscription = useCallback(() => { + if (!currentSubscription) + return Promise.reject("There is no current subscription") + + return storageApiClient.cancelSubscription(currentSubscription.id) + .then(() => { + fetchCurrentSubscription() + refreshBuckets() + }) + .catch((error) => { + console.error(error) + return Promise.reject() + }) + }, [currentSubscription, fetchCurrentSubscription, storageApiClient, refreshBuckets]) + + const downloadInvoice = useCallback(async (invoiceId: string) => { + try { + const result = await storageApiClient.downloadInvoice(invoiceId) + const link = document.createElement("a") + link.href = URL.createObjectURL(result.data) + link.download = "Chainsafe Files Invoice" + link.click() + } catch (error) { + console.error(error) + } + }, [storageApiClient]) + + return ( + + {children} + + ) +} + +const useBilling = () => { + const context = React.useContext(BillingContext) + if (context === undefined) { + throw new Error("useBilling must be used within a BillingProvider") + } + return context +} + +export { BillingProvider, useBilling } diff --git a/packages/storage-ui/src/Contexts/StorageContext.tsx b/packages/storage-ui/src/Contexts/StorageContext.tsx index 6d3d26c368..e7931cc5c8 100644 --- a/packages/storage-ui/src/Contexts/StorageContext.tsx +++ b/packages/storage-ui/src/Contexts/StorageContext.tsx @@ -66,6 +66,7 @@ type StorageContext = { storageBuckets: Bucket[] createBucket: (name: string) => Promise removeBucket: (id: string) => void + refreshBuckets: () => void } // This represents a File or Folder on the @@ -359,7 +360,8 @@ const StorageProvider = ({ children }: StorageContextProps) => { downloadFile, createBucket, removeBucket, - uploadFiles + uploadFiles, + refreshBuckets }} > {children} diff --git a/packages/storage-ui/src/Themes/Constants.ts b/packages/storage-ui/src/Themes/Constants.ts index 844ac3663b..e08e9c94a2 100644 --- a/packages/storage-ui/src/Themes/Constants.ts +++ b/packages/storage-ui/src/Themes/Constants.ts @@ -155,6 +155,11 @@ export interface CsSColors extends IConstants { borderColor?: string } } + addCard: { + color: string + shadow: string + placeholderColor: string + } surveyBanner: { color: string } diff --git a/packages/storage-ui/src/Themes/DarkTheme.ts b/packages/storage-ui/src/Themes/DarkTheme.ts index 483034e653..950fad33e7 100644 --- a/packages/storage-ui/src/Themes/DarkTheme.ts +++ b/packages/storage-ui/src/Themes/DarkTheme.ts @@ -471,6 +471,11 @@ export const darkTheme = createTheme({ }, surveyBanner: { color: "var(--gray9)" + }, + addCard: { + color: "#DBDBDB", + shadow: "0px 0px 4px rgba(24, 144, 255, 0.5)", + placeholderColor: "#595959" } } as CsSColors) }, diff --git a/packages/storage-ui/src/Themes/LightTheme.ts b/packages/storage-ui/src/Themes/LightTheme.ts index 048c875de4..062688e50d 100644 --- a/packages/storage-ui/src/Themes/LightTheme.ts +++ b/packages/storage-ui/src/Themes/LightTheme.ts @@ -158,6 +158,11 @@ export const lightTheme = createTheme({ }, surveyBanner: { color: "var(--gray1)" + }, + addCard: { + color: "#595959", + shadow: "0px 0px 4px rgba(24, 144, 255, 0.5)", + placeholderColor: "#BFBFBF" } } as CsSColors) }, diff --git a/packages/storage-ui/src/locales/en/messages.po b/packages/storage-ui/src/locales/en/messages.po index 6fa78f5051..a4b7476fb3 100644 --- a/packages/storage-ui/src/locales/en/messages.po +++ b/packages/storage-ui/src/locales/en/messages.po @@ -13,6 +13,21 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" +msgid "(Awaiting payment)" +msgstr "(Awaiting payment)" + +msgid "/month" +msgstr "/month" + +msgid "/year" +msgstr "/year" + +msgid "<0>{currentStorage} of storage" +msgstr "<0>{currentStorage} of storage" + +msgid "<0>{planStorageCapacity} of storage" +msgstr "<0>{planStorageCapacity} of storage" + msgid "A bucket with this name already exists" msgstr "A bucket with this name already exists" @@ -22,21 +37,69 @@ msgstr "A file with the same name already exists" msgid "API Keys" msgstr "API Keys" +msgid "Accepted currencies" +msgstr "Accepted currencies" + +msgid "Access your billing history in settings or view your" +msgstr "Access your billing history in settings or view your" + msgid "Add API Key" msgstr "Add API Key" +msgid "Add Card" +msgstr "Add Card" + msgid "Add S3 Key" msgstr "Add S3 Key" +msgid "Add a credit card" +msgstr "Add a credit card" + +msgid "Add card" +msgstr "Add card" + +msgid "Add credit card" +msgstr "Add credit card" + msgid "Add more files" msgstr "Add more files" +msgid "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." +msgstr "All crypto payments are final and ineligible for credits, exchanges or refunds. If you wish to change your plan, your funds will not be reimbursed." + +msgid "All invoices" +msgstr "All invoices" + +msgid "Amount added to credit" +msgstr "Amount added to credit" + +msgid "Amount available from last bill" +msgstr "Amount available from last bill" + +msgid "Amount due" +msgstr "Amount due" + +msgid "Amount from credit" +msgstr "Amount from credit" + +msgid "Annual billing{0}" +msgstr "Annual billing{0}" + msgid "Api Keys" msgstr "Api Keys" msgid "Are we on the right track? Let us know in less than 1 minute." msgstr "Are we on the right track? Let us know in less than 1 minute." +msgid "Are you sure? This will delete your default payment method." +msgstr "Are you sure? This will delete your default payment method." + +msgid "Billing history" +msgstr "Billing history" + +msgid "Billing start time" +msgstr "Billing start time" + msgid "Bucket name" msgstr "Bucket name" @@ -49,6 +112,9 @@ msgstr "Buckets" msgid "By connecting your wallet, you agree to our <0>Terms of Service and <1>Privacy Policy" msgstr "By connecting your wallet, you agree to our <0>Terms of Service and <1>Privacy Policy" +msgid "By switching plan, you will loose access to:" +msgstr "By switching plan, you will loose access to:" + msgid "CID invalid" msgstr "CID invalid" @@ -61,9 +127,24 @@ msgstr "CIDs" msgid "Cancel" msgstr "Cancel" +msgid "Card added" +msgstr "Card added" + +msgid "Card inputs invalid" +msgstr "Card inputs invalid" + +msgid "Card updated" +msgstr "Card updated" + msgid "Chainsafe Storage Beta does not encrypt data. All data uploaded is publicly accessible on IPFS network." msgstr "Chainsafe Storage Beta does not encrypt data. All data uploaded is publicly accessible on IPFS network." +msgid "Change Plan" +msgstr "Change Plan" + +msgid "Change plan" +msgstr "Change plan" + msgid "Check your inbox! We've sent another email." msgstr "Check your inbox! We've sent another email." @@ -76,9 +157,24 @@ msgstr "Cids" msgid "Click or drag to upload files" msgstr "Click or drag to upload files" +msgid "Close" +msgstr "Close" + msgid "Confirm" msgstr "Confirm" +msgid "Confirm plan change" +msgstr "Confirm plan change" + +msgid "Confirm plan downgrade" +msgstr "Confirm plan downgrade" + +msgid "Confirmation" +msgstr "Confirmation" + +msgid "Connect Wallet" +msgstr "Connect Wallet" + msgid "Connect Wallet to Storage" msgstr "Connect Wallet to Storage" @@ -88,6 +184,9 @@ msgstr "Connect a new wallet" msgid "Connection failed" msgstr "Connection failed" +msgid "Contact us" +msgstr "Contact us" + msgid "Continue" msgstr "Continue" @@ -124,6 +223,9 @@ msgstr "Created" msgid "Created At" msgstr "Created At" +msgid "DAI, USDC, ETH or BTC" +msgstr "DAI, USDC, ETH or BTC" + msgid "Date uploaded" msgstr "Date uploaded" @@ -142,6 +244,9 @@ msgstr "Delete selected" msgid "Deletion successful" msgstr "Deletion successful" +msgid "Destination Address" +msgstr "Destination Address" + msgid "Didn't receive the email ?" msgstr "Didn't receive the email ?" @@ -157,15 +262,33 @@ msgstr "Download complete" msgid "Drop to upload files" msgstr "Drop to upload files" +msgid "Edit payment" +msgstr "Edit payment" + +msgid "Edit payment method" +msgstr "Edit payment method" + +msgid "Edit plan" +msgstr "Edit plan" + msgid "Email is required" msgstr "Email is required" msgid "Enter the verification code:" msgstr "Enter the verification code:" +msgid "Failed to add payment method" +msgstr "Failed to add payment method" + +msgid "Failed to create a charge" +msgstr "Failed to create a charge" + msgid "Failed to get signature" msgstr "Failed to get signature" +msgid "Failed to update the subscription. Please try again later." +msgstr "Failed to update the subscription. Please try again later." + msgid "" "Failed to validate signature.\n" "If you are using a contract wallet, please make \n" @@ -175,6 +298,9 @@ msgstr "" "If you are using a contract wallet, please make \n" "sure you have activated your wallet." +msgid "Features" +msgstr "Features" + msgid "File" msgstr "File" @@ -190,12 +316,24 @@ msgstr "Folder name is already in use" msgid "Folder uploads are currently not supported" msgstr "Folder uploads are currently not supported" +msgid "Free" +msgstr "Free" + +msgid "Free plan" +msgstr "Free plan" + msgid "Get Started" msgstr "Get Started" +msgid "Go back" +msgstr "Go back" + msgid "Go back" msgstr "Go back" +msgid "Have a question?" +msgstr "Have a question?" + msgid "Hold on, we are logging you in…" msgstr "Hold on, we are logging you in…" @@ -205,6 +343,9 @@ msgstr "Id" msgid "Info" msgstr "Info" +msgid "Insufficient balance" +msgstr "Insufficient balance" + msgid "Key ID" msgstr "Key ID" @@ -232,18 +373,33 @@ msgstr "New Key" msgid "New folder" msgstr "New folder" +msgid "Next payment" +msgstr "Next payment" + +msgid "No Card" +msgstr "No Card" + msgid "No files to show" msgstr "No files to show" msgid "No folders" msgstr "No folders" +msgid "No invoice found" +msgstr "No invoice found" + msgid "OK" msgstr "OK" +msgid "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." +msgstr "Once you proceed, your account is expected to make a payment within 60 minutes. If no payment is received , your plan will not change." + msgid "One sec, getting files ready..." msgstr "One sec, getting files ready..." +msgid "Only send the exact amount of {0} to this address" +msgstr "Only send the exact amount of {0} to this address" + msgid "Open on Gateway" msgstr "Open on Gateway" @@ -253,12 +409,42 @@ msgstr "Or using one of the following:" msgid "Paste the CID to pin it with ChainSafe Storage" msgstr "Paste the CID to pin it with ChainSafe Storage" +msgid "Pay now" +msgstr "Pay now" + +msgid "Pay with Crypto" +msgstr "Pay with Crypto" + +msgid "Pay with crypto" +msgstr "Pay with crypto" + +msgid "Pay with {0}" +msgstr "Pay with {0}" + +msgid "Payment information" +msgstr "Payment information" + +msgid "Payment method" +msgstr "Payment method" + +msgid "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." +msgstr "Payments are final and non-refundable. If you wish to change your plan, any extra funds will be applied as credit towards future payments." + msgid "Pin" msgstr "Pin" msgid "Pin CID" msgstr "Pin CID" +msgid "Plan changed successfully" +msgstr "Plan changed successfully" + +msgid "Plan price" +msgstr "Plan price" + +msgid "Please complete payment of the following outstanding invoices in order to avoid account suspension" +msgstr "Please complete payment of the following outstanding invoices in order to avoid account suspension" + msgid "Please enter a file name" msgstr "Please enter a file name" @@ -274,15 +460,24 @@ msgstr "Please select a file to upload" msgid "Preview" msgstr "Preview" +msgid "Pricing details" +msgstr "Pricing details" + msgid "Privacy Policy" msgstr "Privacy Policy" +msgid "Proceed to payment" +msgstr "Proceed to payment" + msgid "Recover" msgstr "Recover" msgid "Recover selected" msgstr "Recover selected" +msgid "Remove" +msgstr "Remove" + msgid "Rename" msgstr "Rename" @@ -295,9 +490,24 @@ msgstr "Rename folder" msgid "Secret" msgstr "Secret" +msgid "See payment info" +msgstr "See payment info" + +msgid "Select a cryptocurrency" +msgstr "Select a cryptocurrency" + msgid "Select a wallet" msgstr "Select a wallet" +msgid "Select payment method" +msgstr "Select payment method" + +msgid "Select plan" +msgstr "Select plan" + +msgid "Select this plan" +msgstr "Select this plan" + msgid "Send another email" msgstr "Send another email" @@ -334,6 +544,27 @@ msgstr "Start Upload" msgid "Status" msgstr "Status" +msgid "Storage Max" +msgstr "Storage Max" + +msgid "Storage Pro" +msgstr "Storage Pro" + +msgid "Subscription Plan" +msgstr "Subscription Plan" + +msgid "Subscription plan" +msgstr "Subscription plan" + +msgid "Switch Network" +msgstr "Switch Network" + +msgid "Switch Plan" +msgstr "Switch Plan" + +msgid "Switch to Free plan" +msgstr "Switch to Free plan" + msgid "System maintenance is scheduled to start at {0}. The system will be unavailable." msgstr "System maintenance is scheduled to start at {0}. The system will be unavailable." @@ -349,6 +580,15 @@ msgstr "The files are already in this folder" msgid "The name cannot contain '/' character" msgstr "The name cannot contain '/' character" +msgid "The time to pay with crypto is up. Updating your plan..." +msgstr "The time to pay with crypto is up. Updating your plan..." + +msgid "The transaction was declined. Please use a different card or try again." +msgstr "The transaction was declined. Please use a different card or try again." + +msgid "There was a problem creating a charge {error}" +msgstr "There was a problem creating a charge {error}" + msgid "There was an error authenticating" msgstr "There was an error authenticating" @@ -364,6 +604,18 @@ msgstr "There was an error deleting this item" msgid "There was an error moving this" msgstr "There was an error moving this" +msgid "This card will become your default payment method" +msgstr "This card will become your default payment method" + +msgid "This is the free product." +msgstr "This is the free product." + +msgid "Total" +msgstr "Total" + +msgid "Total Amount" +msgstr "Total Amount" + msgid "Try again" msgstr "Try again" @@ -376,6 +628,18 @@ msgstr "Unpin" msgid "Update" msgstr "Update" +msgid "Update Card" +msgstr "Update Card" + +msgid "Update card" +msgstr "Update card" + +msgid "Update credit card" +msgstr "Update credit card" + +msgid "Update your credit card" +msgstr "Update your credit card" + msgid "Upload" msgstr "Upload" @@ -388,6 +652,9 @@ msgstr "Uploading {0} files" msgid "Use a different login method" msgstr "Use a different login method" +msgid "Use this card" +msgstr "Use this card" + msgid "Using an email:" msgstr "Using an email:" @@ -400,6 +667,9 @@ msgstr "Verification code not correct!" msgid "Verification code sent!" msgstr "Verification code sent!" +msgid "View PDF" +msgstr "View PDF" + msgid "View folder" msgstr "View folder" @@ -415,20 +685,50 @@ msgstr "We’ve sent an email to {email}. It contains a verification code that msgid "You can't move folders to this path" msgstr "You can't move folders to this path" +msgid "You get access to these features right now." +msgstr "You get access to these features right now." + +msgid "You now have:" +msgstr "You now have:" + msgid "You will need to sign a message in your wallet to complete sign in." msgstr "You will need to sign a message in your wallet to complete sign in." +msgid "Your content exceeds the {planStorageCapacity} storage capacity for this plan." +msgstr "Your content exceeds the {planStorageCapacity} storage capacity for this plan." + +msgid "expires" +msgstr "expires" + msgid "file" msgstr "file" msgid "folder" msgstr "folder" +msgid "invoices here" +msgstr "invoices here" + msgid "moved successfully" msgstr "moved successfully" msgid "{0, plural, one {You are about to delete {1} item.} other {You are about to delete {2} items.}}" msgstr "{0, plural, one {You are about to delete {1} item.} other {You are about to delete {2} items.}}" +msgid "{0} of {1} used" +msgstr "{0} of {1} used" + +msgid "{currentPlanStorage} of storage" +msgstr "{currentPlanStorage} of storage" + +msgid "{newPlanCapacity} of storage" +msgstr "{newPlanCapacity} of storage" + +msgid "{newPlanStorage} of storage" +msgstr "{newPlanStorage} of storage" + msgid "{noOfFiles, plural, one {Uploading {noOfFiles} file} other {Uploading {noOfFiles} files}}" msgstr "{noOfFiles, plural, one {Uploading {noOfFiles} file} other {Uploading {noOfFiles} files}}" + +msgid "{planStorageCapacity} of storage" +msgstr "{planStorageCapacity} of storage" diff --git a/yarn.lock b/yarn.lock index 8a3935a276..c979621344 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1850,6 +1850,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.16.0": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.4.0", "@babel/template@^7.8.6": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -1925,19 +1932,10 @@ resolved "https://registry.yarnpkg.com/@chainsafe/browser-storage-hooks/-/browser-storage-hooks-1.0.1.tgz#26d32cde1999914db755a631e2643823c54959f7" integrity sha512-Q4b5gQAZnsRXKeADspd5isqfwwhhXjDk70y++YadufA6EZ3tf340oW0OVszp74KaGEw+CAYFGQR4X7bzpZ3x9Q== -"@chainsafe/files-api-client@1.18.25": - version "1.18.25" - resolved "https://registry.yarnpkg.com/@chainsafe/files-api-client/-/files-api-client-1.18.25.tgz#e30398ad1b2033735528a97c3d129c8e734a65f1" - integrity sha512-nHARiVcspMZZ54sUs02w8baj0DXh4I1bPOyyOSGKOmy8gcHN1yxUx7XHmq21+lICU7Vv+ziddDY6bGYyHTSKuw== - dependencies: - "@redocly/openapi-cli" "^1.0.0-beta.58" - "@redocly/openapi-core" "^1.0.0-beta.69" - redoc-cli "^0.12.3" - -"@chainsafe/files-api-client@1.18.26": - version "1.18.26" - resolved "https://registry.yarnpkg.com/@chainsafe/files-api-client/-/files-api-client-1.18.26.tgz#47efee4431757e9ad8698fdd26cebf4b1e44f390" - integrity sha512-m+3q8kIB86aU4xDLTn+Q+pz0QZ8IsmBPWkSZcJGgWy2hyVSgLWTYFGDozxe04IMKCxhRjhFqOIn2b6ryETzqTg== +"@chainsafe/files-api-client@1.18.29": + version "1.18.29" + resolved "https://registry.yarnpkg.com/@chainsafe/files-api-client/-/files-api-client-1.18.29.tgz#49d9970fe6d29b213e9841a49eaeda60314bec0b" + integrity sha512-kleqt3SZtobYzMflAvmfvmQKtswxyiEiKBTCalRKKG8+iTzkRyYVR3OKB961ftUl/RzxfiJdjnSEIh/frXRHOA== dependencies: "@redocly/openapi-cli" "^1.0.0-beta.58" "@redocly/openapi-core" "^1.0.0-beta.69" @@ -2254,7 +2252,7 @@ resolved "https://registry.yarnpkg.com/@ensdomains/resolver/-/resolver-0.2.4.tgz#c10fe28bf5efbf49bff4666d909aed0265efbc89" integrity sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA== -"@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.3.0", "@ethereumjs/common@^2.4.0": +"@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.4.0.tgz#2d67f6e6ba22246c5c89104e6b9a119fb3039766" integrity sha512-UdkhFWzWcJCZVsj1O/H8/oqj/0RVYjLc1OhPjBrQdALAkQHpCp8xXI4WLnuGTADqTdJZww0NtgwG+TRPkXt27w== @@ -2262,7 +2260,7 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.0" -"@ethereumjs/tx@^3.0.0", "@ethereumjs/tx@^3.2.1": +"@ethereumjs/tx@^3.0.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.0.tgz#14ed1b7fa0f28e1cd61e3ecbdab824205f6a4378" integrity sha512-yTwEj2lVzSMgE6Hjw9Oa1DZks/nKTWM8Wn4ykDNapBPua2f4nXO3qKnni86O6lgDj5fVNRqbDsD0yy7/XNGDEA== @@ -5634,141 +5632,131 @@ dependencies: "@babel/runtime" "^7.10.2" -"@tkey/common-types@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/common-types/-/common-types-3.14.2.tgz#f7e71b3773e9871d8bf15588c2dfa8b6fbc6bd40" - integrity sha512-j5BkBAGKApj7PABfdGXYLpK9PRpz1fRFZXe1qt/RgapCIJ33YC4fudYLvSNVWX9ujUrQsAi929g2q1VxRFVPNg== +"@tkey/common-types@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/common-types/-/common-types-5.1.0.tgz#b8bacd387a1a10d8250fafb1ddf30998f10a826e" + integrity sha512-H/xnaxYq5vRqUWb/XT4uOAoyHN/bROkp6cnfJOy6uG+svlBQGQsqtuTc2pSNZGNg+9qS6EhUx+uwPsAZhWRjqg== dependencies: - "@toruslabs/eccrypto" "^1.1.6" - "@toruslabs/torus-direct-web-sdk" "^4.12.0" + "@toruslabs/customauth" "^7.0.0" + "@toruslabs/eccrypto" "^1.1.8" bn.js "^5.2.0" elliptic "^6.5.4" ts-custom-error "^3.2.0" - web3-utils "^1.3.5" + web3-utils "^1.7.0" -"@tkey/core@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/core/-/core-3.14.2.tgz#45ae48b0363d24de08683680aac3e584ba2d442e" - integrity sha512-SPbpPmW/+RhcwBy57/0+FaOespznxLaA+ZKkmDm0M+U72hSzQirGSBwhitjtMFGdbXpIPVi71PVw3vOpn/OHDg== +"@tkey/core@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/core/-/core-5.1.0.tgz#0783b5dc6f04f2f48fad1ae7f44b1938fd678353" + integrity sha512-m4Z65K6vyjM9Inf0kPOWkVrlNpX61tEwvYmzxGiFmYk4QRUNVgf3qdAikk3KCkOURokulJxlHtWyCZm1rMG/+g== dependencies: - "@tkey/common-types" "^3.14.2" - "@toruslabs/eccrypto" "^1.1.6" - "@toruslabs/http-helpers" "^1.3.7" + "@tkey/common-types" "^5.1.0" + "@toruslabs/eccrypto" "^1.1.8" + "@toruslabs/http-helpers" "^2.2.0" bn.js "^5.2.0" elliptic "^6.5.4" json-stable-stringify "^1.0.1" - web3-utils "^1.3.5" - -"@tkey/default@3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/default/-/default-3.14.2.tgz#837b2f68d408adb38ef68bab672f1e9f8e213e51" - integrity sha512-KkkDaRf29ycrW6rGw6xbFuglphYZMxgu1Is4s2LFS44cRGYmdn5kSL9SGKmA8WNra72xYTkPCcuNikyjacCb5A== - dependencies: - "@tkey/common-types" "^3.14.2" - "@tkey/core" "^3.14.2" - "@tkey/private-keys" "^3.14.2" - "@tkey/security-questions" "^3.14.2" - "@tkey/seed-phrase" "^3.14.2" - "@tkey/service-provider-base" "^3.14.2" - "@tkey/service-provider-torus" "^3.14.2" - "@tkey/share-serialization" "^3.14.2" - "@tkey/share-transfer" "^3.14.2" - "@tkey/storage-layer-torus" "^3.14.2" - "@toruslabs/eccrypto" "^1.1.6" - bn.js "^5.2.0" - web3-utils "^1.3.5" - -"@tkey/private-keys@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/private-keys/-/private-keys-3.14.2.tgz#7b6f1bbbc54d22a3534647393299d54ec0c0a1f8" - integrity sha512-93bBsUrEMyWX/yJQXsH103yM9zOAAKmxvjnqU0cd0v3kT7MC4fMoWSiS0BIW482DrQ6lVPLalV4t/q7nWisnsw== - dependencies: - "@tkey/common-types" "^3.14.2" - bn.js "^5.2.0" - randombytes "^2.1.0" + web3-utils "^1.7.0" -"@tkey/security-questions@3.14.2", "@tkey/security-questions@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/security-questions/-/security-questions-3.14.2.tgz#f1a9fb21b0b0a26424c9e7036d2b42fe73c4fa02" - integrity sha512-aTiDAB4AlHFuQHrRTunIQsZcoInocCsV0DlZue3R6eqN22cyhyAC0EDK9Z4iGa6nn4gNDBORZ/jl95Zkn9ueew== - dependencies: - "@tkey/common-types" "^3.14.2" +"@tkey/default@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/default/-/default-5.1.0.tgz#03d29f7c77844e7b7ea0a33ab7101a086bce8a51" + integrity sha512-puYRmEj5ZB8VdP3qO5nXrK6NC8ZV2luJVfZ4C+EJw8wGHIjvTrIQe/2alS0ZFLoVtLaMKOMLJHoSi+ndxVBjeg== + dependencies: + "@tkey/common-types" "^5.1.0" + "@tkey/core" "^5.1.0" + "@tkey/security-questions" "^5.1.0" + "@tkey/service-provider-base" "^5.1.0" + "@tkey/service-provider-torus" "^5.1.0" + "@tkey/share-serialization" "^5.1.0" + "@tkey/share-transfer" "^5.1.0" + "@tkey/storage-layer-torus" "^5.1.0" bn.js "^5.2.0" - web3-utils "^1.3.5" -"@tkey/seed-phrase@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/seed-phrase/-/seed-phrase-3.14.2.tgz#c59f43fcfa3f78b4c51ac6fb2254c598c70eebcb" - integrity sha512-4WxJpBjS5wb4K3BXgtlxZ0XyL0Xt1HXKLLoNwW6kP6qbfb/ibPbTxqxR/I+E3h+Z8zZ6Ll7oTa0lo4JC+nUpkA== +"@tkey/security-questions@5.1.0", "@tkey/security-questions@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/security-questions/-/security-questions-5.1.0.tgz#d8e5050f1e9c9baa417b08eca6c045eed2b647d7" + integrity sha512-5iUfjcBt6o7ZK0iu/kQUUlJjXnn1lR/06DldwQiOErfAQ4FrbemA7/E3Nd6bUWrSJAcYRc1dvZC6cYV+m1mJSA== dependencies: - "@tkey/common-types" "^3.14.2" - bip39 "^3.0.4" + "@tkey/common-types" "^5.1.0" bn.js "^5.2.0" - hdkey "^2.0.1" - web3-core "^1.3.5" - web3-eth "^1.3.5" - web3-utils "^1.3.5" + web3-utils "^1.7.0" -"@tkey/service-provider-base@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/service-provider-base/-/service-provider-base-3.14.2.tgz#e1792f98c0bc3d3e498fafcd02db8cf62549cc3a" - integrity sha512-Kyt24x6S+4n3+bLxmrIiqBiZgjswjod/QLtl/0ClbMwrkGak4UlrpEz4zugI1s7MrLNb4QP2ZcuuTLIymP2Ipg== +"@tkey/service-provider-base@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/service-provider-base/-/service-provider-base-5.1.0.tgz#25e5672d490ca0a1255dbfb8731f500033d8d154" + integrity sha512-/ng8W94uZHeIptgqh3BAHgXyd23IsRaWPn1/72o8Lvf/Zzi/K6baOmn8b9vcm26p0vBVzSRc8j9V0g8OyQ4tcQ== dependencies: - "@tkey/common-types" "^3.14.2" + "@tkey/common-types" "^5.1.0" bn.js "^5.2.0" elliptic "^6.5.4" -"@tkey/service-provider-torus@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/service-provider-torus/-/service-provider-torus-3.14.2.tgz#5ccb3c091e46eccf577d8d4f63405a10d19ed530" - integrity sha512-iHUl9CAZZ7rhZFdIXoAuIe2k8YbF0iBcm6NX5CS5RzYjlZ160Yiq9NUo3ro9JWvc+E/0uumMhk1VGrYGhl4IIg== +"@tkey/service-provider-torus@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/service-provider-torus/-/service-provider-torus-5.1.0.tgz#4d75db661c07370fee20a3e86072a2e4c600ead4" + integrity sha512-EucrT5e+vtSGd7I/iruOC+vfi8ugK82g3T4ji6f9uXVVoLdzMkayRcNo40uYIJ4ohzvbNpQXg0dcNES9cVDQcw== dependencies: - "@tkey/common-types" "^3.14.2" - "@tkey/service-provider-base" "^3.14.2" - "@toruslabs/torus-direct-web-sdk" "^4.12.0" + "@tkey/common-types" "^5.1.0" + "@tkey/service-provider-base" "^5.1.0" + "@toruslabs/customauth" "^7.0.0" bn.js "^5.2.0" elliptic "^6.5.4" -"@tkey/share-serialization@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/share-serialization/-/share-serialization-3.14.2.tgz#f20b6ee102a65d8113d0d8a884a7e607d1dce337" - integrity sha512-W1nkb6hTkzAgnJjqIRDuj1w5AoDIHLCZPl0hrgN6VQnZRgPSfA2iVLhjpe3Uwn+wMUVurjrWi608mwuHQHVU7g== +"@tkey/share-serialization@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/share-serialization/-/share-serialization-5.1.0.tgz#1b807deff34901c0f1b6dea4c79ef0e3a8b8cee2" + integrity sha512-rMo/WQ+iSDkNoUGu3z4TjJMty3vfM/PtV8+quA1tE1pUpP1DMeLBg0uYWhsQik6KDElgARTOtsUzbh6ZWJlp+g== dependencies: - "@tkey/common-types" "^3.14.2" + "@tkey/common-types" "^5.1.0" "@types/create-hash" "1.2.2" bn.js "^5.2.0" create-hash "^1.2.0" -"@tkey/share-transfer@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/share-transfer/-/share-transfer-3.14.2.tgz#beae019d77f1eaea472bd22e136fe1362be58054" - integrity sha512-mzJuq7HHRxGTt6Nrp+fj1MJWDnjttPuJ4SVpdbCIWCV5Lvizj1Z5MmkEQQj0bHeUayFwEMs1eoLquaht3KgtPA== +"@tkey/share-transfer@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/share-transfer/-/share-transfer-5.1.0.tgz#96ce677f27e8e8af00e9c45eabeb869ae62a34e7" + integrity sha512-XXqp7TZE2388XQ97KzL5AxRJCcEtFSkti2xl0PG4vxx2ydv1wgfp0oR0r/kYrshfPaH6aCmDGwdJ+xSfHPIflw== dependencies: - "@tkey/common-types" "^3.14.2" - "@toruslabs/eccrypto" "^1.1.6" - "@toruslabs/http-helpers" "^1.3.7" + "@tkey/common-types" "^5.1.0" + "@toruslabs/eccrypto" "^1.1.8" + "@toruslabs/http-helpers" "^2.2.0" bn.js "^5.2.0" -"@tkey/storage-layer-torus@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/storage-layer-torus/-/storage-layer-torus-3.14.2.tgz#36f7291ca6a69f90faec2e0b245f17a85f547215" - integrity sha512-t511F8H2dxAgiXrHrgtwBWeETCYdMgCZCn/neAjE1PzMjiCY+hwo0HBd91nC/8/Lw1klKKXHz4ybvoI6ytSjSQ== +"@tkey/storage-layer-torus@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/storage-layer-torus/-/storage-layer-torus-5.1.0.tgz#6d2108c7fcde81a6de74437f155e5e7caae2b648" + integrity sha512-AwcQj0HsVTCP29+KLZ6q+edcfXOEanHkXpQ7fIpI+Waq28k738qXWQtE8ApvaywA597nXpiy2kcIv9kYFERdqQ== dependencies: - "@tkey/common-types" "^3.14.2" - "@toruslabs/http-helpers" "^1.3.7" + "@tkey/common-types" "^5.1.0" + "@toruslabs/http-helpers" "^2.2.0" bn.js "^5.2.0" json-stable-stringify "^1.0.1" - web3-utils "^1.3.5" + web3-utils "^1.7.0" -"@tkey/web-storage@3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@tkey/web-storage/-/web-storage-3.14.2.tgz#3406a4c0975b8e2f077e67826f0320979c86785f" - integrity sha512-0295ZfPV10B/oArehrKu1tYpsnxR+mVJkUNdcIPMGDxV7xBohPj7CHcSkMwXkviIpbyP9ISMFMrC7UIjLhBC4g== +"@tkey/web-storage@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@tkey/web-storage/-/web-storage-5.1.0.tgz#7343bca2be3760af7e5cadc6a9d11b6e223a2cad" + integrity sha512-liolN8rfEt7S6Qgaj4LKC4STeHcvNcZ1N5odPjvQlyup6Owx7/xMh28n5FxDBgCjDXR/xjEV99OWs/KoPJk1CA== dependencies: - "@tkey/common-types" "^3.14.2" + "@tkey/common-types" "^5.1.0" "@types/bn.js" "^5.1.0" bn.js "^5.2.0" +"@toruslabs/customauth@7.0.2", "@toruslabs/customauth@^7.0.0": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@toruslabs/customauth/-/customauth-7.0.2.tgz#0652bd9ab89f2f9fb021bb1d12a6f8f2ae8e0bd9" + integrity sha512-1+O0nD9hExfmRk8RL5D+luJ3Arpx3H8fz/vhuUIVVxC+ZaTu1W0m03cS9YUXKm7HeZI81cOI78KOvc0GsgjWwA== + dependencies: + "@chaitanyapotti/register-service-worker" "^1.7.3" + "@toruslabs/fetch-node-details" "^5.0.1" + "@toruslabs/http-helpers" "^2.2.0" + "@toruslabs/torus.js" "^5.0.1" + broadcast-channel "^4.10.0" + events "^3.3.0" + jwt-decode "^3.1.2" + lodash.merge "^4.6.2" + loglevel "^1.8.0" + web3-utils "^1.7.1" + "@toruslabs/eccrypto@^1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-1.1.6.tgz#ce877cf00d6f9cf7ab3daa6ac4d6d540110b813b" @@ -5793,6 +5781,18 @@ optionalDependencies: secp256k1 "^3.8.0" +"@toruslabs/eccrypto@^1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-1.1.8.tgz#ce1eac9c3964a091cdc74956a62036b5719a41eb" + integrity sha512-5dIrO2KVqvnAPOPfJ2m6bnjp9vav9GIcCZXiXRW/bJuIDRLVxJhVvRlleF4oaEZPq5yX5piHq5jVHagNNS0jOQ== + dependencies: + acorn "^8.4.1" + elliptic "^6.5.4" + es6-promise "^4.2.8" + nan "^2.14.2" + optionalDependencies: + secp256k1 "^3.8.0" + "@toruslabs/fetch-node-details@^2.6.1": version "2.6.1" resolved "https://registry.yarnpkg.com/@toruslabs/fetch-node-details/-/fetch-node-details-2.6.1.tgz#33b33d4dc825c47666a4e96df22ac0ba7f2296d6" @@ -5809,6 +5809,14 @@ web3-eth-contract "^1.5.2" web3-utils "^1.5.2" +"@toruslabs/fetch-node-details@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/fetch-node-details/-/fetch-node-details-5.0.1.tgz#65132921ec3bc3cb78579b03a0c6b0abbf14cc50" + integrity sha512-d7JlzX+Cp9wEXdW4xvj2qClrgPYOKJqaWTzVJJ5gK+KBISaGywrHz1xYSTviHK98SMGoywOLrVAPcHJ1lhqrMQ== + dependencies: + web3-eth-contract "^1.7.0" + web3-utils "^1.7.0" + "@toruslabs/http-helpers@^1.3.7": version "1.3.7" resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-1.3.7.tgz#c2cf64ba628699a01d2808d21ec2158688f2ed75" @@ -5823,7 +5831,15 @@ dependencies: deepmerge "^4.2.2" -"@toruslabs/torus-direct-web-sdk@4.15.1", "@toruslabs/torus-direct-web-sdk@^4.12.0": +"@toruslabs/http-helpers@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-2.2.0.tgz#c494984701ff60eb93c0eaef279daa93b5bcea81" + integrity sha512-xkzZZuE+DmWmJBTYneCrMJY24izNQCOdoJMpsXKQx20Va/rTQvNPbdkpx9LBf/pisk8jOwETNAfFQ8YTBc/bZw== + dependencies: + lodash.merge "^4.6.2" + loglevel "^1.8.0" + +"@toruslabs/torus-direct-web-sdk@4.15.1": version "4.15.1" resolved "https://registry.yarnpkg.com/@toruslabs/torus-direct-web-sdk/-/torus-direct-web-sdk-4.15.1.tgz#9d1011e8bf93c9d166f1b6d80f86706769159a4d" integrity sha512-InNh2XwQKiFNEId8uEhO3ywDdXT/VMZFz3CZD8DAaUP73cM25kM8bSmS60xWeFsGRFHMULaPnL/4+N02eGzXMg== @@ -5890,6 +5906,19 @@ memory-cache "^0.2.0" web3-utils "^1.5.2" +"@toruslabs/torus.js@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/torus.js/-/torus.js-5.0.1.tgz#4c95275e7abac8956fc45c1d41837ee90a3a45cf" + integrity sha512-rhICMJuAIA/LiS/IuWCqKEqpP8nezw/Hhlphr0VIMelkaQTqAZbS/g1cwlAdPezenTS2CTULfx509pifMLMl/w== + dependencies: + "@toruslabs/eccrypto" "^1.1.8" + "@toruslabs/http-helpers" "^2.2.0" + bn.js "^5.2.0" + elliptic "^6.5.4" + json-stable-stringify "^1.0.1" + loglevel "^1.8.0" + web3-utils "^1.7.0" + "@types/aria-query@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" @@ -8919,7 +8948,7 @@ bip32@^2.0.5: typeforce "^1.11.5" wif "^2.0.6" -bip39@^3.0.2, bip39@^3.0.4: +bip39@^3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0" integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw== @@ -9157,6 +9186,20 @@ broadcast-channel@^3.7.0: rimraf "3.0.2" unload "2.2.0" +broadcast-channel@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.10.0.tgz#d19fb902df227df40b1b580351713d30c302d198" + integrity sha512-hOUh312XyHk6JTVyX9cyXaH1UYs+2gHVtnW16oQAu9FL7ALcXGXc/YoJWqlkV8vUn14URQPMmRi4A9q4UrwVEQ== + dependencies: + "@babel/runtime" "^7.16.0" + detect-node "^2.1.0" + microseconds "0.2.0" + nano-time "1.0.0" + oblivious-set "1.0.0" + p-queue "6.6.2" + rimraf "3.0.2" + unload "2.3.1" + brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -10526,7 +10569,7 @@ crypto-addr-codec@^0.1.7: safe-buffer "^5.2.0" sha3 "^2.1.1" -crypto-browserify@3.12.0, crypto-browserify@^3.11.0, crypto-browserify@^3.12.0: +crypto-browserify@^3.11.0, crypto-browserify@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== @@ -11212,16 +11255,16 @@ detect-node-es@^1.0.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.0.0.tgz#c0318b9e539a5256ca780dd9575c9345af05b8ed" integrity sha512-S4AHriUkTX9FoFvL4G8hXDcx6t3gp2HpfCza3Q0v6S78gul2hKWifLQbeW+ZF89+hSm2ZIc/uF3J97ZgytgTRg== +detect-node@2.1.0, detect-node@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + detect-node@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== -detect-node@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - detect-port-alt@1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" @@ -12370,7 +12413,7 @@ eth-crypto@^1.8.0: ethers "5.0.13" secp256k1 "4.0.2" -eth-ens-namehash@2.0.8, eth-ens-namehash@^2.0.8: +eth-ens-namehash@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf" integrity sha1-IprEbsqG1S4MmR58sq74P/D2i88= @@ -12754,10 +12797,10 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereum rlp "^2.0.0" safe-buffer "^5.1.1" -ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.0.tgz#e2b43a30bfcdbcb432a4eb42bd5f2393209b3fd5" - integrity sha512-kR+vhu++mUDARrsMMhsjjzPduRVAeundLGXucGRHF3B4oEltOUspfgCVco4kckucj3FMlLaZHUl9n7/kdmr6Tw== +ethereumjs-util@^7.0.8: + version "7.0.10" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.0.10.tgz#5fb7b69fa1fda0acc59634cf39d6b0291180fc1f" + integrity sha512-c/xThw6A+EAnej5Xk5kOzFzyoSnw0WX0tSlZ6pAsfGVvQj3TItaDg9b1+Fz1RJXA+y2YksKwQnuzgt1eY6LKzw== dependencies: "@types/bn.js" "^5.1.0" bn.js "^5.1.2" @@ -12766,10 +12809,10 @@ ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0: ethjs-util "0.1.6" rlp "^2.2.4" -ethereumjs-util@^7.0.8: - version "7.0.10" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.0.10.tgz#5fb7b69fa1fda0acc59634cf39d6b0291180fc1f" - integrity sha512-c/xThw6A+EAnej5Xk5kOzFzyoSnw0WX0tSlZ6pAsfGVvQj3TItaDg9b1+Fz1RJXA+y2YksKwQnuzgt1eY6LKzw== +ethereumjs-util@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.0.tgz#e2b43a30bfcdbcb432a4eb42bd5f2393209b3fd5" + integrity sha512-kR+vhu++mUDARrsMMhsjjzPduRVAeundLGXucGRHF3B4oEltOUspfgCVco4kckucj3FMlLaZHUl9n7/kdmr6Tw== dependencies: "@types/bn.js" "^5.1.0" bn.js "^5.1.2" @@ -17090,6 +17133,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -17162,6 +17210,11 @@ loglevel@^1.7.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== +loglevel@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" + integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== + long@~3: version "3.2.0" resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" @@ -18963,6 +19016,14 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-queue@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + p-queue@^6.3.0: version "6.6.1" resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.1.tgz#578891ada028a61371ec2692b26614d1b7d2b10a" @@ -18983,7 +19044,7 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" -p-timeout@^3.1.0: +p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== @@ -22349,7 +22410,7 @@ scrypt-js@2.0.4: resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-2.0.4.tgz#32f8c5149f0797672e551c07e230f834b6af5f16" integrity sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw== -scrypt-js@3.0.1, scrypt-js@^3.0.0, scrypt-js@^3.0.1: +scrypt-js@3.0.1, scrypt-js@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== @@ -22719,9 +22780,9 @@ simple-concat@^1.0.0: integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== simple-get@^2.7.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d" - integrity sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw== + version "2.8.2" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.2.tgz#5708fb0919d440657326cd5fe7d2599d07705019" + integrity sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw== dependencies: decompress-response "^3.3.0" once "^1.3.1" @@ -24342,6 +24403,14 @@ unload@2.2.0: "@babel/runtime" "^7.6.2" detect-node "^2.0.4" +unload@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe" + integrity sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "2.1.0" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -24392,9 +24461,9 @@ url-loader@2.3.0, url-loader@^2.0.1: schema-utils "^2.5.0" url-parse@^1.4.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" - integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== + version "1.5.9" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.9.tgz#05ff26484a0b5e4040ac64dcee4177223d74675e" + integrity sha512-HpOvhKBvre8wYez+QhHcYiVvVmeF6DVnuSOOPhe3cTum3BnqHhvKaZm8FU5yTiOu/Jut2ZpB2rA/SbBA1JIGlQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -24537,11 +24606,6 @@ uuid@2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.1.tgz#c2a30dedb3e535d72ccf82e343941a50ba8533ac" integrity sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w= -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - uuid@7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6" @@ -24769,6 +24833,14 @@ web3-core-helpers@1.5.2: web3-eth-iban "1.5.2" web3-utils "1.5.2" +web3-core-helpers@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.7.1.tgz#6dc34eff6ad31149db6c7cc2babbf574a09970cd" + integrity sha512-xn7Sx+s4CyukOJdlW8bBBDnUCWndr+OCJAlUe/dN2wXiyaGRiCWRhuQZrFjbxLeBt1fYFH7uWyYHhYU6muOHgw== + dependencies: + web3-eth-iban "1.7.1" + web3-utils "1.7.1" + web3-core-method@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.3.6.tgz#4b0334edd94b03dfec729d113c69a4eb6ebc68ae" @@ -24793,6 +24865,17 @@ web3-core-method@1.5.2: web3-core-subscriptions "1.5.2" web3-utils "1.5.2" +web3-core-method@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.7.1.tgz#912c87d0f107d3f823932cf8a716852e3250e557" + integrity sha512-383wu5FMcEphBFl5jCjk502JnEg3ugHj7MQrsX7DY76pg5N5/dEzxeEMIJFCN6kr5Iq32NINOG3VuJIyjxpsEg== + dependencies: + "@ethersproject/transactions" "^5.0.0-beta.135" + web3-core-helpers "1.7.1" + web3-core-promievent "1.7.1" + web3-core-subscriptions "1.7.1" + web3-utils "1.7.1" + web3-core-promievent@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.3.6.tgz#6c27dc79de8f71b74f5d17acaf9aaf593d3cb0c9" @@ -24807,6 +24890,13 @@ web3-core-promievent@1.5.2: dependencies: eventemitter3 "4.0.4" +web3-core-promievent@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.7.1.tgz#7f78ec100a696954d0c882dac619fec28b2efc96" + integrity sha512-Vd+CVnpPejrnevIdxhCkzMEywqgVbhHk/AmXXceYpmwA6sX41c5a65TqXv1i3FWRJAz/dW7oKz9NAzRIBAO/kA== + dependencies: + eventemitter3 "4.0.4" + web3-core-requestmanager@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.3.6.tgz#4fea269fe913fd4fca464b4f7c65cb94857b5b2a" @@ -24830,6 +24920,17 @@ web3-core-requestmanager@1.5.2: web3-providers-ipc "1.5.2" web3-providers-ws "1.5.2" +web3-core-requestmanager@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.7.1.tgz#5cd7507276ca449538fe11cb4f363de8507502e5" + integrity sha512-/EHVTiMShpZKiq0Jka0Vgguxi3vxq1DAHKxg42miqHdUsz4/cDWay2wGALDR2x3ofDB9kqp7pb66HsvQImQeag== + dependencies: + util "^0.12.0" + web3-core-helpers "1.7.1" + web3-providers-http "1.7.1" + web3-providers-ipc "1.7.1" + web3-providers-ws "1.7.1" + web3-core-subscriptions@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.3.6.tgz#ee24e7974d1d72ff6c992c599deba4ef9b308415" @@ -24847,6 +24948,14 @@ web3-core-subscriptions@1.5.2: eventemitter3 "4.0.4" web3-core-helpers "1.5.2" +web3-core-subscriptions@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.7.1.tgz#f7c834ee3544f4a5641a989304f61fde6a523e0b" + integrity sha512-NZBsvSe4J+Wt16xCf4KEtBbxA9TOwSVr8KWfUQ0tC2KMdDYdzNswl0Q9P58xaVuNlJ3/BH+uDFZJJ5E61BSA1Q== + dependencies: + eventemitter3 "4.0.4" + web3-core-helpers "1.7.1" + web3-core@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.3.6.tgz#a6a761d1ff2f3ee462b8dab679229d2f8e267504" @@ -24860,7 +24969,7 @@ web3-core@1.3.6: web3-core-requestmanager "1.3.6" web3-utils "1.3.6" -web3-core@1.5.2, web3-core@^1.3.5: +web3-core@1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.5.2.tgz#ca2b9b1ed3cf84d48b31c9bb91f7628f97cfdcd5" integrity sha512-sebMpQbg3kbh3vHUbHrlKGKOxDWqjgt8KatmTBsTAWj/HwWYVDzeX+2Q84+swNYsm2DrTBVFlqTErFUwPBvyaA== @@ -24873,6 +24982,19 @@ web3-core@1.5.2, web3-core@^1.3.5: web3-core-requestmanager "1.5.2" web3-utils "1.5.2" +web3-core@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.7.1.tgz#ef9b7f03909387b9ab783f34cdc5ebcb50248368" + integrity sha512-HOyDPj+4cNyeNPwgSeUkhtS0F+Pxc2obcm4oRYPW5ku6jnTO34pjaij0us+zoY3QEusR8FfAKVK1kFPZnS7Dzw== + dependencies: + "@types/bn.js" "^4.11.5" + "@types/node" "^12.12.6" + bignumber.js "^9.0.0" + web3-core-helpers "1.7.1" + web3-core-method "1.7.1" + web3-core-requestmanager "1.7.1" + web3-utils "1.7.1" + web3-eth-abi@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.3.6.tgz#4272ca48d817aa651bbf97b269f5ff10abc2b8a9" @@ -24890,36 +25012,13 @@ web3-eth-abi@1.5.2: "@ethersproject/abi" "5.0.7" web3-utils "1.5.2" -web3-eth-accounts@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.5.2.tgz#cf506c21037fa497fe42f1f055980ce4acf83731" - integrity sha512-F8mtzxgEhxfLc66vPi0Gqd6mpscvvk7Ua575bsJ1p9J2X/VtuKgDgpWcU4e4LKeROQ+ouCpAG9//0j9jQuij3A== - dependencies: - "@ethereumjs/common" "^2.3.0" - "@ethereumjs/tx" "^3.2.1" - crypto-browserify "3.12.0" - eth-lib "0.2.8" - ethereumjs-util "^7.0.10" - scrypt-js "^3.0.1" - uuid "3.3.2" - web3-core "1.5.2" - web3-core-helpers "1.5.2" - web3-core-method "1.5.2" - web3-utils "1.5.2" - -web3-eth-contract@1.5.2, web3-eth-contract@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.5.2.tgz#ffbd799fd01e36596aaadefba323e24a98a23c2f" - integrity sha512-4B8X/IPFxZCTmtENpdWXtyw5fskf2muyc3Jm5brBQRb4H3lVh1/ZyQy7vOIkdphyaXu4m8hBLHzeyKkd37mOUg== +web3-eth-abi@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.7.1.tgz#6632003220a4defee4de8215dc703e43147382ea" + integrity sha512-8BVBOoFX1oheXk+t+uERBibDaVZ5dxdcefpbFTWcBs7cdm0tP8CD1ZTCLi5Xo+1bolVHNH2dMSf/nEAssq5pUA== dependencies: - "@types/bn.js" "^4.11.5" - web3-core "1.5.2" - web3-core-helpers "1.5.2" - web3-core-method "1.5.2" - web3-core-promievent "1.5.2" - web3-core-subscriptions "1.5.2" - web3-eth-abi "1.5.2" - web3-utils "1.5.2" + "@ethersproject/abi" "5.0.7" + web3-utils "1.7.1" web3-eth-contract@^1.3.6: version "1.3.6" @@ -24936,20 +25035,34 @@ web3-eth-contract@^1.3.6: web3-eth-abi "1.3.6" web3-utils "1.3.6" -web3-eth-ens@1.5.2: +web3-eth-contract@^1.5.2: version "1.5.2" - resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.5.2.tgz#ecb3708f0e8e2e847e9d89e8428da12c30bba6a4" - integrity sha512-/UrLL42ZOCYge+BpFBdzG8ICugaRS4f6X7PxJKO+zAt+TwNgBpjuWfW/ZYNcuqJun/ZyfcTuj03TXqA1RlNhZQ== + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.5.2.tgz#ffbd799fd01e36596aaadefba323e24a98a23c2f" + integrity sha512-4B8X/IPFxZCTmtENpdWXtyw5fskf2muyc3Jm5brBQRb4H3lVh1/ZyQy7vOIkdphyaXu4m8hBLHzeyKkd37mOUg== dependencies: - content-hash "^2.5.2" - eth-ens-namehash "2.0.8" + "@types/bn.js" "^4.11.5" web3-core "1.5.2" web3-core-helpers "1.5.2" + web3-core-method "1.5.2" web3-core-promievent "1.5.2" + web3-core-subscriptions "1.5.2" web3-eth-abi "1.5.2" - web3-eth-contract "1.5.2" web3-utils "1.5.2" +web3-eth-contract@^1.7.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.7.1.tgz#3f5147e5f1441ae388c985ba95023d02503378ae" + integrity sha512-HpnbkPYkVK3lOyos2SaUjCleKfbF0SP3yjw7l551rAAi5sIz/vwlEzdPWd0IHL7ouxXbO0tDn7jzWBRcD3sTbA== + dependencies: + "@types/bn.js" "^4.11.5" + web3-core "1.7.1" + web3-core-helpers "1.7.1" + web3-core-method "1.7.1" + web3-core-promievent "1.7.1" + web3-core-subscriptions "1.7.1" + web3-eth-abi "1.7.1" + web3-utils "1.7.1" + web3-eth-iban@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.3.6.tgz#0d6ba21fe78f190af8919e9cd5453882457209e0" @@ -24966,44 +25079,13 @@ web3-eth-iban@1.5.2: bn.js "^4.11.9" web3-utils "1.5.2" -web3-eth-personal@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.5.2.tgz#043335a19ab59e119ba61e3bd6c3b8cde8120490" - integrity sha512-nH5N2GiVC0C5XeMEKU16PeFP3Hb3hkPvlR6Tf9WQ+pE+jw1c8eaXBO1CJQLr15ikhUF3s94ICyHcfjzkDsmRbA== - dependencies: - "@types/node" "^12.12.6" - web3-core "1.5.2" - web3-core-helpers "1.5.2" - web3-core-method "1.5.2" - web3-net "1.5.2" - web3-utils "1.5.2" - -web3-eth@^1.3.5: - version "1.5.2" - resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.5.2.tgz#0f6470df60a2a7d04df4423ca7721db8ed5ad72b" - integrity sha512-DwWQ6TCOUqvYyo7T20S7HpQDPveNHNqOn2Q2F3E8ZFyEjmqT4XsGiwvm08kB/VgQ4e/ANyq/i8PPFSYMT8JKHg== - dependencies: - web3-core "1.5.2" - web3-core-helpers "1.5.2" - web3-core-method "1.5.2" - web3-core-subscriptions "1.5.2" - web3-eth-abi "1.5.2" - web3-eth-accounts "1.5.2" - web3-eth-contract "1.5.2" - web3-eth-ens "1.5.2" - web3-eth-iban "1.5.2" - web3-eth-personal "1.5.2" - web3-net "1.5.2" - web3-utils "1.5.2" - -web3-net@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.5.2.tgz#58915d7e2dad025d2a08f02c865f3abe61c48eff" - integrity sha512-VEc9c+jfoERhbJIxnx0VPlQDot8Lm4JW/tOWFU+ekHgIiu2zFKj5YxhURIth7RAbsaRsqCb79aE+M0eI8maxVQ== +web3-eth-iban@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.7.1.tgz#2148dff256392491df36b175e393b03c6874cd31" + integrity sha512-XG4I3QXuKB/udRwZdNEhdYdGKjkhfb/uH477oFVMLBqNimU/Cw8yXUI5qwFKvBHM+hMQWfzPDuSDEDKC2uuiMg== dependencies: - web3-core "1.5.2" - web3-core-method "1.5.2" - web3-utils "1.5.2" + bn.js "^4.11.9" + web3-utils "1.7.1" web3-provider-engine@15.0.4: version "15.0.4" @@ -25105,6 +25187,14 @@ web3-providers-http@1.5.2: web3-core-helpers "1.5.2" xhr2-cookies "1.1.0" +web3-providers-http@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.7.1.tgz#3e00e013f013766aade28da29247daa1a937e759" + integrity sha512-dmiO6G4dgAa3yv+2VD5TduKNckgfR97VI9YKXVleWdcpBoKXe2jofhdvtafd42fpIoaKiYsErxQNcOC5gI/7Vg== + dependencies: + web3-core-helpers "1.7.1" + xhr2-cookies "1.1.0" + web3-providers-ipc@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.3.6.tgz#cef8d12c1ebb47adce5ebf597f553c623362cb4a" @@ -25122,6 +25212,14 @@ web3-providers-ipc@1.5.2: oboe "2.1.5" web3-core-helpers "1.5.2" +web3-providers-ipc@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.7.1.tgz#cde879a2ba57b1deac2e1030de90d185b793dd50" + integrity sha512-uNgLIFynwnd5M9ZC0lBvRQU5iLtU75hgaPpc7ZYYR+kjSk2jr2BkEAQhFVJ8dlqisrVmmqoAPXOEU0flYZZgNQ== + dependencies: + oboe "2.1.5" + web3-core-helpers "1.7.1" + web3-providers-ws@1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.3.6.tgz#e1df617bc89d66165abdf2191da0014c505bfaac" @@ -25141,6 +25239,15 @@ web3-providers-ws@1.5.2: web3-core-helpers "1.5.2" websocket "^1.0.32" +web3-providers-ws@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.7.1.tgz#b6b3919ce155eff29b21bc3f205a098299a8c1b2" + integrity sha512-Uj0n5hdrh0ESkMnTQBsEUS2u6Unqdc7Pe4Zl+iZFb7Yn9cIGsPJBl7/YOP4137EtD5ueXAv+MKwzcelpVhFiFg== + dependencies: + eventemitter3 "4.0.4" + web3-core-helpers "1.7.1" + websocket "^1.0.32" + web3-utils@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.2.1.tgz#21466e38291551de0ab34558de21512ac4274534" @@ -25168,7 +25275,7 @@ web3-utils@1.3.6, web3-utils@^1.3.6: underscore "1.12.1" utf8 "3.0.0" -web3-utils@1.5.2, web3-utils@^1.3.5, web3-utils@^1.5.2: +web3-utils@1.5.2, web3-utils@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.5.2.tgz#150982dcb1918ffc54eba87528e28f009ebc03aa" integrity sha512-quTtTeQJHYSxAwIBOCGEcQtqdVcFWX6mCFNoqnp+mRbq+Hxbs8CGgO/6oqfBx4OvxIOfCpgJWYVHswRXnbEu9Q== @@ -25181,6 +25288,19 @@ web3-utils@1.5.2, web3-utils@^1.3.5, web3-utils@^1.5.2: randombytes "^2.1.0" utf8 "3.0.0" +web3-utils@1.7.1, web3-utils@^1.7.0, web3-utils@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.7.1.tgz#77d8bacaf426c66027d8aa4864d77f0ed211aacd" + integrity sha512-fef0EsqMGJUgiHPdX+KN9okVWshbIumyJPmR+btnD1HgvoXijKEkuKBv0OmUqjbeqmLKP2/N9EiXKJel5+E1Dw== + dependencies: + bn.js "^4.11.9" + ethereum-bloom-filters "^1.0.6" + ethereumjs-util "^7.1.0" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + utf8 "3.0.0" + web3-utils@^1.0.0-beta.31: version "1.5.0" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.5.0.tgz#48c8ba0d95694e73b9a6d473d955880cd4758e4a"