From 89219b7afe6d8e8edbdd1a7d8c9af814d9c80daf Mon Sep 17 00:00:00 2001 From: Petromir Petrov Date: Wed, 7 Aug 2024 14:48:00 +0300 Subject: [PATCH] add: extension invite code modal and top bar invite code option --- .../ExtensionInviteCodeModal.js | 106 ++++++++ .../ExtensionInviteCodeModal.module.scss | 241 ++++++++++++++++++ src/components/Wallet/TopBar/Links/Links.js | 43 +++- .../Wallet/TopBar/Links/Links.module.scss | 9 +- .../Links/images/extension-invite-code.svg | 6 + .../TopBar/Links/images/help-center.svg | 25 +- .../Links/images/help-with-notification.svg | 8 + .../Wallet/TopBar/Links/images/help.svg | 16 +- src/components/Wallet/TopBar/TopBar.js | 6 +- src/components/Wallet/Wallet.js | 38 ++- src/resources/chrome-web-store.svg | 198 ++++++++++++++ src/resources/icons/copy-new.svg | 5 + src/resources/logo-new.svg | 72 ++++++ 13 files changed, 742 insertions(+), 31 deletions(-) create mode 100644 src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.js create mode 100644 src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.module.scss create mode 100644 src/components/Wallet/TopBar/Links/images/extension-invite-code.svg create mode 100644 src/components/Wallet/TopBar/Links/images/help-with-notification.svg create mode 100644 src/resources/chrome-web-store.svg create mode 100644 src/resources/icons/copy-new.svg create mode 100644 src/resources/logo-new.svg diff --git a/src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.js b/src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.js new file mode 100644 index 0000000000..c29bdc3b69 --- /dev/null +++ b/src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.js @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useState } from 'react' +import cn from 'classnames' + +import { useModals } from 'hooks' + +import { ReactComponent as AmbireLogo } from 'resources/logo-new.svg' +import { ReactComponent as CopyIcon } from 'resources/icons/copy-new.svg' +import { ReactComponent as CloseIcon } from 'resources/icons/close.svg' +import { ReactComponent as ChromeWebStore } from 'resources/chrome-web-store.svg' + +import { useToasts } from 'hooks/toasts' +import styles from './ExtensionInviteCodeModal.module.scss' + +const CAN_CLOSE_AFTER_MS = 5000 + +const ExtensionInviteCodeModal = ({ + inviteCode, + setExtensionInviteCodeModalSeenBy, + accountId, + waitForClose = true +}) => { + const { onHideModal } = useModals() + const { addToast } = useToasts() + const [canClose, setCanClose] = useState(!waitForClose) + + const onCloseModal = useCallback(() => { + onHideModal() + setExtensionInviteCodeModalSeenBy((prev) => [...prev, accountId]) + }, [accountId, onHideModal, setExtensionInviteCodeModalSeenBy]) + + useEffect(() => { + const startingTime = Date.now() + + const timeout = setTimeout(() => { + if (Date.now() - startingTime < CAN_CLOSE_AFTER_MS) return + + setCanClose(true) + }, CAN_CLOSE_AFTER_MS) + + return () => { + clearTimeout(timeout) + } + }) + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(inviteCode) + addToast('Invite code copied to clipboard') + } catch { + addToast('Failed to copy invite code to clipboard', { error: true }) + } + }, [addToast, inviteCode]) + + return ( +
+
+
+
+ +

Easy and secure self-custody for the Ethereum ecosystem

+ +
+
+
+

Hey!

+

+ We are onboarding only selected Ambire Wallet users to our newest product - the Ambire + browser extension. +

+

+ Claim this exclusive invitation code to get early access and start collecting XP for our + launch campaign before everyone else 🤫 +

+
+
+ Invitation code + {inviteCode} + +
+
+

+ Go to Chrome Web Store, install the extension and use the invitation code to log in. +

+ + + +
+
+
+ ) +} + +export default ExtensionInviteCodeModal diff --git a/src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.module.scss b/src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.module.scss new file mode 100644 index 0000000000..8c95a9ef25 --- /dev/null +++ b/src/components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal.module.scss @@ -0,0 +1,241 @@ +@import 'variables.scss'; + +$max-modal-height: 55.375rem; +$min-modal-height: 34.375rem; + +.wrapper { + z-index: 52; + position: relative; + max-height: $max-modal-height; + min-height: $min-modal-height; + max-width: 37.75rem; + width: 100%; + border-radius: 1.125rem; + border: 1px #3e436b; + background: #24263d; +} + +// Reset +.wrapper p, +.wrapper span { + margin: 0; +} + +.header { + overflow: hidden; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + background: linear-gradient(82deg, #6000ff, #353d6e); + border-radius: 0.75rem 0.75rem 0 0; + + .headerPrimaryGradient { + position: absolute; + left: -20%; + top: -35%; + z-index: 2; + width: 20rem; + height: 13.75rem; + background-color: #8b3dff; + opacity: 0.66; + filter: blur(4.375rem); + } + + .headerSecondaryGradient { + position: absolute; + left: 60%; + bottom: 30%; + z-index: 2; + width: 26.25rem; + height: 18.5rem; + background-color: #56f6c1; + opacity: 0.48; + filter: blur(6.25rem); + } + + .closeIcon { + position: absolute; + right: 1rem; + top: 1rem; + z-index: 4; + cursor: pointer; + + :global(#background) { + display: none; + } + + &:not(.closeIconEnabled) { + opacity: .4; + cursor: not-allowed; + } + } + + .headerLogo { + position: relative; + z-index: 3; + margin-bottom: 1.5rem; + } + .headerText { + position: relative; + z-index: 3; + font-size: 1rem; + color: rgba(255, 255, 255, 0.6); + text-align: center; + } +} + +.content { + display: flex; + flex-direction: column; + padding: 2rem; + gap: 2.5rem; +} + +.textWrapper { + .text { + font-size: 1.125rem; + line-height: 1.7; + margin-bottom: 1rem; + } + .text:last-child { + margin-bottom: 0; + } +} + +.codeWrapper { + display: flex; + flex-direction: column; + align-items: center; + .codeTitle { + display: block; + color: #bbbde4; + font-size: 1rem; + margin-bottom: 1rem; + } + .code { + display: block; + margin-bottom: 2rem; + border: none; + width: 18ch; + background: repeating-linear-gradient( + 90deg, + #6770b3 0, + #6770b3 1ch, + transparent 0, + transparent 1.5ch + ) + 0 100%/98% 1px no-repeat; + color: #27e8a7; // font: 36px monospace; + font-size: 2.5rem; + font-family: monospace; + letter-spacing: 0.5ch; + } + .copyButton { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1.5rem; + background: rgba(235, 236, 255, 0.08); + border-radius: 1.5rem; + cursor: pointer; + + svg, + span { + color: #bbbde4; + transition: $basic-transition; + } + + svg { + width: 1.125rem; + height: 1.125rem; + } + + &:hover { + background: rgba(235, 236, 255, 0.15); + + svg, + span { + color: #fff; + } + } + } +} + +.storeWrapper { + .storeText { + font-size: 1.125rem; + margin-bottom: 2rem; + } + .storeLink { + display: flex; + justify-content: center; + align-items: center; + width: fit-content; + gap: 0.5rem; + padding: 1rem 3rem; + margin: 0 auto; + border-radius: 4rem; + border: 1px solid #6770b3; + background: rgba(0, 0, 0, 0.3); + cursor: pointer; + + svg { + width: 9.25rem; + height: 2rem; + } + + &:hover { + background-color: #0e0e0e; + } + } +} + +@mixin SmallScreen { + .header { + padding: 1.5rem; + + .headerLogo { + margin-bottom: 1rem; + } + + .headerText { + font-size: .75rem; + } + } + .content { + gap: 1.5rem; + } + .storeWrapper .storeText, .textWrapper .text { + font-size: 0.875rem; + } + .storeWrapper .storeText { + margin-bottom: 1rem; + } + .codeWrapper { + .codeTitle { + font-size: 0.875rem; + margin-bottom: .5rem; + } + .code { + margin-bottom: 1.5rem; + } + } +} + +@media screen and (max-height: calc(#{$max-modal-height} + 100px)) { + @include SmallScreen; +} + +@include sm-breakpoint { + @include SmallScreen; + .header, .content { + padding: 1.5rem; + } + .codeWrapper { + .code { + font-size: 2rem; + } + } +} diff --git a/src/components/Wallet/TopBar/Links/Links.js b/src/components/Wallet/TopBar/Links/Links.js index 6a9d36f81c..df186c5226 100644 --- a/src/components/Wallet/TopBar/Links/Links.js +++ b/src/components/Wallet/TopBar/Links/Links.js @@ -1,10 +1,14 @@ -import React from 'react' +import React, { useCallback } from 'react' import { DropDown } from 'components/common' import useLocalStorage from 'hooks/useLocalStorage' import DropDownItem from 'components/common/DropDown/DropDownItem/DropDownItem' -import { ReactComponent as QuestionMark } from 'resources/icons/question-mark.svg' +import { useModals } from 'hooks' +import ExtensionInviteCodeModal from 'components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal' import styles from './Links.module.scss' +import { ReactComponent as QuestionMark } from './images/help.svg' +import { ReactComponent as ExtensionInviteCode } from './images/extension-invite-code.svg' +import { ReactComponent as QuestionMarkWithNotification } from './images/help-with-notification.svg' import { ReactComponent as HelpCenter } from './images/help-center.svg' import { ReactComponent as Issue } from './images/issue.svg' import { ReactComponent as Discord } from './images/discord.svg' @@ -12,17 +16,46 @@ import { ReactComponent as Twitter } from './images/twitter.svg' import { ReactComponent as Telegram } from './images/telegram.svg' import { ReactComponent as Tos } from './images/tos.svg' -const Links = () => { +const Links = ({ extensionInviteCodeUsed, inviteCode, accountId }) => { + const { showModal } = useModals() + const [, setExtensionInviteCodeModalSeenBy] = useLocalStorage({ + key: 'extensionInviteCodeModalSeenBy', + defaultValue: [] + }) const [linksViewed, setLinksViewed] = useLocalStorage({ key: 'linksViewed', defaultValue: false }) - const onOpen = () => setLinksViewed(true) + const onOpen = useCallback(() => setLinksViewed(true), [setLinksViewed]) + + const openExtensionInviteCodeModal = useCallback(() => { + showModal( + + ) + }, [accountId, inviteCode, setExtensionInviteCodeModalSeenBy, showModal]) return ( } + title={ + extensionInviteCodeUsed ? ( + + ) : ( + + ) + } onOpen={onOpen} > + {!extensionInviteCodeUsed && ( + + + + )} + + + \ No newline at end of file diff --git a/src/components/Wallet/TopBar/Links/images/help-center.svg b/src/components/Wallet/TopBar/Links/images/help-center.svg index 9540489556..2d4534e40b 100644 --- a/src/components/Wallet/TopBar/Links/images/help-center.svg +++ b/src/components/Wallet/TopBar/Links/images/help-center.svg @@ -1,11 +1,14 @@ - - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/src/components/Wallet/TopBar/Links/images/help-with-notification.svg b/src/components/Wallet/TopBar/Links/images/help-with-notification.svg new file mode 100644 index 0000000000..98cccdbb3a --- /dev/null +++ b/src/components/Wallet/TopBar/Links/images/help-with-notification.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/src/components/Wallet/TopBar/Links/images/help.svg b/src/components/Wallet/TopBar/Links/images/help.svg index bf4f72e6d6..02c20f4174 100644 --- a/src/components/Wallet/TopBar/Links/images/help.svg +++ b/src/components/Wallet/TopBar/Links/images/help.svg @@ -1,8 +1,8 @@ - - - - - - - - + + + + \ No newline at end of file diff --git a/src/components/Wallet/TopBar/TopBar.js b/src/components/Wallet/TopBar/TopBar.js index c2724a3dd6..8448a6302d 100644 --- a/src/components/Wallet/TopBar/TopBar.js +++ b/src/components/Wallet/TopBar/TopBar.js @@ -165,7 +165,11 @@ const TopBar = ({ dappsCatalog={dappsCatalog} dapModeTopBar={dappModeTopBar} /> - +
diff --git a/src/components/Wallet/Wallet.js b/src/components/Wallet/Wallet.js index 913c691db6..7ef7b2357c 100644 --- a/src/components/Wallet/Wallet.js +++ b/src/components/Wallet/Wallet.js @@ -10,6 +10,7 @@ import unsupportedDApps from 'ambire-common/src/constants/unsupportedDApps' import PermissionsModal from 'components/Modals/PermissionsModal/PermissionsModal' import UnsupportedDAppsModal from 'components/Modals/UnsupportedDAppsModal/UnsupportedDAppsModal' import { Loading } from 'components/common' +import ExtensionInviteCodeModal from 'components/Modals/ExtensionInviteCodeModal/ExtensionInviteCodeModal' import SideBar from './SideBar/SideBar' import TopBar from './TopBar/TopBar' import DappsCatalog from './DappsCatalog/DappsCatalog' @@ -42,6 +43,10 @@ export default function Wallet(props) { key: 'dAppsAdvancedMode', defaultValue: [] }) + const [extensionInviteCodeModalSeenBy, setExtensionInviteCodeModalSeenBy] = useLocalStorage({ + key: 'extensionInviteCodeModalSeenBy', + defaultValue: [] + }) const routes = [ { @@ -220,9 +225,9 @@ export default function Wallet(props) { const LoggedInGuard = () => (!isLoggedIn ? : null) - const handlePermissionsModal = useCallback(async () => { + const handleDisplayInitialModal = useCallback(async () => { const account = props.accounts.find(({ id }) => id === props.selectedAcc) - if (!account) return + if (!account || !arePermissionsLoaded) return const relayerIdentityURL = `${props.relayerURL}/identity/${account.id}` @@ -243,23 +248,46 @@ export default function Wallet(props) { /> ) - const isMobile = navigator.platform.includes('Android') || navigator.platform.includes('iOS') - if ((showCauseOfEmail || showCauseOfPermissions || showCauseOfBackupOptout) && !isMobile) + if (showCauseOfEmail || showCauseOfPermissions || showCauseOfBackupOptout) { + const isMobile = navigator.platform.includes('Android') || navigator.platform.includes('iOS') + if (isMobile) return + showModal(permissionsModal, { disableClose: true }) + return + } + const key = props.rewardsData?.rewards.extensionKey?.key + const used = props.rewardsData?.rewards.extensionKey?.used + + if (!key || used) return + const isSeen = extensionInviteCodeModalSeenBy.includes(account.id) + + if (isSeen) return + + showModal( + , + { disableClose: true } + ) }, [ props.accounts, props.relayerURL, props.onAddAccount, props.showThankYouPage, + props.rewardsData?.rewards.extensionKey?.key, + props.rewardsData?.rewards.extensionKey?.used, props.selectedAcc, arePermissionsLoaded, isClipboardGranted, isNoticationsGranted, modalHidden, + extensionInviteCodeModalSeenBy, showModal ]) - useEffect(() => handlePermissionsModal(), [handlePermissionsModal]) + useEffect(() => handleDisplayInitialModal(), [handleDisplayInitialModal]) // On pathname change (i.e. navigating to different page), always scroll to top useEffect(() => { diff --git a/src/resources/chrome-web-store.svg b/src/resources/chrome-web-store.svg new file mode 100644 index 0000000000..b21f08cad9 --- /dev/null +++ b/src/resources/chrome-web-store.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/resources/icons/copy-new.svg b/src/resources/icons/copy-new.svg new file mode 100644 index 0000000000..d38fd6e698 --- /dev/null +++ b/src/resources/icons/copy-new.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/resources/logo-new.svg b/src/resources/logo-new.svg new file mode 100644 index 0000000000..8e43297111 --- /dev/null +++ b/src/resources/logo-new.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file