diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index ea0cde3b1b..7717e0f85a 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -1019,6 +1019,35 @@ "info": { "message": "Info" }, + "welcomeBack": { + "message": "Welcome Back to Temple" + }, + "enterPasswordToUnlock": { + "message": "Enter your password to unlock wallet" + }, + "forgotPasswordQuestion": { + "message": "Forgot password?" + }, + "forgotPasswordModalTitle": { + "message": "Forgot password" + }, + "forgotPasswordModalDescription": { + "message": "For safety reasons, Temple never stores your password and can’t help recover it. If you forgot your password, you can reset extension and re-import your wallet using your Seed Phrase without losing any funds." + }, + "unlockScreenResetModalAlertText": { + "message": "Before you continue, ensure you have a back up of your Seed Phrase to avoid permanent loss of your funds and recover access later." + }, + "unlockScreenResetModalDescription": { + "message": "If you reset the extension without Seed Phrase back up, you'll not be able to recover your funds." + }, + "walletTemporarilyBlockedError": { + "message": "Incorrect password entered $attempts$ times. Wallet temporarily blocked for safety reason.", + "placeholders": { + "attempts": { + "content": "$1" + } + } + }, "unlockWallet": { "message": "Unlock the Wallet" }, diff --git a/src/app/atoms/NetworkLogo.tsx b/src/app/atoms/NetworkLogo.tsx index ab3e075ba2..09c7e63579 100644 --- a/src/app/atoms/NetworkLogo.tsx +++ b/src/app/atoms/NetworkLogo.tsx @@ -23,13 +23,14 @@ interface TezosNetworkLogoProps { networkName: string; chainId: string; size?: number; + className?: string; } -export const TezosNetworkLogo = memo(({ networkName, chainId, size = 16 }) => +export const TezosNetworkLogo = memo(({ className, networkName, chainId, size = 16 }) => chainId === TEZOS_MAINNET_CHAIN_ID ? ( - + ) : ( - + ) ); diff --git a/src/app/atoms/ScrollView.tsx b/src/app/atoms/ScrollView.tsx index 10b9d5a844..dec31800ae 100644 --- a/src/app/atoms/ScrollView.tsx +++ b/src/app/atoms/ScrollView.tsx @@ -1,49 +1,21 @@ -import React, { FC, UIEventHandler, useCallback, useMemo, useRef } from 'react'; +import React, { FC, UIEventHandler, useCallback } from 'react'; import clsx from 'clsx'; -import { throttle } from 'lodash'; -import { useSafeState } from 'lib/ui/hooks'; -import { useWillUnmount } from 'lib/ui/hooks/useWillUnmount'; +import { useResizeDependentValue } from 'app/hooks/use-resize-dependent-value'; interface Props extends PropsWithChildren { className?: string; } export const ScrollView: FC = ({ className, children }) => { - const [contentHiding, setContentHiding] = useSafeState(false); + const { value: contentHiding, updateValue, refFn } = useResizeDependentValue(isContentHidingBelow, false, 300); - const ref = useRef(); - - const setContentHidingThrottled = useMemo(() => throttle((value: boolean) => setContentHiding(value), 300), []); - - const onScroll = useCallback>(event => { - const node = event.currentTarget; - - setContentHidingThrottled(isContentHidingBelow(node)); - }, []); - - const resizeObserver = useMemo( - () => - new ResizeObserver(() => { - const node = ref.current; - - if (node) setContentHidingThrottled(isContentHidingBelow(node)); - }), - [] + const onScroll = useCallback>( + event => updateValue(event.currentTarget), + [updateValue] ); - useWillUnmount(() => void resizeObserver.disconnect()); - - const refFn = useCallback((node: HTMLDivElement | null) => { - ref.current = node; - if (!node) return void setContentHiding(false); - - resizeObserver.observe(node); - - setContentHiding(isContentHidingBelow(node)); - }, []); - return (
>( - ({ Icon, color, onClick, testID, testIDProperties, children, className }, ref) => { + ({ Icon, color = 'grey', onClick, testID, testIDProperties, children, className }, ref) => { const { textClassName, iconClassName } = useMemo(() => { switch (color) { case 'black': @@ -52,7 +52,7 @@ export const TextButton = memo( testIDProperties={testIDProperties} > {children} - + {Icon && } ); } diff --git a/src/app/hooks/use-resize-dependent-value.ts b/src/app/hooks/use-resize-dependent-value.ts new file mode 100644 index 0000000000..5d69cd6494 --- /dev/null +++ b/src/app/hooks/use-resize-dependent-value.ts @@ -0,0 +1,59 @@ +import { useCallback, useMemo, useRef } from 'react'; + +import { throttle } from 'lodash'; + +import { useSafeState } from 'lib/ui/hooks'; +import { useWillUnmount } from 'lib/ui/hooks/useWillUnmount'; + +export const useResizeDependentValue = ( + fn: (element: E) => T, + fallbackValue: T, + throttleTime: number +) => { + const [value, setValue] = useSafeState(fallbackValue); + + const ref = useRef(); + + const setValueThrottled = useMemo( + () => throttle((value: T) => setValue(value), throttleTime), + [setValue, throttleTime] + ); + + const updateValue = useCallback( + (node: E) => { + setValueThrottled(fn(node)); + }, + [fn, setValueThrottled] + ); + + const resizeObserver = useMemo( + () => + new ResizeObserver(() => { + const node = ref.current; + + if (node) updateValue(node); + }), + [updateValue] + ); + + useWillUnmount(() => void resizeObserver.disconnect()); + + const refFn = useCallback( + (node: E | null) => { + ref.current = node; + if (!node) return void setValue(fallbackValue); + + resizeObserver.disconnect(); + resizeObserver.observe(node); + + updateValue(node); + }, + [fallbackValue, resizeObserver, setValue, updateValue] + ); + + return { + value, + updateValue, + refFn + }; +}; diff --git a/src/app/layouts/PageLayout/index.tsx b/src/app/layouts/PageLayout/index.tsx index 7e87a43669..55d206f223 100644 --- a/src/app/layouts/PageLayout/index.tsx +++ b/src/app/layouts/PageLayout/index.tsx @@ -45,7 +45,6 @@ export interface PageLayoutProps extends DefaultHeaderProps, ScrollEdgesVisibili contentPadding?: boolean; contentClassName?: string; paperClassName?: string; - dimBg?: boolean; headerChildren?: ReactNode; } @@ -56,7 +55,6 @@ const PageLayout: FC> = ({ contentPadding = true, contentClassName, paperClassName, - dimBg = true, headerChildren, onBottomEdgeVisibilityChange, bottomEdgeThreshold, @@ -93,7 +91,7 @@ const PageLayout: FC> = ({ 'flex-grow flex flex-col', noScroll && 'overflow-hidden', contentPadding && 'p-4 pb-15', - dimBg && 'bg-background', + 'bg-background', contentClassName )} > diff --git a/src/app/layouts/SimplePageLayout.tsx b/src/app/layouts/SimplePageLayout.tsx deleted file mode 100644 index 31e5c5f041..0000000000 --- a/src/app/layouts/SimplePageLayout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { FC, ReactNode } from 'react'; - -import clsx from 'clsx'; - -import DocBg from 'app/a11y/DocBg'; -import { Logo } from 'app/atoms/Logo'; -import { useAppEnv } from 'app/env'; - -import { LAYOUT_CONTAINER_CLASSNAME } from './containers'; - -interface SimplePageLayoutProps extends PropsWithChildren { - title: ReactNode; -} - -const SimplePageLayout: FC = ({ title, children }) => { - const appEnv = useAppEnv(); - - return ( - <> - {!appEnv.fullPage && } - -
-
-
- -
- -
{title}
-
- -
- {children} -
-
- - ); -}; - -export default SimplePageLayout; diff --git a/src/app/pages/AccountSettings/index.tsx b/src/app/pages/AccountSettings/index.tsx index f54d3df410..d1c2bc98c0 100644 --- a/src/app/pages/AccountSettings/index.tsx +++ b/src/app/pages/AccountSettings/index.tsx @@ -135,7 +135,6 @@ export const AccountSettings = memo(({ id }) => { diff --git a/src/app/pages/Receive/Receive.tsx b/src/app/pages/Receive/Receive.tsx index a133a657d1..0ad30a4a05 100644 --- a/src/app/pages/Receive/Receive.tsx +++ b/src/app/pages/Receive/Receive.tsx @@ -27,7 +27,7 @@ export const Receive = memo(() => { const resetReceivePayload = useCallback(() => setReceivePayload(null), []); return ( - } dimBg> + }> {receivePayload && } diff --git a/src/app/pages/Unlock/Unlock.selectors.ts b/src/app/pages/Unlock/Unlock.selectors.ts index 6416a1ae55..b2a000f06e 100644 --- a/src/app/pages/Unlock/Unlock.selectors.ts +++ b/src/app/pages/Unlock/Unlock.selectors.ts @@ -1,5 +1,8 @@ export enum UnlockSelectors { passwordInput = 'Unlock/Password Input', unlockButton = 'Unlock/Unlock Button', - importWalletUsingSeedPhrase = 'Unlock/Import Wallet using Seed Phrase Link' + importWalletUsingSeedPhrase = 'Unlock/Import Wallet using Seed Phrase Link', + continueResetButton = 'Unlock/Continue Reset Button', + cancelResetExtensionButton = 'Unlock/Cancel Reset Extension Button', + confirmResetExtensionButton = 'Unlock/Confirm Reset Extension Button' } diff --git a/src/app/pages/Unlock/Unlock.tsx b/src/app/pages/Unlock/Unlock.tsx index c22b739d9f..15df55c3fb 100644 --- a/src/app/pages/Unlock/Unlock.tsx +++ b/src/app/pages/Unlock/Unlock.tsx @@ -1,13 +1,16 @@ import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import classNames from 'clsx'; import { OnSubmit, useForm } from 'react-hook-form'; import { useDispatch } from 'react-redux'; -import { Alert, FormField, FormSubmitButton } from 'app/atoms'; -import SimplePageLayout from 'app/layouts/SimplePageLayout'; +import { FormField } from 'app/atoms'; +import { StyledButton } from 'app/atoms/StyledButton'; +import { TextButton } from 'app/atoms/TextButton'; +import { useResizeDependentValue } from 'app/hooks/use-resize-dependent-value'; +import PageLayout from 'app/layouts/PageLayout'; import { getUserTestingGroupNameActions } from 'app/store/ab-testing/actions'; import { useUserTestingGroupNameSelector } from 'app/store/ab-testing/selectors'; +import { toastError } from 'app/toaster'; import { useFormAnalytics } from 'lib/analytics'; import { ABTestGroup } from 'lib/apis/temple'; import { DEFAULT_PASSWORD_INPUT_PLACEHOLDER } from 'lib/constants'; @@ -18,10 +21,17 @@ import { loadMnemonicToBackup } from 'lib/temple/front/mnemonic-to-backup-keeper import { TempleSharedStorageKey } from 'lib/temple/types'; import { useLocalStorage } from 'lib/ui/local-storage'; import { delay } from 'lib/utils'; -import { Link } from 'lib/woozie'; +import { ForgotPasswordModal } from './forgot-password-modal'; +import { PlanetsAnimation } from './planets-animation'; +import { SUN_RADIUS } from './planets-animation/constants'; +import { ResetExtensionModal } from './reset-extension-modal'; import { UnlockSelectors } from './Unlock.selectors'; +const EmptyHeader = () => null; + +const MIN_BOTTOM_GAP = 88; + interface UnlockProps { canImportNew?: boolean; } @@ -30,6 +40,11 @@ interface FormData { password: string; } +enum PageModalName { + ForgotPassword = 'ForgotPassword', + ResetExtension = 'ResetExtension' +} + const SUBMIT_ERROR_TYPE = 'submit-error'; const LOCK_TIME = 2 * USER_ACTION_TIMEOUT; const LAST_ATTEMPT = 3; @@ -44,11 +59,15 @@ const getTimeLeft = (start: number, end: number) => { return `${checkTime(minutes)}:${checkTime(seconds)}`; }; +const getAnimationBottomGap = (bottomGapElement: HTMLDivElement) => + bottomGapElement.getBoundingClientRect().height - SUN_RADIUS; + const Unlock: FC = ({ canImportNew = true }) => { const { unlock } = useTempleClient(); const dispatch = useDispatch(); const formAnalytics = useFormAnalytics('UnlockWallet'); + const [pageModalName, setPageModalName] = useState(null); const [attempt, setAttempt] = useLocalStorage(TempleSharedStorageKey.PasswordAttempts, 1); const [timelock, setTimeLock] = useLocalStorage(TempleSharedStorageKey.TimeLock, 0); const lockLevel = LOCK_TIME * Math.floor(attempt / 3); @@ -65,6 +84,12 @@ const Unlock: FC = ({ canImportNew = true }) => { const formRef = useRef(null); + const { value: bottomGap, refFn: bottomGapElementRef } = useResizeDependentValue( + getAnimationBottomGap, + MIN_BOTTOM_GAP, + 100 + ); + const focusPasswordField = useCallback(() => { formRef.current?.querySelector("input[name='password']")?.focus(); }, []); @@ -87,7 +112,10 @@ const Unlock: FC = ({ canImportNew = true }) => { setAttempt(1); } catch (err: any) { formAnalytics.trackSubmitFail(); - if (attempt >= LAST_ATTEMPT) setTimeLock(Date.now()); + if (attempt >= LAST_ATTEMPT) { + setTimeLock(Date.now()); + toastError(t('walletTemporarilyBlockedError', String(LAST_ATTEMPT))); + } setAttempt(attempt + 1); setTimeleft(getTimeLeft(Date.now(), LOCK_TIME * Math.floor((attempt + 1) / 3))); @@ -102,6 +130,10 @@ const Unlock: FC = ({ canImportNew = true }) => { [submitting, clearError, setError, unlock, focusPasswordField, formAnalytics, attempt, setAttempt, setTimeLock] ); + const handleForgotPasswordClick = useCallback(() => setPageModalName(PageModalName.ForgotPassword), []); + const handleModalClose = useCallback(() => setPageModalName(null), []); + const handleForgotPasswordContinueClick = useCallback(() => setPageModalName(PageModalName.ResetExtension), []); + const isDisabled = useMemo(() => Date.now() - timelock <= lockLevel, [timelock, lockLevel]); useEffect(() => { @@ -118,68 +150,56 @@ const Unlock: FC = ({ canImportNew = true }) => { }, [timelock, lockLevel, setTimeLock]); return ( - - -
- - - - - } - > - {isDisabled && ( - - )} -
- - - - {t('unlock')} - - - {canImportNew && ( -
-

- -

- - + +
+
+
+ +

+ +

+

+ +

+
+ + + - - + {isDisabled ? timeleft : t('unlock')} +
- )} - - + {canImportNew && ( + + + + )} + +
+ + {pageModalName === PageModalName.ForgotPassword && ( + + )} + {pageModalName === PageModalName.ResetExtension && } + ); }; diff --git a/src/app/pages/Unlock/forgot-password-modal.tsx b/src/app/pages/Unlock/forgot-password-modal.tsx new file mode 100644 index 0000000000..28095bf93e --- /dev/null +++ b/src/app/pages/Unlock/forgot-password-modal.tsx @@ -0,0 +1,34 @@ +import React, { memo } from 'react'; + +import { + ActionModal, + ActionModalBodyContainer, + ActionModalButton, + ActionModalButtonsContainer +} from 'app/atoms/action-modal'; +import { T, t } from 'lib/i18n'; + +import { UnlockSelectors } from './Unlock.selectors'; + +interface ForgotPasswordModalProps { + onClose: EmptyFn; + onContinueClick: EmptyFn; +} + +export const ForgotPasswordModal = memo(({ onClose, onContinueClick }) => ( + + +

{t('forgotPasswordModalDescription')}

+
+ + + + + +
+)); diff --git a/src/app/pages/Unlock/planets-animation/PlanetsAnimation.module.css b/src/app/pages/Unlock/planets-animation/PlanetsAnimation.module.css new file mode 100644 index 0000000000..a1f26ed933 --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/PlanetsAnimation.module.css @@ -0,0 +1,3 @@ +.tinting { + background: linear-gradient(180deg, rgba(38, 132, 252, 0.15) 0%, rgba(255, 255, 255, 0.00) 63.12%); +} diff --git a/src/app/pages/Unlock/planets-animation/constants.ts b/src/app/pages/Unlock/planets-animation/constants.ts new file mode 100644 index 0000000000..dde7dad78d --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/constants.ts @@ -0,0 +1 @@ +export const SUN_RADIUS = 36; diff --git a/src/app/pages/Unlock/planets-animation/evm-planet-item.tsx b/src/app/pages/Unlock/planets-animation/evm-planet-item.tsx new file mode 100644 index 0000000000..ecf19e39a1 --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/evm-planet-item.tsx @@ -0,0 +1,20 @@ +import React, { memo } from 'react'; + +import { EvmNetworkLogo } from 'app/atoms/NetworkLogo'; + +interface EvmPlanetItemProps { + name: string; + chainId: number; + padding?: 'none' | 'small' | 'medium' | 'large'; +} + +const paddingClassNames = { + none: '', + small: 'p-[3px]', + medium: 'p-1', + large: 'p-1.5' +}; + +export const EvmPlanetItem = memo(({ name, chainId, padding = 'small' }) => ( + +)); diff --git a/src/app/pages/Unlock/planets-animation/index.tsx b/src/app/pages/Unlock/planets-animation/index.tsx new file mode 100644 index 0000000000..3dfcaf51db --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/index.tsx @@ -0,0 +1,153 @@ +import React, { memo, useMemo } from 'react'; + +import clsx from 'clsx'; + +import { Logo } from 'app/atoms/Logo'; +import { TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { ETHEREUM_MAINNET_CHAIN_ID, OTHER_COMMON_MAINNET_CHAIN_IDS, TEZOS_MAINNET_CHAIN_ID } from 'lib/temple/types'; + +import { SUN_RADIUS } from './constants'; +import { EvmPlanetItem } from './evm-planet-item'; +import { Orbit } from './orbit'; +import styles from './PlanetsAnimation.module.css'; +import { ReactComponent as SunGlow } from './sun-glow.svg'; +import { PlanetsAnimationProps, OrbitProps } from './types'; +import { calculateBottomGapAngle } from './utils'; + +const orbitsBase = [ + { + fullRotationPeriod: 150, + radius: 86, + direction: 'clockwise' as const, + planets: [ + { + id: 'tezos', + radius: 19, + item: + }, + { + id: 'avalanche', + radius: 19, + item: + } + ] + }, + { + fullRotationPeriod: 142, + radius: 136, + direction: 'counter-clockwise' as const, + planets: [ + { + id: 'bsc', + radius: 19, + item: + }, + { + id: 'polygon', + radius: 19, + item: + } + ] + }, + { + fullRotationPeriod: 136, + radius: 186, + direction: 'clockwise' as const, + planets: [ + { + id: 'eth', + radius: 19, + item: + }, + { + id: 'optimism', + radius: 19, + item: + }, + { + id: 'arbitrum', + radius: 19, + item: + }, + { + id: 'base', + radius: 19, + item: + } + ] + }, + { + fullRotationPeriod: 130, + radius: 236, + direction: 'counter-clockwise' as const, + planets: [] + } +]; + +export const PlanetsAnimation = memo(({ bottomGap }) => { + const orbits = useMemo(() => { + const { planets: thirdOrbitPlanets, radius: thirdOrbitRadius } = orbitsBase[2]; + // This and the following calculations may be invalid if planets have significantly different radii. + const [thirdOrbitFirstPlanetBottomGapAngle, thirdOrbitLastPlanetBottomGapAngle] = [ + thirdOrbitPlanets[0], + thirdOrbitPlanets.at(-1)! + ].map(({ radius: planetRadius }) => calculateBottomGapAngle(bottomGap, thirdOrbitRadius, planetRadius)); + let orbitsStartAlphas: number[][]; + + if (Number.isNaN(thirdOrbitFirstPlanetBottomGapAngle)) { + orbitsStartAlphas = [ + [-Math.PI / 2, Math.PI / 2], + [-Math.PI, 0], + [(-5 * Math.PI) / 4, (-3 * Math.PI) / 4, -Math.PI / 4, Math.PI / 4] + ]; + } else { + const thirdOrbitMinPlanetBottomGapAngle = Math.min( + thirdOrbitFirstPlanetBottomGapAngle, + thirdOrbitLastPlanetBottomGapAngle + ); + const thirdOrbitTravelBeforeFirstBumpAngle = Math.PI / 12; + const thirdOrbitFirstPlanetStartAlpha = + -Math.PI - thirdOrbitMinPlanetBottomGapAngle + thirdOrbitTravelBeforeFirstBumpAngle; + const thirdOrbitLastPlanetStartAlpha = thirdOrbitMinPlanetBottomGapAngle - thirdOrbitTravelBeforeFirstBumpAngle; + const thirdOrbitPlanetsStartAlphas = thirdOrbitPlanets.map( + (_, index) => + thirdOrbitFirstPlanetStartAlpha * (1 - index / (thirdOrbitPlanets.length - 1)) + + thirdOrbitLastPlanetStartAlpha * (index / (thirdOrbitPlanets.length - 1)) + ); + const secondOrbitStartAlphas = [ + (thirdOrbitPlanetsStartAlphas[0] + thirdOrbitPlanetsStartAlphas[1]) / 2, + (thirdOrbitPlanetsStartAlphas.at(-2)! + thirdOrbitPlanetsStartAlphas.at(-1)!) / 2 + ]; + orbitsStartAlphas = [[-Math.PI / 2, Math.PI / 2], secondOrbitStartAlphas, thirdOrbitPlanetsStartAlphas]; + } + + return orbitsBase.map(({ planets, ...restOrbitProps }, index) => ({ + ...restOrbitProps, + planets: planets.map((planet, planetIndex) => ({ + ...planet, + startAlpha: orbitsStartAlphas[index][planetIndex] + })) + })); + }, [bottomGap]); + + return ( +
+
+ {orbits.map((orbit, index) => ( + + ))} + + +
+ +
+ +
+ +
+
+ ); +}); diff --git a/src/app/pages/Unlock/planets-animation/orbit.tsx b/src/app/pages/Unlock/planets-animation/orbit.tsx new file mode 100644 index 0000000000..ed24967a83 --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/orbit.tsx @@ -0,0 +1,85 @@ +import React, { memo, useMemo, useRef } from 'react'; + +import { Planet } from './planet'; +import { PlanetsAnimationProps, OrbitProps, PlanetAnimationParams } from './types'; +import { calculateBottomGapAngle } from './utils'; + +interface Props extends PlanetsAnimationProps { + orbit: OrbitProps; +} + +const planetMayBounce = (separatorAlpha: number) => !Number.isNaN(separatorAlpha) && separatorAlpha < Math.PI / 2; + +export const Orbit = memo(({ bottomGap, orbit }) => { + const { planets, radius, direction, fullRotationPeriod } = orbit; + const ref = useRef(null); + + const orderedPlanets = useMemo(() => planets.toSorted((a, b) => a.startAlpha - b.startAlpha), [planets]); + + const animationsParams = useMemo(() => { + const bottomGapAngles = orderedPlanets.map(({ radius: planetRadius }) => + calculateBottomGapAngle(bottomGap, radius, planetRadius) + ); + + // This and the following calculations may be invalid if planets have significantly different radii. + const leftBouncingPlanetIndex = bottomGapAngles.findIndex(planetMayBounce); + const rightBouncingPlanetIndex = bottomGapAngles.findLastIndex(planetMayBounce); + + if (leftBouncingPlanetIndex === -1) { + return planets.map(() => ({ bounces: false, duration: fullRotationPeriod })); + } + + const leftSeparatorAlpha = bottomGapAngles[leftBouncingPlanetIndex]; + const rightSeparatorAlpha = bottomGapAngles[rightBouncingPlanetIndex]; + const { startAlpha: leftStartAlpha } = orderedPlanets[leftBouncingPlanetIndex]; + const { startAlpha: rightStartAlpha } = orderedPlanets[rightBouncingPlanetIndex]; + + let travelBeforeFirstBumpAngle: number; + let travelBetweenBumpsAngle: number; + let travelToStartAngle: number; + if (direction === 'clockwise') { + travelBeforeFirstBumpAngle = rightSeparatorAlpha - rightStartAlpha; + travelBetweenBumpsAngle = Math.PI + leftSeparatorAlpha + rightSeparatorAlpha - (rightStartAlpha - leftStartAlpha); + travelToStartAngle = 1.5 * Math.PI - leftSeparatorAlpha + leftStartAlpha; + } else { + travelBeforeFirstBumpAngle = Math.PI + leftSeparatorAlpha + leftStartAlpha; + travelBetweenBumpsAngle = Math.PI + leftSeparatorAlpha + rightSeparatorAlpha - (rightStartAlpha - leftStartAlpha); + travelToStartAngle = rightSeparatorAlpha - rightStartAlpha; + } + + const totalAnimationAngle = travelBeforeFirstBumpAngle + travelBetweenBumpsAngle + travelToStartAngle; + const beforeFirstBumpPercentage = (travelBeforeFirstBumpAngle / totalAnimationAngle) * 100; + const beforeSecondBumpPercentage = + ((travelBeforeFirstBumpAngle + travelBetweenBumpsAngle) / totalAnimationAngle) * 100; + const duration = (fullRotationPeriod * totalAnimationAngle) / (2 * Math.PI); + + return planets.map(({ startAlpha: planetStartAlpha }) => ({ + bounces: true, + leftEdgeAlpha: leftSeparatorAlpha - planetStartAlpha + leftStartAlpha, + rightEdgeAlpha: rightSeparatorAlpha + planetStartAlpha - rightStartAlpha, + beforeFirstBumpPercentage, + beforeSecondBumpPercentage, + duration + })); + }, [bottomGap, direction, fullRotationPeriod, orderedPlanets, planets, radius]); + + return ( + <> +
+ {planets.map((planet, index) => ( + + ))} +
+ + ); +}); diff --git a/src/app/pages/Unlock/planets-animation/planet.tsx b/src/app/pages/Unlock/planets-animation/planet.tsx new file mode 100644 index 0000000000..80537f17ab --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/planet.tsx @@ -0,0 +1,61 @@ +import React, { memo, useMemo } from 'react'; + +import { OrbitProps, PlanetAnimationParams, PlanetProps } from './types'; + +interface Props { + planet: PlanetProps; + animationParams: PlanetAnimationParams; + orbitRadius: OrbitProps['radius']; + direction: OrbitProps['direction']; +} + +const makePlanetAnimationKeyframe = (percentage: number, alpha: number, orbitRadius: number) => `${percentage}% { + transform: translate(-50%, -50%) rotate(${alpha}rad) translateX(${orbitRadius}px) rotate(${-alpha}rad); + }`; + +export const Planet = memo(({ planet, animationParams, orbitRadius, direction }) => { + const { id, startAlpha } = planet; + + const planetAnimationName = useMemo(() => `planet-motion-${id}`, [id]); + const animationsStylesheetText = useMemo(() => { + const isClockwise = direction === 'clockwise'; + + if (!animationParams.bounces) { + const endAlpha = startAlpha + 2 * Math.PI * (isClockwise ? 1 : -1); + + return `@keyframes ${planetAnimationName} { + ${makePlanetAnimationKeyframe(0, startAlpha, orbitRadius)} + + ${makePlanetAnimationKeyframe(100, endAlpha, orbitRadius)} +}`; + } + + const { beforeFirstBumpPercentage, beforeSecondBumpPercentage, leftEdgeAlpha, rightEdgeAlpha } = animationParams; + const beforeFirstBumpAlpha = isClockwise ? rightEdgeAlpha : -Math.PI - leftEdgeAlpha; + const beforeSecondBumpAlpha = isClockwise ? -Math.PI - leftEdgeAlpha : rightEdgeAlpha; + + return `@keyframes ${planetAnimationName} { + ${makePlanetAnimationKeyframe(0, startAlpha, orbitRadius)} + + ${makePlanetAnimationKeyframe(beforeFirstBumpPercentage, beforeFirstBumpAlpha, orbitRadius)} + + ${makePlanetAnimationKeyframe(beforeSecondBumpPercentage, beforeSecondBumpAlpha, orbitRadius)} + + ${makePlanetAnimationKeyframe(100, startAlpha, orbitRadius)} +}`; + }, [animationParams, direction, orbitRadius, planetAnimationName, startAlpha]); + + return ( + <> + +
+ {planet.item} +
+ + ); +}); diff --git a/src/app/pages/Unlock/planets-animation/sun-glow.svg b/src/app/pages/Unlock/planets-animation/sun-glow.svg new file mode 100644 index 0000000000..fb5deee414 --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/sun-glow.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/app/pages/Unlock/planets-animation/types.ts b/src/app/pages/Unlock/planets-animation/types.ts new file mode 100644 index 0000000000..aaae4bed29 --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/types.ts @@ -0,0 +1,39 @@ +import { ReactNode } from 'react'; + +export interface PlanetProps { + startAlpha: number; + /** Must be unique */ + id: string | number; + radius: number; + item: ReactNode | ReactNode[]; +} + +export interface OrbitProps { + fullRotationPeriod: number; + radius: number; + planets: PlanetProps[]; + direction: 'clockwise' | 'counter-clockwise'; +} + +export interface PlanetsAnimationProps { + bottomGap: number; +} + +interface PlanetStickAnimationParamsBase { + bounces: boolean; + duration: number; +} + +interface NormalPlanetAnimationParams extends PlanetStickAnimationParamsBase { + bounces: false; +} + +interface BouncingPlanetAnimationParams extends PlanetStickAnimationParamsBase { + bounces: true; + leftEdgeAlpha: number; + rightEdgeAlpha: number; + beforeFirstBumpPercentage: number; + beforeSecondBumpPercentage: number; +} + +export type PlanetAnimationParams = NormalPlanetAnimationParams | BouncingPlanetAnimationParams; diff --git a/src/app/pages/Unlock/planets-animation/utils.ts b/src/app/pages/Unlock/planets-animation/utils.ts new file mode 100644 index 0000000000..7ca3c4d64e --- /dev/null +++ b/src/app/pages/Unlock/planets-animation/utils.ts @@ -0,0 +1,8 @@ +import { SUN_RADIUS } from './constants'; + +/** + * Calculates an angle between a horizontal and a line through the center of the 'sun' and the point where + * a 'planet' hits the bottom gap. + */ +export const calculateBottomGapAngle = (bottomGap: number, orbitRadius: number, planetRadius: number) => + Math.asin((SUN_RADIUS + bottomGap - planetRadius) / orbitRadius); diff --git a/src/app/pages/Unlock/reset-extension-modal.tsx b/src/app/pages/Unlock/reset-extension-modal.tsx new file mode 100644 index 0000000000..f40131f897 --- /dev/null +++ b/src/app/pages/Unlock/reset-extension-modal.tsx @@ -0,0 +1,82 @@ +import React, { memo, useCallback, useState } from 'react'; + +import browser from 'webextension-polyfill'; + +import { Alert } from 'app/atoms'; +import { + ActionModal, + ActionModalBodyContainer, + ActionModalButton, + ActionModalButtonsContainer +} from 'app/atoms/action-modal'; +import { T, t } from 'lib/i18n'; +import { clearAllStorages } from 'lib/temple/reset'; +import { useAlert } from 'lib/ui'; + +import { UnlockSelectors } from './Unlock.selectors'; + +interface ResetExtensionModalProps { + onClose: EmptyFn; +} + +export const ResetExtensionModal = memo(({ onClose }) => { + const [submitting, setSubmitting] = useState(false); + const customAlert = useAlert(); + + const resetWallet = useCallback(async () => { + try { + setSubmitting(true); + await clearAllStorages(); + browser.runtime.reload(); + } catch (err: any) { + await customAlert({ + title: t('error'), + children: err.message + }); + } finally { + setSubmitting(false); + } + }, [customAlert]); + + return ( + + + + +

+ } + /> + +

+ +

+
+ + + + + + + + + + +
+ ); +}); diff --git a/src/app/pages/Welcome/Welcome.tsx b/src/app/pages/Welcome/Welcome.tsx index f297d4579f..49097253d7 100644 --- a/src/app/pages/Welcome/Welcome.tsx +++ b/src/app/pages/Welcome/Welcome.tsx @@ -57,7 +57,7 @@ const Welcome = memo(() => { ); return ( - + = { - 1: 'ethereum', - 11155111: 'sepolia', - 137: 'polygon', - 56: 'smartchain', + [ETHEREUM_MAINNET_CHAIN_ID]: 'ethereum', + [ETH_SEPOLIA_CHAIN_ID]: 'sepolia', + [OTHER_COMMON_MAINNET_CHAIN_IDS.polygon]: 'polygon', + [OTHER_COMMON_MAINNET_CHAIN_IDS.bsc]: 'smartchain', 97: 'bnbt', - 43114: 'avalanchex', + [OTHER_COMMON_MAINNET_CHAIN_IDS.avalanche]: 'avalanchex', 43113: 'avalanchecfuji', - 10: 'optimism', + [OTHER_COMMON_MAINNET_CHAIN_IDS.optimism]: 'optimism', 42170: 'arbitrumnova', 1313161554: 'aurora', 81457: 'blast', @@ -212,8 +213,8 @@ const chainIdsChainNamesRecord: Record = { 100: 'xdai', 324: 'zksync', 787: 'acalaevm', - 42161: 'arbitrum', - 8453: 'base', + [OTHER_COMMON_MAINNET_CHAIN_IDS.arbitrum]: 'arbitrum', + [OTHER_COMMON_MAINNET_CHAIN_IDS.base]: 'base', 321: 'kcc', 4200: 'merlin', 82: 'meter', diff --git a/src/lib/temple/types.ts b/src/lib/temple/types.ts index 858fea6c6f..5e6b61ac98 100644 --- a/src/lib/temple/types.ts +++ b/src/lib/temple/types.ts @@ -30,6 +30,14 @@ export interface TempleState { export const TEZOS_MAINNET_CHAIN_ID = 'NetXdQprcVkpaWU'; export const ETHEREUM_MAINNET_CHAIN_ID = 1; +export const OTHER_COMMON_MAINNET_CHAIN_IDS = { + polygon: 137, + bsc: 56, + avalanche: 43114, + optimism: 10, + arbitrum: 42161, + base: 8453 +}; export const ETH_SEPOLIA_CHAIN_ID = 11155111; export enum TempleTezosChainId { diff --git a/src/temple/front/block-explorers.ts b/src/temple/front/block-explorers.ts index abd273ac74..4e5eb687f8 100644 --- a/src/temple/front/block-explorers.ts +++ b/src/temple/front/block-explorers.ts @@ -5,7 +5,12 @@ import { nanoid } from 'nanoid'; import { BLOCKCHAIN_EXPLORERS_OVERRIDES_STORAGE_KEY } from 'lib/constants'; import { useStorage } from 'lib/temple/front/storage'; -import { TempleTezosChainId } from 'lib/temple/types'; +import { + OTHER_COMMON_MAINNET_CHAIN_IDS, + ETHEREUM_MAINNET_CHAIN_ID, + ETH_SEPOLIA_CHAIN_ID, + TempleTezosChainId +} from 'lib/temple/types'; import { EMPTY_FROZEN_OBJ } from 'lib/utils'; import { TempleChainKind } from 'temple/types'; @@ -227,42 +232,42 @@ const DEFAULT_BLOCK_EXPLORERS_BASE: Record = { - '1': { + [ETHEREUM_MAINNET_CHAIN_ID]: { name: 'Ethereum Mainnet', testnet: false, currency: DEFAULT_EVM_CURRENCY }, - '137': { + [OTHER_COMMON_MAINNET_CHAIN_IDS.polygon]: { name: 'Polygon Mainnet', testnet: false, currency: { @@ -56,7 +61,7 @@ const DEFAULT_EVM_CHAINS_SPECS: Record = [ id: 'eth-mainnet', name: 'Ethereum Mainnet', chain: TempleChainKind.EVM, - chainId: 1, + chainId: ETHEREUM_MAINNET_CHAIN_ID, rpcBaseURL: 'https://cloudflare-eth.com', color: '#0036fc', default: true @@ -139,7 +144,7 @@ export const EVM_DEFAULT_NETWORKS: NonEmptyArray = [ id: 'matic-mainnet', name: 'Polygon Mainnet', chain: TempleChainKind.EVM, - chainId: 137, + chainId: OTHER_COMMON_MAINNET_CHAIN_IDS.polygon, rpcBaseURL: 'https://polygon-rpc.com', color: '#725ae8', default: true @@ -148,7 +153,7 @@ export const EVM_DEFAULT_NETWORKS: NonEmptyArray = [ id: 'bsc-mainnet', name: 'BSC Mainnet', chain: TempleChainKind.EVM, - chainId: 56, + chainId: OTHER_COMMON_MAINNET_CHAIN_IDS.bsc, rpcBaseURL: 'https://bsc-rpc.publicnode.com', description: 'Binance Smart Chain Mainnet', color: '#f5d300', @@ -158,7 +163,7 @@ export const EVM_DEFAULT_NETWORKS: NonEmptyArray = [ id: 'avalanche-mainnet', name: 'Avalanche Mainnet', chain: TempleChainKind.EVM, - chainId: 43114, + chainId: OTHER_COMMON_MAINNET_CHAIN_IDS.avalanche, rpcBaseURL: 'https://avalanche.drpc.org', color: '#ff5959', default: true @@ -167,7 +172,7 @@ export const EVM_DEFAULT_NETWORKS: NonEmptyArray = [ id: 'optimism-mainnet', name: 'OP Mainnet', chain: TempleChainKind.EVM, - chainId: 10, + chainId: OTHER_COMMON_MAINNET_CHAIN_IDS.avalanche, rpcBaseURL: 'https://mainnet.optimism.io', description: 'Optimism Mainnet', color: '#fc0000', @@ -177,7 +182,7 @@ export const EVM_DEFAULT_NETWORKS: NonEmptyArray = [ id: 'eth-sepolia', name: 'Ethereum Sepolia', chain: TempleChainKind.EVM, - chainId: 11155111, + chainId: ETH_SEPOLIA_CHAIN_ID, rpcBaseURL: 'https://ethereum-sepolia-rpc.publicnode.com', color: '#010b79', default: true