diff --git a/.eslintrc b/.eslintrc index d6d42ce..4cbd176 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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"] }], diff --git a/src/App.tsx b/src/App.tsx index 5bbc465..bf158a6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( @@ -76,7 +85,7 @@ const App: React.FC = () => { - + {/* @@ -127,7 +136,7 @@ const App: React.FC = () => { {/* Redirect */} - + {/* @@ -135,7 +144,7 @@ const App: React.FC = () => { - + */} {/* 404 */} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index e9e993b..7194980 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -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} /> ) diff --git a/src/config/constants/cacheKey.ts b/src/config/constants/cacheKey.ts new file mode 100644 index 0000000..1b23abc --- /dev/null +++ b/src/config/constants/cacheKey.ts @@ -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' diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 13c718e..b24b341 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -9,3 +9,4 @@ interface Window { bnbSign?: (address: string, message: string) => Promise<{ publicKey: string; signature: string }> } } +declare module 'axios' diff --git a/src/services/login.ts b/src/services/login.ts new file mode 100644 index 0000000..7f05b64 --- /dev/null +++ b/src/services/login.ts @@ -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, + }) +} diff --git a/src/services/user.ts b/src/services/user.ts new file mode 100644 index 0000000..868f00a --- /dev/null +++ b/src/services/user.ts @@ -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, + }) +} diff --git a/src/state/actions.ts b/src/state/actions.ts index 926d16f..968253b 100644 --- a/src/state/actions.ts +++ b/src/state/actions.ts @@ -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' diff --git a/src/state/index.ts b/src/state/index.ts index 6e35a4d..3800834 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -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, diff --git a/src/state/types.ts b/src/state/types.ts index 2842e84..49b1165 100644 --- a/src/state/types.ts +++ b/src/state/types.ts @@ -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 } diff --git a/src/state/userInfo/hooks.ts b/src/state/userInfo/hooks.ts new file mode 100644 index 0000000..a9bfa72 --- /dev/null +++ b/src/state/userInfo/hooks.ts @@ -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 +} diff --git a/src/state/userInfo/index.ts b/src/state/userInfo/index.ts new file mode 100644 index 0000000..b5277fa --- /dev/null +++ b/src/state/userInfo/index.ts @@ -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/fetchUserInfo', async () => { + const result = await queryUserInfo() + return result +}) + +// export const fetchUserInviteInfo = createAsyncThunk('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 diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..0f6a38e --- /dev/null +++ b/src/types/user.ts @@ -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 +} diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..b1db7c4 --- /dev/null +++ b/src/utils/request.ts @@ -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, 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