diff --git a/src/constants/index.ts b/src/constants/index.ts
index c71fa4e63a..63347d9dc0 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -233,6 +233,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/pages/App.tsx b/src/pages/App.tsx
index 0d636745c7..30123dfd20 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'))
@@ -426,6 +430,9 @@ export default function App() {
} />
} />
+ } />
+ } />
+ } />
} />
diff --git a/src/pages/Oauth/Consent.tsx b/src/pages/Oauth/Consent.tsx
new file mode 100644
index 0000000000..af418b7292
--- /dev/null
+++ b/src/pages/Oauth/Consent.tsx
@@ -0,0 +1,27 @@
+import KyberOauth2 from '@kybernetwork/oauth2'
+import { useEffect } from 'react'
+
+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('resp consent', data)
+ })
+ .catch(err => {
+ console.debug('err consent', err)
+ })
+ }, [consent_challenge])
+
+ return
+}
+
+export default Page
diff --git a/src/pages/Oauth/Login.tsx b/src/pages/Oauth/Login.tsx
new file mode 100644
index 0000000000..405542416f
--- /dev/null
+++ b/src/pages/Oauth/Login.tsx
@@ -0,0 +1,173 @@
+import KyberOauth2, { LoginFlow, LoginMethod } from '@kybernetwork/oauth2'
+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 { Col, Container, KyberLogo, TextDesc } from 'pages/Oauth/styled'
+import getShortenAddress from 'utils/getShortenAddress'
+import { queryStringToObject } from 'utils/string'
+import { formatSignature } from 'utils/transaction'
+
+import AuthForm from './components/AuthForm'
+import { BUTTON_IDS } from './constants/index'
+import { createSignMessage, getSupportLoginMethods } from './utils'
+
+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 + ''
+}
+
+function Login() {
+ const { account: address, chainId } = useActiveWeb3React()
+ const { library: provider } = useWeb3React()
+
+ const [processingSignEth, setProcessingSign] = useState(false)
+ const [authFormConfig, setAuthFormConfig] = useState()
+ const [error, setError] = useState('')
+ const [autoLogin, setAutoLogin] = useState(false) // not waiting for click btn
+
+ 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 &&
+ address &&
+ wallet_address?.toLowerCase() !== address?.toLowerCase()
+
+ const connectingWallet = useRef(false)
+
+ const signInWithEth = useCallback(async () => {
+ try {
+ const siweConfig = authFormConfig?.oauth_client?.metadata?.siwe_config
+ if (isMismatchEthAddress || !siweConfig || connectingWallet.current || !provider || !address || !chainId) {
+ return
+ }
+ setProcessingSign(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,
+ chainId,
+ nonce: challenge,
+ issuedAt: issued_at,
+ ...siweConfig,
+ })
+
+ const signature = await provider.getSigner().signMessage(message)
+ const resp = await KyberOauth2.oauthUi.loginEthereum({
+ address,
+ signature: formatSignature(signature),
+ csrf,
+ chainId,
+ })
+
+ if (resp) {
+ connectingWallet.current = false
+ setProcessingSign(false)
+ }
+ } catch (error: any) {
+ if (!didUserReject(error)) {
+ setError(getErrorMsg(error))
+ }
+ console.error('signInWithEthereum err', error)
+ connectingWallet.current = false
+ setProcessingSign(false)
+ }
+ }, [address, 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 autoLogin = false
+ 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) {
+ autoLogin = true
+ }
+ }
+ // todo
+ if (loginMethods.includes(LoginMethod.ETH) && !isIncludeGoogle) {
+ setTimeout(() => document.getElementById(BUTTON_IDS.LOGIN_ETH)?.click(), 200)
+ }
+ setAutoLogin(autoLogin)
+ KyberOauth2.initialize({ clientId: client_id, mode: ENV_KEY })
+
+ if (autoLogin) setTimeout(() => document.getElementById(BUTTON_IDS.LOGIN_GOOGLE)?.click(), 200)
+ } 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(address)}. Please change your wallet address accordingly.
+
+ ) : (
+ address && (
+
+ To get started, please sign-in to verify your ownership of this wallet address {getShortenAddress(address)}
+
+ )
+ )
+
+ return (
+
+
+
+ {error ? (
+ {error}
+ ) : autoLogin ? (
+
+ Checking data ...
+
+ ) : isSignInEth && address ? (
+ 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..b131e543d8
--- /dev/null
+++ b/src/pages/Oauth/Logout.tsx
@@ -0,0 +1,27 @@
+import KyberOauth2 from '@kybernetwork/oauth2'
+import { useEffect } from 'react'
+
+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('logout resp', data)
+ })
+ .catch(err => {
+ console.debug('err logout', err)
+ })
+ }, [logout_challenge])
+
+ return
+}
+
+export default Logout
diff --git a/src/pages/Oauth/components/AuthForm/AuthFormField.tsx b/src/pages/Oauth/components/AuthForm/AuthFormField.tsx
new file mode 100644
index 0000000000..a4ab45b476
--- /dev/null
+++ b/src/pages/Oauth/components/AuthForm/AuthFormField.tsx
@@ -0,0 +1,28 @@
+import { LoginFlowUiNode } from '@kybernetwork/oauth2'
+import React from 'react'
+
+import { ButtonOutlined, ButtonPrimary } from 'components/Button'
+
+import { BUTTON_IDS } from '../../constants/index'
+
+interface AuthFormFieldProps extends React.InputHTMLAttributes {
+ field: LoginFlowUiNode
+ outline?: boolean
+}
+
+const AuthFormField: React.FC = ({ field, outline }) => {
+ const attributes = field.attributes
+ if (field.group === 'oidc') {
+ const props = {
+ height: '36px',
+ id: BUTTON_IDS.LOGIN_GOOGLE,
+ type: 'submit',
+ value: attributes.value,
+ name: attributes.name,
+ children: <>Sign-In with Google>,
+ }
+ return React.createElement(outline ? ButtonOutlined : ButtonPrimary, props)
+ }
+ return null
+}
+export default AuthFormField
diff --git a/src/pages/Oauth/components/AuthForm/AuthFormFieldMessage.tsx b/src/pages/Oauth/components/AuthForm/AuthFormFieldMessage.tsx
new file mode 100644
index 0000000000..40eeac866a
--- /dev/null
+++ b/src/pages/Oauth/components/AuthForm/AuthFormFieldMessage.tsx
@@ -0,0 +1,20 @@
+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
+
+ const messageList: JSX.Element[] = messages.map((value, index) => {
+ return (
+
+ {value.text}
+
+ )
+ })
+
+ return {messageList}
+}
+
+export default AuthFormFieldMessage
diff --git a/src/pages/Oauth/components/AuthForm/index.tsx b/src/pages/Oauth/components/AuthForm/index.tsx
new file mode 100644
index 0000000000..4198aa4478
--- /dev/null
+++ b/src/pages/Oauth/components/AuthForm/index.tsx
@@ -0,0 +1,124 @@
+import { LoginFlow, LoginFlowUiNode, LoginMethod } from '@kybernetwork/oauth2'
+import React from 'react'
+import { Flex, Text } from 'rebass'
+import styled from 'styled-components'
+
+import { ButtonOutlined, ButtonPrimary } from 'components/Button'
+import Wallet from 'components/Icons/Wallet'
+import Loader from 'components/Loader'
+import { useActiveWeb3React } from 'hooks'
+import useParsedQueryString from 'hooks/useParsedQueryString'
+import { useWalletModalToggle } from 'state/application/hooks'
+
+import { BUTTON_IDS } from '../../constants/index'
+import { getSupportLoginMethods, navigateToUrl } from '../../utils'
+import AuthFormField from './AuthFormField'
+import AuthFormFieldMessage from './AuthFormFieldMessage'
+
+const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 14px;
+`
+
+interface AuthFormProps extends React.FormHTMLAttributes {
+ formConfig: LoginFlow | undefined
+ autoLogin: boolean
+ processingSignEth: boolean
+ signInWithEth: () => void
+ disableEth: boolean
+}
+
+const Splash = () =>
+
+const AuthForm: React.FC = ({
+ children,
+ formConfig,
+ autoLogin,
+ signInWithEth,
+ processingSignEth,
+ disableEth,
+ ...otherProps
+}) => {
+ const { back_uri } = useParsedQueryString<{ back_uri: string }>()
+
+ const loginMethods = getSupportLoginMethods(formConfig)
+ const showEth = loginMethods.includes(LoginMethod.ETH) && !autoLogin
+ const hasGoogle = loginMethods.includes(LoginMethod.GOOGLE)
+ const { account } = useActiveWeb3React()
+ const toggleWalletModal = useWalletModalToggle()
+
+ const onClickEth = (e: React.MouseEvent) => {
+ e.preventDefault()
+ !account ? toggleWalletModal() : signInWithEth()
+ }
+
+ const renderBtnEth = () => (
+
+ {processingSignEth ? (
+ <>
+ Signing In
+ >
+ ) : (
+ <>
+
+ Sign-In with Wallet
+ >
+ )}
+
+ )
+
+ if (!formConfig) return null
+ const { ui } = formConfig
+
+ const showBtnCancel = !hasGoogle && back_uri && !processingSignEth
+ const hasBothEthAndGoogle = hasGoogle && showEth
+ return (
+
+ )
+}
+export default AuthForm
diff --git a/src/pages/Oauth/constants/index.ts b/src/pages/Oauth/constants/index.ts
new file mode 100644
index 0000000000..dade1a84e5
--- /dev/null
+++ b/src/pages/Oauth/constants/index.ts
@@ -0,0 +1,4 @@
+export const BUTTON_IDS = {
+ LOGIN_GOOGLE: 'btnLoginGoogle',
+ LOGIN_ETH: 'btnLoginEth',
+}
diff --git a/src/pages/Oauth/styled.tsx b/src/pages/Oauth/styled.tsx
new file mode 100644
index 0000000000..ab1a513232
--- /dev/null
+++ b/src/pages/Oauth/styled.tsx
@@ -0,0 +1,57 @@
+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 Col = 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};
+ ${({ theme }) => theme.mediaWidth.upToSmall`
+ font-size: 16px;
+ line-height: 20px;
+ `};
+`
+
+export const KyberLogo = () => {
+ const iseDark = useIsDarkMode()
+ return (
+
+ )
+}
+
+export function PageContainer({ msg }: { msg: string }) {
+ return (
+
+
+
+
+ {msg}
+
+
+
+ )
+}
diff --git a/src/pages/Oauth/utils.ts b/src/pages/Oauth/utils.ts
new file mode 100644
index 0000000000..00d67528c1
--- /dev/null
+++ b/src/pages/Oauth/utils.ts
@@ -0,0 +1,67 @@
+import { LoginFlow } from '@kybernetwork/oauth2'
+
+export const getSupportLoginMethods = (loginFlow: LoginFlow | undefined) => {
+ return loginFlow?.oauth_client?.metadata?.allowed_login_methods ?? []
+}
+
+const whiteListDomains = [/https:\/\/(.+?\.)?kyberswap.com/, /https:\/\/(.+)\.kyberengineering.io/]
+const isValidRedirectURL = (url: string | undefined) => {
+ try {
+ if (!url) return false
+ const newUrl = new URL(url) // valid url
+ if (
+ url.endsWith('.js') ||
+ newUrl.pathname.endsWith('.js') ||
+ !whiteListDomains.some(regex => newUrl.origin.match(regex))
+ ) {
+ return false
+ }
+ return newUrl.protocol === 'http:' || newUrl.protocol === 'https:'
+ } catch (error) {
+ return false
+ }
+}
+
+export const navigateToUrl = (url: string | undefined) => {
+ if (url && isValidRedirectURL(url)) window.location.href = url
+}
+
+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/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/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)
+}