mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-04-24 03:00:09 -04:00
3box profiles (#9)
* Fix auth * Added 3box profile info * Added player names in list * Added hasura action to fetch verified accounts on 3Box * Added usernames * fix router
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import { ApolloProvider } from '@apollo/react-hooks';
|
||||
|
||||
import { CssBaseline } from '@material-ui/core';
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
|
||||
import { createApolloClient } from './apollo';
|
||||
|
||||
import { Home } from './containers/Home';
|
||||
import Web3ContextProvider from './contexts/Web3';
|
||||
|
||||
import Header from './components/Header';
|
||||
import Routes from './Routes';
|
||||
|
||||
const apolloClient = createApolloClient();
|
||||
|
||||
function App() {
|
||||
@@ -14,7 +17,10 @@ function App() {
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<Web3ContextProvider>
|
||||
<CssBaseline/>
|
||||
<Home/>
|
||||
<Router>
|
||||
<Header/>
|
||||
<Routes/>
|
||||
</Router>
|
||||
</Web3ContextProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
|
||||
19
packages/app-react/src/Routes.tsx
Normal file
19
packages/app-react/src/Routes.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route, Redirect } from "react-router-dom";
|
||||
|
||||
import { Home } from './containers/Home';
|
||||
import { Player } from './containers/Player';
|
||||
|
||||
export default function Routes() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route path="/player/:playerId">
|
||||
<Player />
|
||||
</Route>
|
||||
<Redirect to="/"/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,11 @@ export function loginLoading(client, loading = true) {
|
||||
|
||||
export async function login(client, token, ethAddress) {
|
||||
loginLoading(client);
|
||||
|
||||
client.writeData({
|
||||
data: {
|
||||
authToken: token,
|
||||
},
|
||||
});
|
||||
return client.query({
|
||||
query: queries.get_MyAccount,
|
||||
variables: { eth_address: ethAddress }
|
||||
@@ -36,14 +40,13 @@ export async function login(client, token, ethAddress) {
|
||||
client.writeData({
|
||||
data: {
|
||||
authState: 'logged',
|
||||
authToken: token,
|
||||
playerId: res.data.Account[0].Player.id,
|
||||
},
|
||||
});
|
||||
setTokenInStore(token);
|
||||
})
|
||||
.catch(async error => {
|
||||
logout();
|
||||
logout(client);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
15
packages/app-react/src/components/Header.tsx
Normal file
15
packages/app-react/src/components/Header.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
import {Login} from "../containers/Login";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<Box>
|
||||
<Link to={`/`}><button>Home</button></Link>
|
||||
<Login/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
export default function Player({ player }: { player: any }) {
|
||||
return (
|
||||
<Box>
|
||||
{player.id}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
90
packages/app-react/src/components/PlayerDetails.tsx
Normal file
90
packages/app-react/src/components/PlayerDetails.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {useMutation} from "@apollo/react-hooks";
|
||||
|
||||
import ThreeBox from '3box';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
import {useMyPlayer} from "../graphql/hooks";
|
||||
import {getPlayerETHAddress} from "../utils/players";
|
||||
import mutations from '../graphql/mutations';
|
||||
|
||||
function getProfilePicture(boxProfile: any) {
|
||||
const imageHash = boxProfile && boxProfile.image && boxProfile.image[0] && boxProfile.image[0].contentUrl && boxProfile.image[0].contentUrl['/'];
|
||||
if(imageHash) {
|
||||
return `https://ipfs.infura.io/ipfs/${imageHash}`;
|
||||
} else {
|
||||
return 'https://i.imgur.com/RXJO8FD.png';
|
||||
}
|
||||
}
|
||||
|
||||
export default function PlayerDetails({ player }: { player: any }) {
|
||||
const myPlayer = useMyPlayer();
|
||||
const isMyPlayer = myPlayer && myPlayer.id === player.id;
|
||||
const [boxProfile, setBoxProfile] = useState<any>();
|
||||
const [usernameInput, setUsernameInput] = useState<string>(player.username);
|
||||
const [updateBoxProfiles] = useMutation(mutations.UpdateBoxProfiles);
|
||||
const [updateUsername] = useMutation(mutations.UpdateUsername);
|
||||
|
||||
const ethAddress = getPlayerETHAddress(player);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
const boxProfile = await ThreeBox.getProfile(ethAddress);
|
||||
setBoxProfile(boxProfile);
|
||||
|
||||
})();
|
||||
}, [ethAddress]);
|
||||
|
||||
const goToEditBoxProfile = useCallback(() => {
|
||||
window.open(`https://3box.io/${ethAddress}/edit`)
|
||||
}, []);
|
||||
|
||||
const editUserName = useCallback(() => {
|
||||
// TODO Apollo does not updates caches as it expects that the mutation returns an object with id, but hasura returns { returning: [{id}] }
|
||||
updateUsername({
|
||||
variables: {
|
||||
username: usernameInput
|
||||
}
|
||||
}).then(res =>
|
||||
console.log('updated username', res.data)
|
||||
);
|
||||
}, [usernameInput]);
|
||||
|
||||
const updateAccounts = useCallback(() => {
|
||||
updateBoxProfiles().then(res =>
|
||||
console.log('updated verified profiles', res.data.updateBoxProfile.updatedProfiles)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<h3>{player.username}</h3>
|
||||
<h4>{player.id}</h4>
|
||||
{isMyPlayer && <button onClick={goToEditBoxProfile}>Edit profile</button>}
|
||||
{isMyPlayer && <div>
|
||||
<input value={usernameInput} onChange={e => setUsernameInput(e.target.value)} />
|
||||
<button onClick={editUserName}>Change username</button>
|
||||
</div>}
|
||||
{boxProfile ?
|
||||
<div>
|
||||
<p><b>Name:</b> {boxProfile.name}</p>
|
||||
<p><b>Description:</b> {boxProfile.description}</p>
|
||||
<img src={getProfilePicture(boxProfile)} width={100} alt="profile-image"/>
|
||||
</div>
|
||||
:
|
||||
<p>Loading box profile</p>
|
||||
}
|
||||
<div>
|
||||
<h4>External accounts</h4>
|
||||
<ul>
|
||||
{player.Accounts.map((account: any) =>
|
||||
<li key={account.type}><b>{account.type}</b>: {account.identifier}</li>
|
||||
)}
|
||||
</ul>
|
||||
{isMyPlayer && <button onClick={updateAccounts}>Update accounts</button>}
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
13
packages/app-react/src/components/PlayerListItem.tsx
Normal file
13
packages/app-react/src/components/PlayerListItem.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
export default function PlayerListItem({ player }: { player: any }) {
|
||||
return (
|
||||
<Box>
|
||||
{player.username}
|
||||
<Link to={`/player/${player.id}`}><button>View player</button></Link>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
|
||||
import PlayerList from './PlayerList';
|
||||
import { Login } from './Login';
|
||||
import { MyPlayer } from './MyPlayer';
|
||||
import {localQueries} from "../apollo";
|
||||
|
||||
export const Home: React.FC = () => {
|
||||
const { data, loading } = useQuery(localQueries.get_authState);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<PlayerList/>
|
||||
<Login/>
|
||||
{!loading && data?.authState === 'logged' && <MyPlayer/>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import React, {useContext} from 'react';
|
||||
import React, {useContext, useCallback} from 'react';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
import { Web3Context } from '../contexts/Web3';
|
||||
import {localQueries} from "../apollo";
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
export const Login: React.FC = () => {
|
||||
const { data, loading } = useQuery(localQueries.get_authState);
|
||||
|
||||
const { connectWeb3 } = useContext(Web3Context);
|
||||
const { connectWeb3, disconnect } = useContext(Web3Context);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
connectWeb3().catch(console.error);
|
||||
}, [connectWeb3]);
|
||||
|
||||
if(loading || data?.authState === 'loading') {
|
||||
return (
|
||||
@@ -18,13 +23,18 @@ export const Login: React.FC = () => {
|
||||
</Box>
|
||||
);
|
||||
} else if(data?.authState === 'logged') {
|
||||
const { playerId } = data;
|
||||
return (
|
||||
<Box>Connected</Box>
|
||||
<Box>
|
||||
Connected
|
||||
<Link to={`/player/${playerId}`}><button>View my player</button></Link>
|
||||
<button onClick={disconnect}>Logout</button>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box>
|
||||
Unknown state
|
||||
<button onClick={connect}>Connect</button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
import Player from '../components/Player';
|
||||
import { useMyPlayer } from '../graphql/hooks';
|
||||
|
||||
export const MyPlayer: React.FC = () => {
|
||||
const { data, called, loading, error } = useMyPlayer();
|
||||
|
||||
if(error) {
|
||||
return <div>error</div>
|
||||
}
|
||||
if(loading || !called) {
|
||||
return <div>loading</div>
|
||||
}
|
||||
|
||||
const myPlayer = data.Player[0];
|
||||
return (
|
||||
<Box>
|
||||
<h4>My player</h4>
|
||||
<Player player={myPlayer} />
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
|
||||
33
packages/app-react/src/containers/Player.tsx
Normal file
33
packages/app-react/src/containers/Player.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import {useQuery} from "@apollo/react-hooks";
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
import PlayerDetails from '../components/PlayerDetails';
|
||||
|
||||
import queries from "../graphql/queries";
|
||||
|
||||
export const Player: React.FC = ({}) => {
|
||||
const { playerId } = useParams();
|
||||
const { data, loading, error } = useQuery(queries.get_Player, {
|
||||
variables: {
|
||||
player_id: playerId,
|
||||
}
|
||||
});
|
||||
|
||||
if(error) {
|
||||
return <div>error</div>
|
||||
}
|
||||
if(loading) {
|
||||
return <div>loading</div>
|
||||
}
|
||||
|
||||
const myPlayer = data.Player[0];
|
||||
return (
|
||||
<Box>
|
||||
<h4>Player</h4>
|
||||
<PlayerDetails player={myPlayer} />
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -6,19 +6,23 @@ import {useQuery} from '@apollo/react-hooks';
|
||||
|
||||
import queries from '../graphql/queries';
|
||||
|
||||
import Player from '../components/Player';
|
||||
import PlayerListItem from '../components/PlayerListItem';
|
||||
|
||||
export default function PlayerList() {
|
||||
const { data, loading } = useQuery(queries.get_Player);
|
||||
const { data, loading, error } = useQuery(queries.get_Player);
|
||||
|
||||
if(error) {
|
||||
return <div>error: {error.message}</div>
|
||||
}
|
||||
if(loading) {
|
||||
return <div>loading</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<h4>Player list</h4>
|
||||
{data.Player.map((player: any) =>
|
||||
<Player key={player.id} player={player} />
|
||||
<PlayerListItem key={player.id} player={player} />
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -8,16 +8,18 @@ import {useApolloClient} from '@apollo/react-hooks';
|
||||
|
||||
import config from '../config';
|
||||
import { did } from '@the-game/utils';
|
||||
import {loginLoading, login, getTokenFromStore} from '../apollo/auth';
|
||||
import {loginLoading, login, logout, getTokenFromStore} from '../apollo/auth';
|
||||
|
||||
type Web3ContextType = {
|
||||
ethersProvider: Web3Provider | null,
|
||||
connectWeb3: () => void;
|
||||
connectWeb3: () => Promise<void>;
|
||||
disconnect: () => void,
|
||||
}
|
||||
|
||||
export const Web3Context = createContext<Web3ContextType>({
|
||||
ethersProvider: null,
|
||||
connectWeb3: () => {},
|
||||
connectWeb3: async () => {},
|
||||
disconnect: () => undefined,
|
||||
});
|
||||
|
||||
const providerOptions = {
|
||||
@@ -75,6 +77,11 @@ const Web3ContextProvider: React.FC = props => {
|
||||
}
|
||||
}, [apolloClient, connectDID]);
|
||||
|
||||
const disconnect = useCallback(async () => {
|
||||
web3Modal.clearCachedProvider();
|
||||
logout(apolloClient);
|
||||
}, [apolloClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if(web3Modal.cachedProvider) {
|
||||
connectWeb3().catch(console.error);
|
||||
@@ -82,7 +89,7 @@ const Web3ContextProvider: React.FC = props => {
|
||||
}, [connectWeb3]);
|
||||
|
||||
return (
|
||||
<Web3Context.Provider value={{ ethersProvider, connectWeb3 }}>
|
||||
<Web3Context.Provider value={{ ethersProvider, connectWeb3, disconnect }}>
|
||||
{props.children}
|
||||
</Web3Context.Provider>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,13 @@ const fragments: any = {};
|
||||
fragments.PlayerFragment = `
|
||||
fragment PlayerFragment on Player {
|
||||
id
|
||||
username
|
||||
}
|
||||
`;
|
||||
fragments.AccountFragment = `
|
||||
fragment AccountFragment on Account {
|
||||
identifier
|
||||
type
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import queries from './queries';
|
||||
|
||||
export function useMyPlayer() {
|
||||
const authStateQuery = useQuery(localQueries.get_authState);
|
||||
const [getMyPlayer, myPlayerQuery] = useLazyQuery(queries.get_MyPlayer);
|
||||
const [getMyPlayer, myPlayerQuery] = useLazyQuery(queries.get_Player);
|
||||
|
||||
const playerId = authStateQuery.data?.playerId;
|
||||
|
||||
@@ -21,5 +21,5 @@ export function useMyPlayer() {
|
||||
}
|
||||
}, [getMyPlayer, playerId]);
|
||||
|
||||
return myPlayerQuery;
|
||||
return myPlayerQuery.data?.Player[0];
|
||||
}
|
||||
|
||||
31
packages/app-react/src/graphql/mutations.ts
Normal file
31
packages/app-react/src/graphql/mutations.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
const mutations: any = {};
|
||||
|
||||
mutations.UpdateBoxProfiles = gql`
|
||||
mutation UpdateBoxProfiles {
|
||||
updateBoxProfile {
|
||||
success
|
||||
updatedProfiles
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
mutations.UpdateUsername = gql`
|
||||
mutation UpdateUsername($username: String!) {
|
||||
update_Player(
|
||||
where: {},
|
||||
_set: {
|
||||
username: $username
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default mutations;
|
||||
@@ -5,23 +5,18 @@ import fragments from './fragments';
|
||||
const queries: any = {};
|
||||
|
||||
queries.get_Player = gql`
|
||||
query GetPlayer {
|
||||
Player {
|
||||
...PlayerFragment
|
||||
}
|
||||
}
|
||||
${fragments.PlayerFragment}
|
||||
`;
|
||||
|
||||
queries.get_MyPlayer = gql`
|
||||
query GetPlayer($player_id: uuid) {
|
||||
Player(
|
||||
where: { id: { _eq: $player_id } }
|
||||
) {
|
||||
...PlayerFragment
|
||||
Accounts {
|
||||
...AccountFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.PlayerFragment}
|
||||
${fragments.AccountFragment}
|
||||
`;
|
||||
|
||||
queries.get_MyAccount = gql`
|
||||
|
||||
3
packages/app-react/src/utils/players.ts
Normal file
3
packages/app-react/src/utils/players.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function getPlayerETHAddress(player: any) {
|
||||
return player.Accounts.find((a: any) => a.type === "ETHEREUM").identifier;
|
||||
}
|
||||
Reference in New Issue
Block a user