Skip to content

Commit

Permalink
feat(Thorium Account): Add support for signing into the Thorium Accou…
Browse files Browse the repository at this point in the history
…nt within Thorium Nova. (#106)
  • Loading branch information
alexanderson1993 authored Nov 17, 2021
1 parent f0b0712 commit 8b5b58a
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 31 deletions.
28 changes: 3 additions & 25 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {FlightLobby} from "./components/FlightLobby";
import {FaCamera} from "react-icons/fa";
import Button from "@thorium/ui/Button";
import {netSend} from "./context/netSend";
import LoginButton from "./components/LoginButton";

const DocLayout = lazy(() => import("./docs"));
const Config = lazy(() => import("./pages/Config"));
Expand All @@ -20,33 +21,10 @@ const MainPage = () => {
<div className="welcome h-full p-12 grid grid-cols-2 grid-rows-2">
<WelcomeLogo />
<Credits className="row-start-2 col-start-2" />
{/* <div>
<Button
onClick={() =>
netSend("flightStart", {flightName: "Test", plugins: []})
}
className="btn"
>
Start Flight
</Button>
<Button
onClick={() => netSend("flightResume")}
className="btn btn-success"
>
Resume Flight
</Button>
<Button
onClick={() => netSend("dotCreate")}
className="btn btn-success"
>
Add Dot
</Button>
</div>
<CardProvider cardName="clients">
<CardData />
</CardProvider> */}

<WelcomeButtons className="col-start-1 row-start-2" />
<QuoteOfTheDay />
<LoginButton />
</div>
);
};
Expand Down
108 changes: 108 additions & 0 deletions client/src/components/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Button from "@thorium/ui/Button";
import {useEffect, useRef} from "react";
import {useThoriumAccount} from "../context/ThoriumAccountContext";

// https://stackoverflow.com/a/16861050/4697675
const popupCenter = ({
url,
title,
w,
h,
}: {
url: string;
title: string;
w: number;
h: number;
}) => {
// Fixes dual-screen position Most browsers Firefox
const dualScreenLeft =
window.screenLeft !== undefined ? window.screenLeft : window.screenX;
const dualScreenTop =
window.screenTop !== undefined ? window.screenTop : window.screenY;

const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: window.screen.width;
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: window.screen.height;

const systemZoom = width / window.screen.availWidth;
const left = (width - w) / 2 / systemZoom + dualScreenLeft;
const top = (height - h) / 2 / systemZoom + dualScreenTop;
const newWindow = window.open(
url,
title,
`
scrollbars=yes,
width=${w / systemZoom},
height=${h / systemZoom},
top=${top},
left=${left}
`
);

newWindow?.focus();
return newWindow;
};

export default function LoginButton() {
const {login, logout, account, verificationUrl, verifying} =
useThoriumAccount();
const linkRef = useRef<HTMLAnchorElement>(null);
const windowRef = useRef<Window | null>(null);
useEffect(() => {
if (verificationUrl) {
if (!windowRef.current) {
windowRef.current = popupCenter({
url: verificationUrl,
title: "Verify your account",
w: 500,
h: 500,
});
}
// linkRef.current?.click();
}
}, [verificationUrl]);
useEffect(() => {
if (windowRef.current) {
windowRef.current.close();
windowRef.current = null;
}
}, [account]);
return (
<div className="flex self-start place-self-end items-center">
<a
ref={linkRef}
href={verificationUrl}
target="thorium-account"
className="opacity-0"
>
{" "}
</a>
<Button
className={`w-max btn-ghost btn-sm ${verifying ? "loading" : ""}`}
onClick={() => {
if (account) {
logout();
} else {
login();
}
}}
>
{verifying ? "Verifying..." : account ? "Logout" : "Login to Thorium"}
</Button>
{account && (
<img
className="avatar w-10 h-10 rounded-full border border-gray-500"
src={account.profilePictureUrl}
alt={account.displayName}
/>
)}
</div>
);
}
150 changes: 150 additions & 0 deletions client/src/context/ThoriumAccountContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {useLocalStorage} from "../hooks/useLocalStorage";

type ThoriumAccountContextProps = {
account: ThoriumAccount;
login: () => void;
logout: () => void;
userCode?: string;
verificationUrl?: string;
verifying: boolean;
};
interface ThoriumAccount {
id: number;
displayName: string;
profilePictureUrl: string;
githubConnection: boolean;
access_token: string;
}
const ThoriumAccountContext = createContext<ThoriumAccountContextProps>(null!);

export function ThoriumAccountContextProvider({
children,
}: {
children: ReactNode;
}) {
const [account, setAccount] = useLocalStorage<ThoriumAccount | null>(
"thorium_account",
null
);
const [verifying, setVerifying] = useState(false);
const [deviceCode, setDeviceCode] = useState<{
device_code: string;
user_code: string;
expires_in: number;
interval: number;
verification_uri: string;
} | null>(null);

const value = useMemo(() => {
function logout() {
setDeviceCode(null);
setAccount(null);
}
async function login() {
setVerifying(true);
// Kick off the login process
const data = await fetch(
`${process.env.THORIUMSIM_URL}/oauth/device_request`,
{
method: "POST",
body: JSON.stringify({
client_id: process.env.THORIUMSIM_CLIENT_ID,
scope: "identity github:issues",
}),
headers: {
"Content-Type": "application/json",
},
}
).then(res => res.json());
if (data.error) {
setVerifying(false);
throw new Error(data.error_description);
}

setDeviceCode(data);
}
return {
account,
login,
logout,
userCode: deviceCode?.user_code,
verificationUrl: deviceCode?.verification_uri,
verifying,
};
}, [account, setAccount, deviceCode, verifying]);

useEffect(() => {
if (deviceCode) {
const interval = setInterval(async () => {
const data = await fetch(
`${process.env.THORIUMSIM_URL}/oauth/access_token`,
{
method: "POST",
body: JSON.stringify({
client_id: process.env.THORIUMSIM_CLIENT_ID,
device_code: deviceCode.device_code,
grant_type: "device_code",
}),
headers: {
"Content-Type": "application/json",
},
}
).then(res => res.json());

if (data.error) {
if (data.error === "authorization_pending") return;
if (data.error === "slow_down") {
setDeviceCode({...deviceCode, interval: deviceCode.interval * 2});
return;
}
if (data.error === "expired_token") {
setDeviceCode(null);
value.login();
clearInterval(interval);
return;
}
setVerifying(false);
setDeviceCode(null);
}

if (data.access_token) {
setVerifying(false);
clearInterval(interval);
const user = await fetch(
`${process.env.THORIUMSIM_URL}/api/identity`,
{
headers: {
Authorization: `Bearer ${data.access_token}`,
},
credentials: "omit",
}
).then(res => res.json());
setAccount({...data, ...user});
setDeviceCode(null);
}
}, deviceCode.interval * 1000);

return () => clearInterval(interval);
}
}, [deviceCode, setAccount, value]);

return (
<ThoriumAccountContext.Provider value={value}>
{children}
</ThoriumAccountContext.Provider>
);
}

export function useThoriumAccount() {
return useContext(ThoriumAccountContext);
}
8 changes: 2 additions & 6 deletions client/src/context/ThoriumContext.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import {ClientChannel} from "@geckos.io/client";
import {createContext, ReactNode, useContext, useEffect, useMemo} from "react";
import type {
AllInputNames,
AllInputParams,
AllInputReturns,
} from "@thorium/inputs";
import {useDataConnection} from "../hooks/useDataConnection";
import {FaSpinner} from "react-icons/fa";
import {SnapshotInterpolation, Types} from "@geckos.io/snapshot-interpolation";
import {decode} from "@msgpack/msgpack";
import {ClientSocket} from "../utils/clientSocket";
import Button from "@thorium/ui/Button";
import {ThoriumAccountContextProvider} from "./ThoriumAccountContext";
const serverFPS = 3;

const ThoriumContext = createContext<IThoriumContext | null>(null);
Expand Down Expand Up @@ -79,7 +75,7 @@ export function ThoriumProvider({children}: {children: ReactNode}) {

return (
<ThoriumContext.Provider value={value}>
{children}
<ThoriumAccountContextProvider>{children}</ThoriumAccountContextProvider>
{reconnectionState === "reconnecting" && <Reconnecting />}
{reconnectionState === "disconnected" && <Disconnected />}
</ThoriumContext.Provider>
Expand Down
37 changes: 37 additions & 0 deletions client/src/hooks/useLocalStorage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {useReducer, useEffect, useCallback, Reducer} from "react";

export function useLocalStorageReducer<R extends Reducer<I, any>, I>(
reducer: R,
defaultState: I,
storageKey: string
) {
const init = useCallback(() => {
let preloadedState;
try {
preloadedState = JSON.parse(
window.localStorage.getItem(storageKey) || ""
);
// validate preloadedState if necessary
} catch (e) {
// ignore
}
return preloadedState || defaultState;
}, [storageKey, defaultState]);

const hookVars = useReducer(reducer, null, init);

const hookyHook = hookVars[0];
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(hookyHook));
}, [storageKey, hookyHook]);

return hookVars;
}

export function useLocalStorage<T>(storageKey: string, defaultValue: T) {
return useLocalStorageReducer(
(state, action) => action,
defaultValue,
storageKey
);
}
Loading

0 comments on commit 8b5b58a

Please sign in to comment.