feat: 添加登录功能

This commit is contained in:
gary 2022-04-13 23:46:20 +08:00
parent 42b382398e
commit 681a83e8c0
14 changed files with 285 additions and 12 deletions

View File

@ -17,6 +17,7 @@
"no-plusplus": 0,
"prefer-destructuring": ["warn", { "object": true, "array": false }],
"no-underscore-dangle": 0,
"camelcase":0,
// Start temporary rules
// These rules are here just to keep the lint error to 0 during the migration to the new rule set
// They need to be removed and fixed as soon as possible
@ -26,6 +27,7 @@
"@typescript-eslint/no-explicit-any": 0,
"radix": 0,
"import/no-extraneous-dependencies": 0,
"no-unused-expressions":0,
"jsx-a11y/media-has-caption": 0,
// Exchange
"no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state", "memo"] }],

View File

@ -1,10 +1,13 @@
import React, { lazy } from 'react'
import React, { lazy, useEffect } from 'react'
import { Router, Redirect, Route, Switch } from 'react-router-dom'
import { ResetCSS } from '@pancakeswap/uikit'
import { useDispatch } from 'react-redux'
import BigNumber from 'bignumber.js'
import useEagerConnect from 'hooks/useEagerConnect'
import { usePollCoreFarmData, useFetchProfile, usePollBlockNumber } from 'state/hooks'
import { DatePickerPortal } from 'components/DatePicker'
import { initAxios } from 'utils/request'
import useToast from 'hooks/useToast'
import GlobalStyle from './style/Global'
import Menu from './components/Menu'
import SuspenseWithChunkError from './components/SuspenseWithChunkError'
@ -56,6 +59,12 @@ const App: React.FC = () => {
useEagerConnect()
useFetchProfile()
usePollCoreFarmData()
const dispatch = useDispatch()
const toast = useToast()
useEffect(() => {
initAxios(dispatch, toast)
})
return (
<Router history={history}>
@ -76,7 +85,7 @@ const App: React.FC = () => {
<Route path="/pools">
<Pools />
</Route>
<Route path="/lottery">
{/* <Route path="/lottery">
<Lottery />
</Route>
<Route path="/ifo">
@ -127,7 +136,7 @@ const App: React.FC = () => {
<Route exact strict path="/remove/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
{/* Redirect */}
<Route path="/staking">
{/* <Route path="/staking">
<Redirect to="/pools" />
</Route>
<Route path="/syrup">
@ -135,7 +144,7 @@ const App: React.FC = () => {
</Route>
<Route path="/nft">
<Redirect to="/collectibles" />
</Route>
</Route> */}
{/* 404 */}
<Route component={NotFound} />

View File

@ -28,13 +28,6 @@ const Menu = (props) => {
setLang={setLanguage}
cakePriceUsd={cakePriceUsd.toNumber()}
links={config(t)}
profile={{
username: profile?.username,
image: profile?.nft ? `/images/nfts/${profile.nft?.images.sm}` : undefined,
profileLink: '/profile',
noProfileLink: '/profile',
showPip: !profile?.username,
}}
{...props}
/>
)

View File

@ -0,0 +1,4 @@
export const CACHE_TOKEN = 'token'
export const CACHE_USERIFNO = 'userInfo'
export const CACHE_ACCOUNT = 'account'
export const CACHE_INVITE_CODE = 'inviteCode'

View File

@ -9,3 +9,4 @@ interface Window {
bnbSign?: (address: string, message: string) => Promise<{ publicKey: string; signature: string }>
}
}
declare module 'axios'

17
src/services/login.ts Normal file
View File

@ -0,0 +1,17 @@
import request from 'utils/request'
export const login = (params) => {
return request.request({
url: '/v1/login',
method: 'get',
params,
})
}
export const queryLoginHash = (params) => {
return request.request({
url: '/v1/login/hash',
method: 'get',
params,
})
}

37
src/services/user.ts Normal file
View File

@ -0,0 +1,37 @@
import request from 'utils/request'
export const queryUserInfo = () => {
return request.request({
url: '/v1/user',
method: 'get',
})
}
export const queryUserAvatarList = () => {
return request.request({
url: '/v1/avatar/user/list',
method: 'get',
})
}
export const queryUserInviteList = () => {
return request.request({
url: '/v1/user/invite/top/list',
method: 'get',
})
}
export const updateUserAvater = (params) => {
return request.request({
url: '/v1/user/update/avatar',
method: 'get',
params,
})
}
export const updateUserInfo = (data) => {
return request.request({
url: '/v1/user/update',
method: 'post',
data,
})
}

View File

@ -10,6 +10,7 @@ export {
updateUserPendingReward,
updateUserStakedBalance,
} from './pools'
export { setUserInfo, clearUserInfo } from './userInfo'
export { profileFetchStart, profileFetchSucceeded, profileFetchFailed } from './profile'
export { fetchStart, teamFetchSucceeded, fetchFailed, teamsFetchSucceeded } from './teams'
export { setBlock } from './block'

View File

@ -10,6 +10,7 @@ import blockReducer from './block'
import collectiblesReducer from './collectibles'
import votingReducer from './voting'
import lotteryReducer from './lottery'
import userInfo from './userInfo'
import application from './application/reducer'
import { updateVersion } from './global/actions'
@ -34,7 +35,7 @@ const store = configureStore({
collectibles: collectiblesReducer,
voting: votingReducer,
lottery: lotteryReducer,
userInfo,
// Exchange
application,
user,

View File

@ -466,6 +466,28 @@ export interface UserRound {
export type UserTicketsResponse = [ethers.BigNumber[], number[], boolean[]]
export interface UserInfo {
id?: number
avatar?: string
name?: string
address?: string
description?: string
invite_num?: string
invite_link?: string
email?: string
twitter?: string
facebook?: string
invite_code?: string
scoring_account?: number
invite_reward?: number
inviter_address?: string
}
export interface UserInfoState {
userInfo: UserInfo
token?: string
account?: string
}
// Global state
export interface State {
@ -478,5 +500,6 @@ export interface State {
teams: TeamsState
collectibles: CollectiblesState
voting: VotingState
userInfo: UserInfoState
lottery: LotteryState
}

View File

@ -0,0 +1,59 @@
import { useSelector, useDispatch } from 'react-redux'
import { useWeb3React } from '@web3-react/core'
import { queryLoginHash, login as userLogin } from 'services/login'
import useWeb3Provider from 'hooks/useActiveWeb3React'
import { signMessage } from 'utils/web3React'
import { setUserInfo } from 'state/actions'
import { CACHE_INVITE_CODE } from 'config/constants/cacheKey'
import useToast from 'hooks/useToast'
import { State } from '../types'
export const useAccount = (ellipsis?: boolean) => {
const account = useSelector((state: State) => state.userInfo.account)
if (ellipsis) {
return account ? `${account.substring(0, 4)}...${account.substring(account.length - 4)}` : null
}
return account
}
// 钱包登录 但是未签名获取到token
export const useUnactiveAccount = () => {
const { account } = useWeb3React()
return account
}
export const useToken = () => {
return useSelector((state: State) => state.userInfo.token)
}
export const useUserInfo = () => {
return useSelector((state: State) => state.userInfo.userInfo)
}
export const useUserPortrait = () => {
const userInfo = useSelector((state: State) => state.userInfo.userInfo)
return userInfo?.avatar || '/images/common/portrait.png'
}
// 签名登录
export const useSignLogin = () => {
const unActiveAccount = useUnactiveAccount()
const { library } = useWeb3Provider()
const dispatch = useDispatch()
const { toastError } = useToast()
const signLogin = async () => {
try {
const data = await queryLoginHash({ address: unActiveAccount })
const signHash = await signMessage(library, unActiveAccount, data.hash)
const inviteCode = sessionStorage.getItem(CACHE_INVITE_CODE)
const userInfo = await userLogin({
hash: data.hash,
signature: signHash,
invite_code: inviteCode,
})
dispatch(setUserInfo({ ...userInfo.user_info, token: userInfo.token }))
} catch (e) {
console.log(e)
// e.message&&toastError(e)
}
}
return signLogin
}

View File

@ -0,0 +1,67 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { queryUserInfo } from 'services/user'
import { CACHE_TOKEN, CACHE_ACCOUNT, CACHE_USERIFNO } from 'config/constants/cacheKey'
import multicall from 'utils/multicall'
// import inviteAbi from 'config/abi/invite.json'
// import { getInviteAddress } from 'utils/addressHelpers'
import { getBalanceNumber } from 'utils/formatBalance'
import { UserInfoState, UserInfo } from '../types'
const initialState: UserInfoState = {
userInfo: {},
token: localStorage.getItem(CACHE_TOKEN),
account: localStorage.getItem(CACHE_ACCOUNT),
}
export const fetchUserInfo = createAsyncThunk<UserInfo, null>('userInfo/fetchUserInfo', async () => {
const result = await queryUserInfo()
return result
})
// export const fetchUserInviteInfo = createAsyncThunk<number, string>('userInfo/fetchUserInviteInfo', async (account) => {
// const [canWithdrawReward] = await multicall(inviteAbi, [
// {
// address: getInviteAddress(),
// name: 'canGetRewardMap',
// params: [account],
// },
// ])
// return getBalanceNumber(canWithdrawReward)
// })
export const userInfoSlice = createSlice({
name: 'userInfo',
initialState,
reducers: {
setUserInfo: (state, action) => {
const info = action.payload
state.userInfo = info
state.account = info.address
info.token && (state.token = info.token)
localStorage.setItem(CACHE_TOKEN, state.token)
localStorage.setItem(CACHE_USERIFNO, JSON.stringify(info))
localStorage.setItem(CACHE_ACCOUNT, info.address)
},
clearUserInfo: (state) => {
state.userInfo = {}
state.account = ''
state.token = ''
localStorage.removeItem(CACHE_TOKEN)
localStorage.removeItem(CACHE_USERIFNO)
localStorage.removeItem(CACHE_ACCOUNT)
},
},
extraReducers: (builder) => {
builder.addCase(fetchUserInfo.fulfilled, (state, action) => {
state.userInfo = action.payload
localStorage.setItem(CACHE_USERIFNO, JSON.stringify(state.userInfo))
})
// .addCase(fetchUserInviteInfo.fulfilled, (state, action) => {
// state.userInfo = { ...state.userInfo, invite_reward: action.payload }
// })
},
})
// Actions
export const { setUserInfo, clearUserInfo } = userInfoSlice.actions
export default userInfoSlice.reducer

12
src/types/user.ts Normal file
View File

@ -0,0 +1,12 @@
export interface ProtraitDetailType {
id?: string
url?: string
}
export interface InviteDetailType {
id: number
created_at: string
name: string
avatar: string
invite_num: number
}

47
src/utils/request.ts Normal file
View File

@ -0,0 +1,47 @@
import { Dispatch } from 'react'
import axios from 'axios'
// import { clearUserInfo } from 'state/actions'
// create an axios instance
const request = axios.create({
baseURL: process.env.REACT_APP_REQUEST_URL,
timeout: 10000, // request timeout
})
let hasInit = false
export const initAxios = (dispatch: Dispatch<any>, toast) => {
if (hasInit) return
hasInit = true
// request interceptor
request.interceptors.request.use(
(memo: any) => {
// do something before request is sent
memo.headers.token = localStorage.getItem('token')
return memo
},
(error) => {
return Promise.reject(error)
},
)
// response interceptor
request.interceptors.response.use(
(response) => {
const res: any = response.data
if (res.code !== 200) {
if (res.code === 401) {
// dispatch(clearUserInfo())
toast.toastError('Login expiration')
return Promise.reject(new Error('Login expiration'))
}
toast.toastError(res.msg)
return Promise.reject(res.msg || 'Error')
}
return res.data
},
(error) => {
return Promise.reject(error)
},
)
}
export default request