Skip to content

Commit

Permalink
feat(app): use context API for user state
Browse files Browse the repository at this point in the history
+ add @web3-onboard dependencies
  • Loading branch information
david-focused committed Apr 21, 2022
1 parent eb9b1e3 commit 8de483b
Show file tree
Hide file tree
Showing 11 changed files with 2,338 additions and 58 deletions.
1 change: 1 addition & 0 deletions app/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/*.ts
/test/*
/dist/*
/.next/*
/coverage/*
/node_modules/*
/__mocks__/**/*.js
2 changes: 1 addition & 1 deletion app/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/dist/*
/build/*
/coverage/*

/.next/*
22 changes: 22 additions & 0 deletions app/config/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PROVIDER_ID } from "@dpopp/types";

export type ProviderSpec = {
// icon: ??? // TODO
name: string;
description: string;
};

export type Providers = {
[provider in PROVIDER_ID]: ProviderSpec;
};

export const STAMP_PROVIDERS: Readonly<Providers> = {
Google: {
name: "Google",
description: "Google Authentication",
},
Simple: {
name: "Simple",
description: "Simple Username",
},
};
284 changes: 284 additions & 0 deletions app/context/userContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/* eslint-disable react-hooks/exhaustive-deps */
// --- React Methods
import React, { createContext, useMemo, useState, useEffect } from "react";
import { useConnectWallet, useWallets } from "@web3-onboard/react";

// --- Wallet connection utilities
import { initWeb3Onboard } from "../utils/onboard";
import { Passport, Stamp, PROVIDER_ID } from "@dpopp/types";

// --- Data Storage Functions
import { OnboardAPI, WalletState } from "@web3-onboard/core/dist/types";
import { JsonRpcSigner, Web3Provider } from "@ethersproject/providers";

// --- Data Storage Functions
import { LocalStorageDatabase } from "../services/databaseStorage";
import { ProviderSpec, STAMP_PROVIDERS } from "../config/providers";

export type AllProvidersState = {
[provider in PROVIDER_ID]: {
providerSpec: ProviderSpec;
stamp?: Stamp;
};
};

const startingAllProvidersState: AllProvidersState = {
Google: {
providerSpec: STAMP_PROVIDERS.Google,
stamp: undefined,
},
Simple: {
providerSpec: STAMP_PROVIDERS.Simple,
stamp: undefined,
},
};

export interface UserContextState {
loggedIn: boolean;
passport: Passport | undefined;
allProvidersState: AllProvidersState;
getStampIndex: (stamp: Stamp) => number | undefined;
hasStamp: (provider: string) => boolean;
handleCreatePassport: () => void;
handleSaveStamp: (stamp: Stamp) => void;
handleConnection: () => void;
handleAddStamp: (stamp: Stamp) => void;
address: string | undefined;
connectedWallets: WalletState[];
signer: JsonRpcSigner | undefined;
walletLabel: string | undefined;
}
const startingState: UserContextState = {
loggedIn: false,
passport: undefined,
allProvidersState: startingAllProvidersState,
getStampIndex: () => undefined,
hasStamp: () => false,
handleCreatePassport: () => {},
handleSaveStamp: () => {},
handleConnection: () => {},
handleAddStamp: () => {},
address: undefined,
connectedWallets: [],
signer: undefined,
walletLabel: undefined,
};

// create our app context
export const UserContext = createContext(startingState);

// export function App({ children, ...props }: AppProps): JSX.Element {

export const UserContextProvider = ({ children }: { children: any }) => {
const [loggedIn, setLoggedIn] = useState(false);
const [passport, setPassport] = useState<Passport | undefined>(undefined);
const [localStorageDatabase, setLocalStorageDatabase] = useState<
LocalStorageDatabase | undefined
>(undefined);
const [allProvidersState, setAllProviderState] = useState(
startingAllProvidersState
);

// Use onboard to control the current provider/wallets
const [{ wallet }, connect, disconnect] = useConnectWallet();
const connectedWallets = useWallets();
const [web3Onboard, setWeb3Onboard] = useState<OnboardAPI | undefined>();
const [walletLabel, setWalletLabel] = useState<string | undefined>();
const [address, setAddress] = useState<string>();
const [signer, setSigner] = useState<JsonRpcSigner | undefined>();

// Init onboard to enable hooks
useEffect((): void => {
setWeb3Onboard(initWeb3Onboard);
}, []);

const setWalletFromLocalStorage = async (): Promise<void> => {
const previouslyConnectedWallets = JSON.parse(
// retrieve localstorage state
window.localStorage.getItem("connectedWallets") || "[]"
) as string[];
if (previouslyConnectedWallets?.length) {
connect({
autoSelect: {
label: previouslyConnectedWallets[0],
disableModals: true,
},
}).catch((e): void => {
throw e;
});
}
};

// Connect wallet on reload
useEffect((): void => {
setWalletFromLocalStorage();
}, []);

// Update on wallet connect
useEffect((): void => {
// no connection
if (!connectedWallets.length) {
setWalletLabel(undefined);
setAddress(undefined);
setSigner(undefined);
} else {
// record connected wallet details
setWalletLabel(wallet?.label);
setAddress(wallet?.accounts[0].address);
// get the signer from an ethers wrapped Web3Provider
setSigner(new Web3Provider(connectedWallets[0]?.provider).getSigner());
// flaten array for storage
const connectedWalletsLabelArray = connectedWallets.map(
({ label }) => label
);
// store in localstorage
window.localStorage.setItem(
"connectedWallets",
JSON.stringify(connectedWalletsLabelArray)
);

if (address) {
// Load localStorage Passport data
const localStorageInstance = new LocalStorageDatabase(address);
setLocalStorageDatabase(localStorageInstance);
const loadedPassport = localStorageInstance?.getPassport(
localStorageInstance.passportKey
);
setPassport(loadedPassport);
}
}
}, [connectedWallets, wallet]);

// Toggle connect/disconnect
// clear context passport on disconnect
const handleConnection = (): void => {
if (!address) {
connect({})
.then(() => {
setLoggedIn(true);
})
.catch((e) => {
throw e;
});
} else {
disconnect({
label: walletLabel || "",
})
.then(() => {
window.localStorage.setItem("connectedWallets", "[]");
setPassport(undefined);
setLoggedIn(false);
})
.catch((e) => {
throw e;
});
}
};

// hydrate allProvidersState
useEffect(() => {
passport?.stamps.forEach((stamp: Stamp) => {
const { provider } = stamp;
const providerState = allProvidersState[provider];
const newProviderState = {
providerSpec: providerState.providerSpec,
stamp,
};
setAllProviderState((prevState) => ({
...prevState,
[provider]: newProviderState,
}));
});
// TODO remove providerstate on stamp removal
}, [passport]);

const handleCreatePassport = (): void => {
if (localStorageDatabase) {
const passportDid = localStorageDatabase.createPassport();
const getPassport = localStorageDatabase.getPassport(passportDid);
setPassport(getPassport);
}
};

const handleAddStamp = (stamp: Stamp): void => {
if (localStorageDatabase) {
localStorageDatabase.addStamp(localStorageDatabase.passportKey, stamp);
const getPassport = localStorageDatabase.getPassport(
localStorageDatabase.passportKey
);
setPassport(getPassport);
}
};

const handleSaveStamp = (stamp: Stamp): void => {
if (passport) {
// check if there is already a stamp recorded for this provider
const stampIndex = getStampIndex(stamp);
// place the new stamp into the stamps array
if (stampIndex !== undefined && stampIndex !== -1) {
passport.stamps[stampIndex] = stamp;
} else {
passport.stamps.push(stamp);
}
// propagate the new passport state
setPassport({ ...passport });
}
};

const getStampIndex = (stamp: Stamp): number | undefined => {
// check if there is already a stamp recorded for this provider
return passport?.stamps.findIndex(
(_stamp: Stamp) => _stamp.provider === stamp.provider
);
};

const hasStamp = (provider: string): boolean => {
// check if a stamp exists for a given provider
return (
!!passport?.stamps &&
getStampIndex({ provider } as unknown as Stamp) !== -1
);
};

const stateMemo = useMemo(
() => ({
loggedIn,
address,
passport,
allProvidersState,
handleCreatePassport,
handleSaveStamp,
handleConnection,
getStampIndex,
hasStamp,
handleAddStamp,
connectedWallets,
signer,
walletLabel,
}),
[loggedIn, address, passport, signer, connectedWallets, allProvidersState]
);

// use props as a way to pass configuration values
const providerProps = {
loggedIn,
address,
passport,
allProvidersState,
handleCreatePassport,
handleSaveStamp,
handleConnection,
getStampIndex,
hasStamp,
handleAddStamp,
connectedWallets,
signer,
walletLabel,
};

return (
<UserContext.Provider value={providerProps}>
{children}
</UserContext.Provider>
);
};
6 changes: 6 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
"@chakra-ui/react": "^1.8.8",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@web3-onboard/core": "^2.2.5",
"@web3-onboard/injected-wallets": "^2.0.5",
"@web3-onboard/ledger": "^2.0.3",
"@web3-onboard/react": "^2.1.5",
"@web3-onboard/walletconnect": "^2.0.1",
"@web3-onboard/walletlink": "^2.0.1",
"framer-motion": "^6.3.0",
"next": "latest",
"react": "^17.0.2",
Expand Down
15 changes: 10 additions & 5 deletions app/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { AppProps } from "next/app";
import "../styles/globals.css";
import { ChakraProvider } from "@chakra-ui/react";
import { UserContextProvider } from "../context/userContext";

function MyApp({ Component, pageProps }: AppProps) {
return (
// <UserContext.Provider value={stateMemo}>
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
// </UserContext.Provider>
<UserContextProvider>
<ChakraProvider>
<div className="font-librefranklin font-miriam-libre min-h-default min-h-max bg-violet-700 text-gray-100">
<div className="container mx-auto px-5 py-2">
<Component {...pageProps} />
</div>
</div>
</ChakraProvider>
</UserContextProvider>
);
}

Expand Down
Loading

0 comments on commit 8de483b

Please sign in to comment.