diff --git a/package.json b/package.json index fb6b6adcb5..be9d66f6ab 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "crypto-js": "4.1.1", "d3": "^7.6.1", "dayjs": "^1.11.6", + "dompurify": "^3.0.6", "ethers": "^5.4.6", "events": "^3.3.0", "find-replacement-tx": "^1.2.3", @@ -154,6 +155,7 @@ "@types/big.js": "^6.0.0", "@types/crypto-js": "4.1.1", "@types/d3": "^7.1.0", + "@types/dompurify": "^3.0.3", "@types/mixpanel-browser": "^2.38.0", "@types/multicodec": "^1.0.0", "@types/node": "^13.13.52", diff --git a/src/components/Announcement/Popups/CenterPopup.tsx b/src/components/Announcement/Popups/CenterPopup.tsx index f9cfa6fb01..23e6422dbf 100644 --- a/src/components/Announcement/Popups/CenterPopup.tsx +++ b/src/components/Announcement/Popups/CenterPopup.tsx @@ -4,7 +4,6 @@ import { useMedia } from 'react-use' import styled from 'styled-components' import CtaButton from 'components/Announcement/Popups/CtaButton' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePopup, PopupContentAnnouncement, @@ -17,6 +16,7 @@ import { Z_INDEXS } from 'constants/styles' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import { MEDIA_WIDTHS } from 'theme' +import { useNavigateToUrl } from 'utils/redirect' import { escapeScriptHtml } from 'utils/string' const Wrapper = styled.div` diff --git a/src/components/Announcement/Popups/DetailAnnouncementPopup.tsx b/src/components/Announcement/Popups/DetailAnnouncementPopup.tsx index 04fbcd68cb..5625a397cb 100644 --- a/src/components/Announcement/Popups/DetailAnnouncementPopup.tsx +++ b/src/components/Announcement/Popups/DetailAnnouncementPopup.tsx @@ -6,7 +6,6 @@ import styled from 'styled-components' import NotificationImage from 'assets/images/notification_default.png' import CtaButton from 'components/Announcement/Popups/CtaButton' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePopup } from 'components/Announcement/type' import Modal from 'components/Modal' import Row from 'components/Row' @@ -14,6 +13,7 @@ import { Z_INDEXS } from 'constants/styles' import useTheme from 'hooks/useTheme' import { useDetailAnnouncement } from 'state/application/hooks' import { MEDIA_WIDTHS } from 'theme' +import { useNavigateToUrl } from 'utils/redirect' import { escapeScriptHtml } from 'utils/string' const PaginationButton = styled.div` diff --git a/src/components/Announcement/Popups/SnippetPopup.tsx b/src/components/Announcement/Popups/SnippetPopup.tsx index 3a83802a87..3214bf6729 100644 --- a/src/components/Announcement/Popups/SnippetPopup.tsx +++ b/src/components/Announcement/Popups/SnippetPopup.tsx @@ -8,7 +8,6 @@ import { Swiper, SwiperSlide } from 'swiper/react' import NotificationImage from 'assets/images/notification_default.png' import CtaButton from 'components/Announcement/Popups/CtaButton' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePopup, PopupContentAnnouncement, @@ -20,6 +19,7 @@ import { Z_INDEXS } from 'constants/styles' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import { useDetailAnnouncement, useRemovePopup } from 'state/application/hooks' +import { useNavigateToUrl } from 'utils/redirect' const IMAGE_HEIGHT = '124px' const PADDING_MOBILE = '16px' diff --git a/src/components/Announcement/Popups/TopBanner.tsx b/src/components/Announcement/Popups/TopBanner.tsx index 2ee8a5b0e4..bc3efc59a6 100644 --- a/src/components/Announcement/Popups/TopBanner.tsx +++ b/src/components/Announcement/Popups/TopBanner.tsx @@ -5,13 +5,13 @@ import { useMedia } from 'react-use' import styled, { css, keyframes } from 'styled-components' import CtaButton from 'components/Announcement/Popups/CtaButton' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePopup, PopupType } from 'components/Announcement/type' import Announcement from 'components/Icons/Announcement' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import { useActivePopups, useRemoveAllPopupByType } from 'state/application/hooks' import { MEDIA_WIDTHS } from 'theme' +import { useNavigateToUrl } from 'utils/redirect' import { escapeScriptHtml } from 'utils/string' const BannerWrapper = styled.div<{ color?: string }>` diff --git a/src/components/Announcement/PrivateAnnoucement/InboxItemPoolPosition.tsx b/src/components/Announcement/PrivateAnnoucement/InboxItemPoolPosition.tsx index 01b8948cfd..8d266aa261 100644 --- a/src/components/Announcement/PrivateAnnoucement/InboxItemPoolPosition.tsx +++ b/src/components/Announcement/PrivateAnnoucement/InboxItemPoolPosition.tsx @@ -12,13 +12,13 @@ import { RowItem, Title, } from 'components/Announcement/PrivateAnnoucement/styled' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePoolPosition } from 'components/Announcement/type' import { DoubleCurrencyLogoV2 } from 'components/DoubleLogo' import { MoneyBag } from 'components/Icons' import { APP_PATHS } from 'constants/index' import { NETWORKS_INFO } from 'constants/networks' import useTheme from 'hooks/useTheme' +import { useNavigateToUrl } from 'utils/redirect' function InboxItemBridge({ announcement, diff --git a/src/components/Announcement/PrivateAnnoucement/InboxItemPriceAlert.tsx b/src/components/Announcement/PrivateAnnoucement/InboxItemPriceAlert.tsx index 588394a8ae..931e58ef45 100644 --- a/src/components/Announcement/PrivateAnnoucement/InboxItemPriceAlert.tsx +++ b/src/components/Announcement/PrivateAnnoucement/InboxItemPriceAlert.tsx @@ -6,12 +6,12 @@ import { Flex, Text } from 'rebass' import { PrivateAnnouncementProp } from 'components/Announcement/PrivateAnnoucement' import InboxIcon from 'components/Announcement/PrivateAnnoucement/Icon' import { Dot, InboxItemRow, InboxItemWrapper, RowItem, Title } from 'components/Announcement/PrivateAnnoucement/styled' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePriceAlert } from 'components/Announcement/type' import { ButtonLight } from 'components/Button' import DeltaTokenAmount from 'components/WalletPopup/Transactions/DeltaTokenAmount' import useTheme from 'hooks/useTheme' import { HistoricalPriceAlert, PriceAlertType } from 'pages/NotificationCenter/const' +import { useNavigateToUrl } from 'utils/redirect' export const getSwapUrlPriceAlert = (alert: HistoricalPriceAlert) => { const { swapURL } = alert diff --git a/src/components/Announcement/PrivateAnnoucement/InboxItemPrivateMessage.tsx b/src/components/Announcement/PrivateAnnoucement/InboxItemPrivateMessage.tsx index 7fc2c278e9..98b23a8bd9 100644 --- a/src/components/Announcement/PrivateAnnoucement/InboxItemPrivateMessage.tsx +++ b/src/components/Announcement/PrivateAnnoucement/InboxItemPrivateMessage.tsx @@ -3,8 +3,8 @@ import styled from 'styled-components' import { PrivateAnnouncementProp } from 'components/Announcement/PrivateAnnoucement' import InboxIcon from 'components/Announcement/PrivateAnnoucement/Icon' import { Dot, InboxItemRow, InboxItemWrapper, RowItem, Title } from 'components/Announcement/PrivateAnnoucement/styled' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePopup } from 'components/Announcement/type' +import { useNavigateToUrl } from 'utils/redirect' import { escapeScriptHtml } from 'utils/string' const Desc = styled.div` diff --git a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PoolPosition.tsx b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PoolPosition.tsx index 0bbb9cbec9..88d59dd335 100644 --- a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PoolPosition.tsx +++ b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PoolPosition.tsx @@ -7,13 +7,13 @@ import styled from 'styled-components' import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' import InboxIcon from 'components/Announcement/PrivateAnnoucement/Icon' import { PrivateAnnouncementPropCenter } from 'components/Announcement/PrivateAnnoucement/NotificationCenter' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePoolPosition } from 'components/Announcement/type' import { DoubleCurrencyLogoV2 } from 'components/DoubleLogo' import { MoneyBag } from 'components/Icons' import { APP_PATHS } from 'constants/index' import { NETWORKS_INFO } from 'constants/networks' import useTheme from 'hooks/useTheme' +import { useNavigateToUrl } from 'utils/redirect' import { formatTime } from 'utils/time' import { ArrowWrapper, Desc, Time, Title, Wrapper } from './styled' diff --git a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PriceAlert.tsx b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PriceAlert.tsx index dde6f45330..4f2ed92249 100644 --- a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PriceAlert.tsx +++ b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PriceAlert.tsx @@ -5,9 +5,9 @@ import styled from 'styled-components' import InboxIcon from 'components/Announcement/PrivateAnnoucement/Icon' import { getSwapUrlPriceAlert } from 'components/Announcement/PrivateAnnoucement/InboxItemPriceAlert' import { PrivateAnnouncementPropCenter } from 'components/Announcement/PrivateAnnoucement/NotificationCenter' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePriceAlert } from 'components/Announcement/type' import AlertCondition from 'pages/NotificationCenter/PriceAlerts/AlertCondition' +import { useNavigateToUrl } from 'utils/redirect' import { formatTime } from 'utils/time' import { Desc, Time, Title, Wrapper } from './styled' diff --git a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PrivateMessage.tsx b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PrivateMessage.tsx index 5eeaef808c..a5c87203c1 100644 --- a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PrivateMessage.tsx +++ b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/PrivateMessage.tsx @@ -2,8 +2,8 @@ import { Flex } from 'rebass' import InboxIcon from 'components/Announcement/PrivateAnnoucement/Icon' import { PrivateAnnouncementPropCenter } from 'components/Announcement/PrivateAnnoucement/NotificationCenter' -import { useNavigateToUrl } from 'components/Announcement/helper' import { AnnouncementTemplatePopup } from 'components/Announcement/type' +import { useNavigateToUrl } from 'utils/redirect' import { escapeScriptHtml } from 'utils/string' import { formatTime } from 'utils/time' diff --git a/src/components/Announcement/helper.ts b/src/components/Announcement/helper.ts index 1dd590f0cb..6dfcf4dfca 100644 --- a/src/components/Announcement/helper.ts +++ b/src/components/Announcement/helper.ts @@ -1,12 +1,9 @@ import { ChainId } from '@kyberswap/ks-sdk-core' import { useCallback } from 'react' -import { useNavigate } from 'react-router-dom' import AnnouncementApi from 'services/announcement' import { AnnouncementTemplatePopup, PopupContentAnnouncement, PopupItemType } from 'components/Announcement/type' import { TIMES_IN_SECS } from 'constants/index' -import { useActiveWeb3React } from 'hooks' -import { useChangeNetwork } from 'hooks/web3/useChangeNetwork' import { useAppDispatch } from 'state/hooks' const LsKey = 'ack-announcements' @@ -46,47 +43,6 @@ export const isPopupCanShow = ( return !isRead && !isExpired && isRightChain && isOwn } -/** - * this hook to navigate to specific url - * detect using window.open or navigate (react-router) - * check change chain if needed - */ -export const useNavigateToUrl = () => { - const navigate = useNavigate() - const { chainId: currentChain } = useActiveWeb3React() - const { changeNetwork } = useChangeNetwork() - - const redirect = useCallback( - (actionURL: string) => { - if (actionURL && actionURL.startsWith('/')) { - navigate(actionURL) - return - } - const { pathname, host, search } = new URL(actionURL) - if (window.location.host === host) { - navigate(`${pathname}${search}`) - } else { - window.open(actionURL) - } - }, - [navigate], - ) - - return useCallback( - (actionURL: string, chainId?: ChainId) => { - try { - if (!actionURL) return - if (chainId && chainId !== currentChain) { - changeNetwork(chainId, () => redirect(actionURL), undefined, true) - } else { - redirect(actionURL) - } - } catch (error) {} - }, - [changeNetwork, currentChain, redirect], - ) -} - export const useInvalidateTags = (reducerPath: string) => { const dispatch = useAppDispatch() return useCallback( diff --git a/src/constants/index.ts b/src/constants/index.ts index 45e6354254..f34245325a 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -240,6 +240,10 @@ export const APP_PATHS = { PROFILE_MANAGE: '/manage', ELASTIC_LEGACY: '/elastic-legacy', VERIFY_AUTH: '/auth', + + IAM_LOGIN: '/login', + IAM_LOGOUT: '/logout', + IAM_CONSENT: '/consent', } as const export const TERM_FILES_PATH = { diff --git a/src/hooks/useSessionExpire.ts b/src/hooks/useSessionExpire.ts index 82e8faa1c9..e000a5aca1 100644 --- a/src/hooks/useSessionExpire.ts +++ b/src/hooks/useSessionExpire.ts @@ -37,7 +37,9 @@ export default function useSessionExpiredGlobal() { if (isKyberAIPage && accountId === signedAccount) { delete data.cancelText } - showConfirm(data) + + const isIAMPages = [APP_PATHS.IAM_CONSENT, APP_PATHS.IAM_LOGIN, APP_PATHS.IAM_LOGOUT].includes(pathname) + if (!isIAMPages) showConfirm(data) } KyberOauth2.on(KyberOauth2Event.SESSION_EXPIRED, listener) return () => KyberOauth2.off(KyberOauth2Event.SESSION_EXPIRED, listener) diff --git a/src/pages/About/AboutKyberSwap/MeetTheTeam.tsx b/src/pages/About/AboutKyberSwap/MeetTheTeam.tsx index c85fe499e4..8903ddb64a 100644 --- a/src/pages/About/AboutKyberSwap/MeetTheTeam.tsx +++ b/src/pages/About/AboutKyberSwap/MeetTheTeam.tsx @@ -16,6 +16,7 @@ import VictorTran from 'assets/images/kyber_members/victor_tran.png' import { ReactComponent as LinkedInIcon } from 'assets/svg/linkedin.svg' import { ReactComponent as TwitterIcon } from 'assets/svg/solid_twitter_icon.svg' import useTheme from 'hooks/useTheme' +import { ExternalLink } from 'theme' type Member = { name: string @@ -186,14 +187,14 @@ const MemberView: React.FC = props => { }} > {props.handles.twitter && ( - + - + )} {props.handles.linkedIn && ( - + - + )} )} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index acb2fe4ee2..a92dd039b1 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -43,6 +43,10 @@ import { isAddressString, isSupportLimitOrder, shortenAddress } from 'utils' import ElasticLegacyNotice from './ElasticLegacy/ElasticLegacyNotice' import VerifyAuth from './Verify/VerifyAuth' +const Login = lazy(() => import('./Oauth/Login')) +const Logout = lazy(() => import('./Oauth/Logout')) +const Consent = lazy(() => import('./Oauth/Consent')) + // test page for swap only through elastic const ElasticSwap = lazy(() => import('./ElasticSwap')) const SwapV2 = lazy(() => import('./SwapV2')) @@ -430,6 +434,9 @@ export default function App() { } /> } /> + } /> + } /> + } /> } /> diff --git a/src/pages/KyberDAO/StakeKNC/index.tsx b/src/pages/KyberDAO/StakeKNC/index.tsx index c86ecf5e51..0863bafbc8 100644 --- a/src/pages/KyberDAO/StakeKNC/index.tsx +++ b/src/pages/KyberDAO/StakeKNC/index.tsx @@ -289,9 +289,9 @@ export default function StakeKNC() { You can access legacy KyberDAO v1 to read about previous KIPs{' '} - + here ↗ - + diff --git a/src/pages/NotificationCenter/GeneralAnnouncement/AnnouncementItem.tsx b/src/pages/NotificationCenter/GeneralAnnouncement/AnnouncementItem.tsx index 37d2f9fbae..73eb89ec37 100644 --- a/src/pages/NotificationCenter/GeneralAnnouncement/AnnouncementItem.tsx +++ b/src/pages/NotificationCenter/GeneralAnnouncement/AnnouncementItem.tsx @@ -7,9 +7,9 @@ import NotificationImage from 'assets/images/notification_default.png' import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' import CtaButton from 'components/Announcement/Popups/CtaButton' import { formatCtaName } from 'components/Announcement/Popups/DetailAnnouncementPopup' -import { useNavigateToUrl } from 'components/Announcement/helper' import { Announcement } from 'components/Announcement/type' import { MEDIA_WIDTHS } from 'theme' +import { useNavigateToUrl } from 'utils/redirect' import { escapeScriptHtml } from 'utils/string' import { formatTime } from 'utils/time' diff --git a/src/pages/Oauth/AuthForm/AuthFormMessage.tsx b/src/pages/Oauth/AuthForm/AuthFormMessage.tsx new file mode 100644 index 0000000000..fe979c286b --- /dev/null +++ b/src/pages/Oauth/AuthForm/AuthFormMessage.tsx @@ -0,0 +1,22 @@ +import { Text } from 'rebass' + +import useTheme from 'hooks/useTheme' + +const AuthFormFieldMessage: React.FC<{ messages?: { type: string; text: string }[] }> = ({ messages }) => { + const theme = useTheme() + if (!messages?.length) return null + + return ( +
+ {messages.map((value, index) => { + return ( + + {value.text} + + ) + })} +
+ ) +} + +export default AuthFormFieldMessage diff --git a/src/pages/Oauth/AuthForm/ButtonEth.tsx b/src/pages/Oauth/AuthForm/ButtonEth.tsx new file mode 100644 index 0000000000..c95509a665 --- /dev/null +++ b/src/pages/Oauth/AuthForm/ButtonEth.tsx @@ -0,0 +1,88 @@ +import { LoginMethod } from '@kybernetwork/oauth2' +import { Trans } from '@lingui/macro' +import { useCallback } from 'react' +import { Flex, Text } from 'rebass' + +import { ButtonOutlined, ButtonPrimary } from 'components/Button' +import Wallet from 'components/Icons/Wallet' +import Loader from 'components/Loader' +import { useActiveWeb3React } from 'hooks' +import useAutoSignIn from 'pages/Oauth/AuthForm/useAutoSignIn' +import { FlowStatus } from 'pages/Oauth/Login' +import { useWalletModalToggle } from 'state/application/hooks' +import { navigateToUrl } from 'utils/redirect' + +const ButtonEth = ({ + loading, + disabled, + onClick, + flowStatus, + showBtnCancel, + backUrl, +}: { + disabled: boolean + loading: boolean + onClick: () => void + backUrl: string | undefined + flowStatus: FlowStatus + showBtnCancel: boolean +}) => { + const toggleWalletModal = useWalletModalToggle() + const { account } = useActiveWeb3React() + + const onClickEth = useCallback( + (e?: React.MouseEvent) => { + e?.preventDefault?.() + !account ? toggleWalletModal() : onClick() + }, + [toggleWalletModal, onClick, account], + ) + + useAutoSignIn({ onClick: onClickEth, flowStatus, method: LoginMethod.ETH }) + + return ( + + {showBtnCancel && ( + { + e.preventDefault() + navigateToUrl(backUrl) + }} + > + Cancel + + )} + { + e.preventDefault() + onClickEth() + }} + disabled={disabled} + > + {loading ? ( + <> + +  {' '} + + {' '} + Signing In + + + ) : ( + <> + +   Sign-In with Wallet + + )} + + + ) +} + +export default ButtonEth diff --git a/src/pages/Oauth/AuthForm/ButtonGoogle.tsx b/src/pages/Oauth/AuthForm/ButtonGoogle.tsx new file mode 100644 index 0000000000..34e1896d94 --- /dev/null +++ b/src/pages/Oauth/AuthForm/ButtonGoogle.tsx @@ -0,0 +1,37 @@ +import { LoginMethod } from '@kybernetwork/oauth2' +import { Trans } from '@lingui/macro' +import React, { useCallback, useRef } from 'react' + +import { ButtonOutlined, ButtonPrimary } from 'components/Button' +import useAutoSignIn from 'pages/Oauth/AuthForm/useAutoSignIn' +import { FlowStatus } from 'pages/Oauth/Login' + +interface Props { + outline: boolean + flowStatus: FlowStatus +} + +const ButtonGoogle: React.FC = ({ outline, flowStatus }) => { + const ref = useRef(null) + const { autoLoginMethod } = flowStatus + const isAutoLogin = autoLoginMethod === LoginMethod.GOOGLE + + const onClick = useCallback(() => { + ref.current?.click?.() + }, []) + + useAutoSignIn({ onClick, flowStatus, method: LoginMethod.GOOGLE }) + + const props = { + height: '36px', + id: 'btnLoginGoogle', + type: 'submit', + value: 'google', + name: 'provider', + ref, + children: Sign-In with Google, + style: isAutoLogin ? { opacity: 0 } : undefined, + } + return React.createElement(outline ? ButtonOutlined : ButtonPrimary, props) +} +export default ButtonGoogle diff --git a/src/pages/Oauth/AuthForm/index.tsx b/src/pages/Oauth/AuthForm/index.tsx new file mode 100644 index 0000000000..0d287d748d --- /dev/null +++ b/src/pages/Oauth/AuthForm/index.tsx @@ -0,0 +1,68 @@ +import { LoginFlow, LoginMethod } from '@kybernetwork/oauth2' +import React from 'react' +import { isMobile } from 'react-device-detect' +import { Flex } from 'rebass' +import styled from 'styled-components' + +import useParsedQueryString from 'hooks/useParsedQueryString' +import useTheme from 'hooks/useTheme' +import ButtonEth from 'pages/Oauth/AuthForm/ButtonEth' +import ButtonGoogle from 'pages/Oauth/AuthForm/ButtonGoogle' +import { FlowStatus } from 'pages/Oauth/Login' +import { validateRedirectURL } from 'utils/redirect' + +import { getSupportLoginMethods } from '../helpers' +import AuthFormFieldMessage from './AuthFormMessage' + +const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; +` + +interface AuthFormProps extends React.FormHTMLAttributes { + formConfig: LoginFlow | undefined + signInWithEth: () => void + disableEth: boolean + flowStatus: FlowStatus +} + +const Splash = () =>
+ +const AuthForm: React.FC = ({ formConfig, signInWithEth, flowStatus, disableEth }) => { + const { back_uri } = useParsedQueryString<{ back_uri: string }>() + const theme = useTheme() + if (!formConfig) return null + + const { autoLoginMethod, processingSignIn } = flowStatus + const { ui } = formConfig + const loginMethods = getSupportLoginMethods(formConfig) + + const showEth = loginMethods.includes(LoginMethod.ETH) && autoLoginMethod !== LoginMethod.GOOGLE + const hasGoogle = loginMethods.includes(LoginMethod.GOOGLE) + const showBtnCancel = !isMobile && !hasGoogle && validateRedirectURL(back_uri) && !processingSignIn + const hasBothEthAndGoogle = hasGoogle && showEth + return ( +
+ + {showEth && ( + + )} + {hasBothEthAndGoogle && ( + + or + + )} + {hasGoogle && } + + ) +} +export default AuthForm diff --git a/src/pages/Oauth/AuthForm/useAutoSignIn.tsx b/src/pages/Oauth/AuthForm/useAutoSignIn.tsx new file mode 100644 index 0000000000..25c825ab2e --- /dev/null +++ b/src/pages/Oauth/AuthForm/useAutoSignIn.tsx @@ -0,0 +1,27 @@ +import { LoginMethod } from '@kybernetwork/oauth2' +import { useEffect, useRef } from 'react' + +import { useEagerConnect } from 'hooks/web3/useEagerConnect' +import { FlowStatus } from 'pages/Oauth/Login' + +const useAutoSignIn = ({ + onClick, + method, + flowStatus: { flowReady, autoLoginMethod }, +}: { + onClick: (e?: React.MouseEvent) => void + method: LoginMethod + flowStatus: FlowStatus +}) => { + const autoSelect = useRef(false) + const { current: triedEager } = useEagerConnect() + useEffect(() => { + if (autoSelect.current || !flowReady || autoLoginMethod !== method) return + if ((triedEager && autoLoginMethod === LoginMethod.ETH) || autoLoginMethod === LoginMethod.GOOGLE) { + autoSelect.current = true + onClick() + } + }, [flowReady, autoLoginMethod, onClick, triedEager, method]) +} + +export default useAutoSignIn diff --git a/src/pages/Oauth/Consent.tsx b/src/pages/Oauth/Consent.tsx new file mode 100644 index 0000000000..75f6a304e2 --- /dev/null +++ b/src/pages/Oauth/Consent.tsx @@ -0,0 +1,37 @@ +import KyberOauth2 from '@kybernetwork/oauth2' +import { Trans } from '@lingui/macro' +import { useEffect } from 'react' + +import Dots from 'components/Dots' +import { ENV_KEY } from 'constants/env' +import useParsedQueryString from 'hooks/useParsedQueryString' +import { PageContainer } from 'pages/Oauth/styled' + +function Page() { + const { consent_challenge } = useParsedQueryString<{ consent_challenge: string }>() + + useEffect(() => { + if (!consent_challenge) return + KyberOauth2.initialize({ mode: ENV_KEY }) + KyberOauth2.oauthUi + .getFlowConsent(consent_challenge) + .then(data => { + console.debug('Oauth resp consent', data) + }) + .catch(err => { + console.debug('Oauth consent error', err) + }) + }, [consent_challenge]) + + return ( + + Checking data + + } + /> + ) +} + +export default Page diff --git a/src/pages/Oauth/Login.tsx b/src/pages/Oauth/Login.tsx new file mode 100644 index 0000000000..ed807e4f62 --- /dev/null +++ b/src/pages/Oauth/Login.tsx @@ -0,0 +1,179 @@ +import KyberOauth2, { LoginFlow, LoginMethod } from '@kybernetwork/oauth2' +import { Trans } from '@lingui/macro' +import { useCallback, useEffect, useRef, useState } from 'react' + +import Loader from 'components/Loader' +import { didUserReject } from 'constants/connectors/utils' +import { ENV_KEY } from 'constants/env' +import { useActiveWeb3React, useWeb3React } from 'hooks' +import useParsedQueryString from 'hooks/useParsedQueryString' +import { Container, Content, KyberLogo, TextDesc } from 'pages/Oauth/styled' +import getShortenAddress from 'utils/getShortenAddress' +import { queryStringToObject } from 'utils/string' +import { formatSignature } from 'utils/transaction' + +import AuthForm from './AuthForm' +import { createSignMessage, getSupportLoginMethods } from './helpers' + +const getErrorMsg = (error: any) => { + const data = error?.response?.data + const isExpired = data?.error?.id === 'self_service_flow_expired' + if (isExpired) { + return ( + + Time to sign-in is Expired, please go back and try again. + + ) + } + + return data?.ui?.messages?.[0]?.text || data?.error?.reason || data?.error?.message || error?.message || error + '' +} + +export type FlowStatus = { + processingSignIn: boolean + flowReady: boolean + autoLoginMethod: LoginMethod | undefined // not waiting for click btn +} + +export function Login() { + const { account, chainId } = useActiveWeb3React() + const { library: provider } = useWeb3React() + + const [authFormConfig, setAuthFormConfig] = useState() + const [error, setError] = useState('') + const [flowStatus, setFlowStatus] = useState({ + flowReady: false, + autoLoginMethod: undefined, + processingSignIn: false, + }) + + const { wallet_address } = useParsedQueryString<{ wallet_address: string }>() + + const loginMethods = getSupportLoginMethods(authFormConfig) + const isSignInEth = loginMethods.includes(LoginMethod.ETH) + const isMismatchEthAddress = + !loginMethods.includes(LoginMethod.GOOGLE) && + isSignInEth && + wallet_address && + account && + wallet_address?.toLowerCase() !== account?.toLowerCase() + + const connectingWallet = useRef(false) + + const signInWithEth = useCallback(async () => { + try { + const siweConfig = authFormConfig?.oauth_client?.metadata?.siwe_config + if (isMismatchEthAddress || !siweConfig || connectingWallet.current || !provider || !account || !chainId) { + return + } + setFlowStatus(v => ({ ...v, processingSignIn: true })) + const { ui, challenge, issued_at } = authFormConfig + connectingWallet.current = true + const csrf = ui.nodes.find(e => e.attributes.name === 'csrf_token')?.attributes?.value ?? '' + const message = createSignMessage({ + address: account, + chainId, + nonce: challenge, + issuedAt: issued_at, + ...siweConfig, + }) + + const signature = await provider.getSigner().signMessage(message) + const resp = await KyberOauth2.oauthUi.loginEthereum({ + address: account, + signature: formatSignature(signature), + csrf, + chainId, + }) + + if (resp) { + connectingWallet.current = false + setFlowStatus(v => ({ ...v, processingSignIn: false })) + } + } catch (error: any) { + if (!didUserReject(error)) { + setError(getErrorMsg(error)) + } + console.error('signInWithEthereum err', error) + connectingWallet.current = false + setFlowStatus(v => ({ ...v, processingSignIn: false })) + } + }, [account, provider, authFormConfig, chainId, isMismatchEthAddress]) + + useEffect(() => { + const getFlowLogin = async () => { + try { + KyberOauth2.initialize({ mode: ENV_KEY }) + const loginFlow = await KyberOauth2.oauthUi.getFlowLogin() + if (!loginFlow) return + setAuthFormConfig(loginFlow) + + const { client_id } = loginFlow.oauth_client + const loginMethods = getSupportLoginMethods(loginFlow) + + let autoLoginMethod: LoginMethod | undefined + const isIncludeGoogle = loginMethods.includes(LoginMethod.GOOGLE) + if (loginMethods.length === 1) { + if (loginMethods.includes(LoginMethod.ANONYMOUS)) { + throw new Error('Not found login method for this app') + } + if (isIncludeGoogle) { + autoLoginMethod = LoginMethod.GOOGLE + } + } + if (loginMethods.includes(LoginMethod.ETH) && !isIncludeGoogle) { + autoLoginMethod = LoginMethod.ETH + } + KyberOauth2.initialize({ clientId: client_id, mode: ENV_KEY }) + setFlowStatus(v => ({ ...v, flowReady: true, autoLoginMethod })) + } catch (error: any) { + const { error_description } = queryStringToObject(window.location.search) + setError(error_description || getErrorMsg(error)) + } + } + getFlowLogin() + }, []) + + const appName = authFormConfig?.oauth_client?.client_name || authFormConfig?.oauth_client?.client_id + + const renderEthMsg = () => + isMismatchEthAddress ? ( + + Your address is mismatched. The expected address is {getShortenAddress(wallet_address)}, but the address + provided is {getShortenAddress(account)}. Please change your wallet address accordingly. + + ) : ( + account && ( + + To get started, please sign-in to verify your ownership of this wallet address {getShortenAddress(account)} + + ) + ) + + return ( + + + + {error ? ( + {error} + ) : flowStatus.autoLoginMethod === LoginMethod.GOOGLE ? ( + + Checking data ... + + ) : isSignInEth && account ? ( + renderEthMsg() + ) : ( + appName && Please sign in to continue with {appName} + )} + + + + ) +} + +export default Login diff --git a/src/pages/Oauth/Logout.tsx b/src/pages/Oauth/Logout.tsx new file mode 100644 index 0000000000..e66feebdb7 --- /dev/null +++ b/src/pages/Oauth/Logout.tsx @@ -0,0 +1,37 @@ +import KyberOauth2 from '@kybernetwork/oauth2' +import { Trans } from '@lingui/macro' +import { useEffect } from 'react' + +import Dots from 'components/Dots' +import { ENV_KEY } from 'constants/env' +import useParsedQueryString from 'hooks/useParsedQueryString' +import { PageContainer } from 'pages/Oauth/styled' + +function Logout() { + const { logout_challenge } = useParsedQueryString<{ logout_challenge: string }>() + + useEffect(() => { + if (!logout_challenge) return + KyberOauth2.initialize({ mode: ENV_KEY }) + KyberOauth2.oauthUi + .acceptLogout(logout_challenge) + .then(data => { + console.debug('Oauth logout resp', data) + }) + .catch(err => { + console.debug('Oauth logout error', err) + }) + }, [logout_challenge]) + + return ( + + Logging out + + } + /> + ) +} + +export default Logout diff --git a/src/pages/Oauth/helpers.ts b/src/pages/Oauth/helpers.ts new file mode 100644 index 0000000000..125dffee13 --- /dev/null +++ b/src/pages/Oauth/helpers.ts @@ -0,0 +1,45 @@ +import { LoginFlow } from '@kybernetwork/oauth2' + +export const getSupportLoginMethods = (loginFlow: LoginFlow | undefined) => { + return loginFlow?.oauth_client?.metadata?.allowed_login_methods ?? [] +} + +type MessageParams = { + domain: string + uri: string + address: string + version: string + nonce: string + chainId: number + issuedAt: string + statement: string +} + +// message follow eip https://eips.ethereum.org/EIPS/eip-4361 +export const createSignMessage = ({ + domain, + uri, + address, + version, + nonce, + chainId, + issuedAt, + statement, +}: MessageParams): string => { + let prefix = [`${domain} wants you to sign in with your Ethereum account:`, address].join('\n') + + prefix = [prefix, statement].join('\n\n') + if (statement) { + prefix += '\n' + } + + const suffix = [ + `URI: ${uri}`, + `Version: ${version}`, + `Chain ID: ` + chainId, + `Nonce: ${nonce}`, + `Issued At: ${issuedAt}`, + ].join('\n') + + return [prefix, suffix].join('\n') +} diff --git a/src/pages/Oauth/styled.tsx b/src/pages/Oauth/styled.tsx new file mode 100644 index 0000000000..10d20def76 --- /dev/null +++ b/src/pages/Oauth/styled.tsx @@ -0,0 +1,59 @@ +import { ReactNode } from 'react' +import styled from 'styled-components' + +import backgroundImage from 'assets/images/truesight-v2/landing-page/background-gradient.png' +import Loader from 'components/Loader' +import { useIsDarkMode } from 'state/user/hooks' + +export const Container = styled.div` + flex: 1; + justify-content: center; + padding: 20px 0; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + background-image: url(${backgroundImage}); + background-size: 100%; + background-repeat: repeat-y; +` + +export const Content = styled.div` + gap: 30px; + display: flex; + flex-direction: column; + align-items: center; + ${({ theme }) => theme.mediaWidth.upToSmall` + gap: 16px; + `}; +` +export const TextDesc = styled.div` + font-size: 20px; + line-height: 24px; + color: ${({ theme }) => theme.subText}; + text-align: center; + ${({ theme }) => theme.mediaWidth.upToSmall` + font-size: 16px; + line-height: 20px; + `}; +` + +export const KyberLogo = () => { + const iseDark = useIsDarkMode() + return ( + loading-icon + ) +} + +export function PageContainer({ msg }: { msg: ReactNode }) { + return ( + + + + + {msg} + + + + ) +} diff --git a/src/pages/TrueSightV2/components/TokenOverview.tsx b/src/pages/TrueSightV2/components/TokenOverview.tsx index 7f6530bd10..3f6ae6f2ee 100644 --- a/src/pages/TrueSightV2/components/TokenOverview.tsx +++ b/src/pages/TrueSightV2/components/TokenOverview.tsx @@ -479,7 +479,8 @@ export const TokenOverview = ({ data, isLoading }: { data?: IAssetOverview; isLo KyberScore uses AI to measure the upcoming trend of a token (bullish or bearish) by taking into account multiple on-chain and off-chain indicators. The score ranges from 0 to 100. Higher the score, - more bullish the token in the short-term. Read more here ↗ + more bullish the token in the short-term. Read more{' '} + here ↗ } placement="top" diff --git a/src/pages/TrueSightV2/components/TruesightFooter.tsx b/src/pages/TrueSightV2/components/TruesightFooter.tsx index 8557d9b152..1f2566c7fa 100644 --- a/src/pages/TrueSightV2/components/TruesightFooter.tsx +++ b/src/pages/TrueSightV2/components/TruesightFooter.tsx @@ -41,7 +41,12 @@ export default function TruesightFooter() { const toggle = useWalletModalToggle() const { account } = useActiveWeb3React() const location = useLocation() - if (account || location.pathname.startsWith(APP_PATHS.MY_EARNINGS)) { + if ( + account || + [APP_PATHS.MY_EARNINGS, APP_PATHS.IAM_LOGIN, APP_PATHS.IAM_CONSENT, APP_PATHS.IAM_LOGOUT].some(path => + location.pathname.startsWith(path), + ) + ) { return null } diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 1b8bcb7af9..312afc674c 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -4,6 +4,8 @@ import { ArrowLeft, ExternalLink as LinkIconFeather, X } from 'react-feather' import { Link } from 'react-router-dom' import styled, { css, keyframes } from 'styled-components' +import { navigateToUrl, validateRedirectURL } from 'utils/redirect' + export const ButtonText = styled.button<{ color?: string; gap?: string }>` outline: none; border: none; @@ -200,7 +202,9 @@ export function ExternalLink({ }, [target, onClick], ) - return + return ( + + ) } export function ExternalLinkIcon({ @@ -217,14 +221,13 @@ export function ExternalLinkIcon({ console.debug('Fired outbound link event', href) } else { event.preventDefault() - - window.location.href = href + navigateToUrl(href, false) } }, [href, target], ) return ( - + ) diff --git a/src/utils/redirect.ts b/src/utils/redirect.ts new file mode 100644 index 0000000000..4dec7f623a --- /dev/null +++ b/src/utils/redirect.ts @@ -0,0 +1,72 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { useCallback } from 'react' +import { useNavigate } from 'react-router-dom' + +import { useActiveWeb3React } from 'hooks' +import { useChangeNetwork } from 'hooks/web3/useChangeNetwork' + +const whiteListDomains = [/https:\/\/(.+?\.)?kyberswap\.com$/, /https:\/\/(.+)\.kyberengineering\.io$/] +export const validateRedirectURL = (url: string | undefined, whitelistKyberSwap = true) => { + try { + if (!url) throw new Error() + const newUrl = new URL(url) // valid url + if ( + url.endsWith('.js') || + newUrl.pathname.endsWith('.js') || + !['https:', 'http:'].includes(newUrl.protocol) || + (whitelistKyberSwap && !whiteListDomains.some(regex => newUrl.origin.match(regex))) + ) { + throw new Error() + } + return url + } catch (error) { + return '' + } +} + +export const navigateToUrl = (url: string | undefined, whitelistKyberSwap = true) => { + const urlFormatted = validateRedirectURL(url, whitelistKyberSwap) + if (urlFormatted) window.location.href = urlFormatted +} + +/** + * this hook to navigate to specific url + * detect using window.open or navigate (react-router) + * check change chain if needed + */ +export const useNavigateToUrl = () => { + const navigate = useNavigate() + const { chainId: currentChain } = useActiveWeb3React() + const { changeNetwork } = useChangeNetwork() + + const redirect = useCallback( + (actionURL: string) => { + if (actionURL && actionURL.startsWith('/')) { + navigate(actionURL) + return + } + const { pathname, host, search } = new URL(actionURL) + if (!validateRedirectURL(actionURL, false)) return + if (window.location.host === host) { + navigate(`${pathname}${search}`) + } else { + window.open(actionURL) + } + }, + [navigate], + ) + + return useCallback( + (actionURL: string, chainId?: ChainId) => { + try { + if (!actionURL) return + if (chainId && chainId !== currentChain) { + changeNetwork(chainId, () => redirect(actionURL), undefined, true) + } else { + redirect(actionURL) + } + } catch (error) {} + }, + [changeNetwork, currentChain, redirect], + ) +} diff --git a/src/utils/string.ts b/src/utils/string.ts index 45292d30a8..3e0f7d6358 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -1,4 +1,5 @@ import { ChainId, Currency, Token } from '@kyberswap/ks-sdk-core' +import DOMPurify from 'dompurify' import { parse } from 'querystring' import { NETWORKS_INFO, SUPPORTED_NETWORKS } from 'constants/networks' @@ -34,7 +35,7 @@ export const shortString = (str: string | undefined, n: number) => { } export const escapeScriptHtml = (str: string) => { - return str.replace(/<.*?script.*?>.*?<\/.*?script.*?>/gim, '') + return DOMPurify.sanitize(str) } export const isEmailValid = (value: string | undefined) => diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index 1a2e35a0ed..51d866cbec 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -1,3 +1,5 @@ +import { ethers } from 'ethers' + import { GROUP_TRANSACTION_BY_TYPE, TRANSACTION_GROUP, @@ -22,3 +24,14 @@ export const getTransactionStatus = (transaction: TransactionDetails) => { error: !pending && transaction?.receipt?.status !== 1, } } + +// todo danh update limit order use this function +export const formatSignature = (rawSignature: string) => { + const bytes = ethers.utils.arrayify(rawSignature) + const lastByte = bytes[64] + if (lastByte === 0 || lastByte === 1) { + // to support hardware wallet https://ethereum.stackexchange.com/a/113727 + bytes[64] += 27 + } + return ethers.utils.hexlify(bytes) +} diff --git a/yarn.lock b/yarn.lock index 430039ae3b..a4e14a88d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4613,6 +4613,13 @@ dependencies: "@types/ms" "*" +"@types/dompurify@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.3.tgz#d34ba1cf4f8b8f2cbfe5d3118dc3b7d81858fa42" + integrity sha512-odiGr/9/qMqjcBOe5UhcNLOFHSYmKFOyr+bJ/Xu3Qp4k1pNPAlNLUVNNLcLfjQI7+W7ObX58EdD3H+3p3voOvA== + dependencies: + "@types/trusted-types" "*" + "@types/estree@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" @@ -5056,6 +5063,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.8.tgz#511fc1569cc32b0cf50941fe9f00bf70f94116bb" integrity sha512-7axfYN8SW9pWg78NgenHasSproWQee5rzyPVLC9HpaQSDgNArsnKJD88EaMfi4Pl48AyciO3agYCFqpHS1gLpg== +"@types/trusted-types@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65" + integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ== + "@types/trusted-types@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" @@ -8879,6 +8891,11 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +dompurify@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.6.tgz#925ebd576d54a9531b5d76f0a5bef32548351dae" + integrity sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w== + domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"