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:
Pacien Boisson
2020-05-13 10:02:21 +02:00
committed by GitHub
parent 7747ff69d0
commit 214f3f65c2
37 changed files with 9547 additions and 331 deletions

View File

@@ -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>
);

View 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>
);
}

View File

@@ -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;
});
}

View 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>
)
}

View File

@@ -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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -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>
);
};

View File

@@ -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>
)
}

View File

@@ -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>
)
};

View 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>
)
};

View File

@@ -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>
)

View File

@@ -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>
);

View File

@@ -3,11 +3,13 @@ const fragments: any = {};
fragments.PlayerFragment = `
fragment PlayerFragment on Player {
id
username
}
`;
fragments.AccountFragment = `
fragment AccountFragment on Account {
identifier
type
}
`;

View File

@@ -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];
}

View 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;

View File

@@ -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`

View File

@@ -0,0 +1,3 @@
export function getPlayerETHAddress(player: any) {
return player.Accounts.find((a: any) => a.type === "ETHEREUM").identifier;
}