feat: migrated app store

This commit is contained in:
TrèsAbhi committed 2024-07-09 20:05:34 -05:00
1 parent 5aa4b262f8
commit cf841c9bb2
14 files changed
+156 -191

No files matched your search

+2 -1
View File
@@ -4,7 +4,7 @@ import { Flex, Heading, Text } from '@radix-ui/themes';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { mutateApp } from '../../../stores/app';
import * as App from '../../../stores/app';
export interface PatreonAuthResponse {
access_token: string;
@@ -23,6 +23,7 @@ export default function Page({
searchParams: { [key: string]: string };
}) {
const router = useRouter();
const mutateApp = App.useMutation();
useEffect(() => {
switch (params.provider) {
+62 -14
View File
@@ -10,13 +10,21 @@ import {
} from '@radix-ui/themes';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { extendAuthPatreon } from '../../core/blitz/extendAuthPatreon';
import { extendAuthWargaming } from '../../core/blitz/extendAuthWargaming';
import { logoutPatreon } from '../../core/blitz/logoutPatreon';
import { logoutWargaming } from '../../core/blitz/logoutWargaming';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { idToRegion } from '../../core/blitz/idToRegion';
import isDev from '../../core/blitzkit/isDev';
import { isLocalhost } from '../../core/blitzkit/isLocalhost';
import { CURRENT_POLICIES_AGREEMENT_INDEX, useApp } from '../../stores/app';
import * as App from '../../stores/app';
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../../stores/app/constants';
import { PatreonAuthResponse } from '../auth/[provider]/page';
interface Extension {
data: {
access_token: string;
account_id: number;
expires_at: number;
};
}
const DEV_BUILD_AGREEMENT_COOLDOWN = 8 * 24 * 60 * 60 * 1000;
@@ -27,8 +35,10 @@ const DEV_BUILD_AGREEMENT_COOLDOWN = 8 * 24 * 60 * 60 * 1000;
export function Checks() {
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
const logins = useApp((state) => state.logins);
const policiesAgreementIndex = useApp(
const mutateApp = App.useMutation();
const logins = App.use((state) => state.logins);
const appStore = App.useStore();
const policiesAgreementIndex = App.use(
(state) => state.policiesAgreementIndex,
);
const pathname = usePathname();
@@ -39,7 +49,7 @@ export function Checks() {
setShowDevBuildAlert(
isDev() &&
!isLocalhost() &&
Date.now() - useApp.getState().devBuildAgreementTime >=
Date.now() - appStore.getState().devBuildAgreementTime >=
DEV_BUILD_AGREEMENT_COOLDOWN,
);
}, []);
@@ -55,9 +65,31 @@ export function Checks() {
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
if (expiresInDays < 0) {
logoutWargaming();
mutateApp((draft) => {
draft.logins.wargaming = undefined;
});
} else if (expiresInDays < 7) {
extendAuthWargaming();
const { wargaming } = appStore.getState().logins;
if (!wargaming) return;
fetch(
`https://api.worldoftanks.${idToRegion(
wargaming.id,
)}/wot/auth/prolongate/?application_id=${WARGAMING_APPLICATION_ID}&access_token=${
wargaming.token
}`,
)
.then((response) => response.json() as Promise<Extension>)
.then((json) => {
mutateApp((draft) => {
draft.logins.wargaming = {
id: wargaming.id,
token: json.data.access_token,
expires: json.data.expires_at * 1000,
};
});
});
}
}
@@ -66,9 +98,25 @@ export function Checks() {
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
if (expiresInDays < 0) {
logoutPatreon();
mutateApp((draft) => {
draft.logins.patreon = undefined;
});
} else if (expiresInDays < 15) {
extendAuthPatreon();
const { patreon } = appStore.getState().logins;
if (!patreon) return;
fetch(`/api/patreon/refresh/${patreon.refreshToken}`)
.then((response) => response.json() as Promise<PatreonAuthResponse>)
.then((data) => {
mutateApp((draft) => {
draft.logins.patreon = {
token: data.access_token,
refreshToken: data.refresh_token,
expires: Date.now() + data.expires_in * 1000,
};
});
});
}
}
});
@@ -93,7 +141,7 @@ export function Checks() {
variant="solid"
onClick={() => {
setShowDevBuildAlert(false);
useApp.setState({ devBuildAgreementTime: Date.now() });
appStore.setState({ devBuildAgreementTime: Date.now() });
}}
>
Continue
@@ -158,7 +206,7 @@ export function Checks() {
<Button
onClick={() => {
useApp.setState({
appStore.setState({
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
});
}}
+2 -2
View File
@@ -5,10 +5,10 @@ import { Button, Flex, Link, Text } from '@radix-ui/themes';
import { patreonLoginUrl } from '../../core/blitzkit/patreonLoginUrl';
import { useAdExempt } from '../../hooks/useAdExempt';
import { PatreonIcon } from '../../icons/Patreon';
import { useApp } from '../../stores/app';
import * as App from '../../stores/app';
export function PatreonPlug() {
const patreon = useApp((state) => state.logins.patreon);
const patreon = App.use((state) => state.logins.patreon);
const exempt = useAdExempt();
return (
+54 -51
View File
@@ -10,6 +10,7 @@ import { Footer } from '../components/Footer';
import { Loader } from '../components/Loader';
import Navbar, { NAVBAR_HEIGHT } from '../components/Navbar';
import { Party3 } from '../components/Party3';
import * as App from '../stores/app';
import { Checks } from './components/Checks';
import './layout.css';
@@ -31,62 +32,64 @@ export default function RootLayout({ children }: RootLayoutProps) {
const isRoot = pathname === '/';
return (
<html lang="en" className={robotoFlex.className}>
<head>
{isRoot && (
<>
<title>BlitzKit</title>
<meta
name="description"
content="🎉 Tools for everything World of Tanks Blitz"
/>
</>
)}
<meta property="og:site_name" content="BlitzKit" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<App.Provider>
<html lang="en" className={robotoFlex.className}>
<head>
{isRoot && (
<>
<title>BlitzKit</title>
<meta
name="description"
content="🎉 Tools for everything World of Tanks Blitz"
/>
</>
)}
<meta property="og:site_name" content="BlitzKit" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<Party3 />
</head>
<Party3 />
</head>
<body
style={{
margin: 0,
backgroundColor: isEmbed ? 'transparent' : undefined,
}}
>
<Theme
appearance="dark"
panelBackground="solid"
radius="full"
accentColor="amber"
suppressHydrationWarning
suppressContentEditableWarning
<body
style={{
margin: 0,
backgroundColor: isEmbed ? 'transparent' : undefined,
}}
>
<Flex
direction="column"
style={{
minHeight: '100vh',
paddingTop: isEmbed ? undefined : NAVBAR_HEIGHT,
}}
<Theme
appearance="dark"
panelBackground="solid"
radius="full"
accentColor="amber"
suppressHydrationWarning
suppressContentEditableWarning
>
{!isEmbed && <Navbar />}
<Checks />
<Suspense
fallback={
<div style={{ flex: 1 }}>
<Loader />
</div>
}
<Flex
direction="column"
style={{
minHeight: '100vh',
paddingTop: isEmbed ? undefined : NAVBAR_HEIGHT,
}}
>
{children}
</Suspense>
{!isEmbed && <Navbar />}
{!isEmbed && <Footer />}
</Flex>
</Theme>
</body>
</html>
<Checks />
<Suspense
fallback={
<div style={{ flex: 1 }}>
<Loader />
</div>
}
>
{children}
</Suspense>
{!isEmbed && <Footer />}
</Flex>
</Theme>
</body>
</html>
</App.Provider>
);
}
+4 -3
View File
@@ -2,10 +2,11 @@
import { Flex, Heading, Switch, Text } from '@radix-ui/themes';
import PageWrapper from '../../components/PageWrapper';
import { useApp } from '../../stores/app';
import * as App from '../../stores/app';
export default function Page() {
const developerMode = useApp((state) => state.developerMode);
const developerMode = App.use((state) => state.developerMode);
const appStore = App.useStore();
return (
<PageWrapper justify="center" align="center">
@@ -18,7 +19,7 @@ export default function Page() {
<Switch
checked={developerMode}
onCheckedChange={(checked) =>
useApp.setState({ developerMode: checked })
appStore.setState({ developerMode: checked })
}
/>
</Flex>
@@ -31,7 +31,7 @@ import {
} from '../../../../../../../core/blitzkit/tankDefinitions/constants';
import { useEquipment } from '../../../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../../../hooks/useFullScreen';
import { useApp } from '../../../../../../../stores/app';
import * as App from '../../../../../../../stores/app';
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../../../stores/tankopediaEphemeral';
import * as TankopediaPersistent from '../../../../../../../stores/tankopediaPersistent';
@@ -65,7 +65,7 @@ export function Options() {
const environment = TankopediaPersistent.use(
(state) => state.model.visual.environment,
);
const developerMode = useApp((state) => state.developerMode);
const developerMode = App.use((state) => state.developerMode);
const antagonistGun = useDuel((state) => state.antagonist!.gun);
const antagonistShell = useDuel((state) => state.antagonist!.shell);
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
+13 -6
View File
@@ -23,8 +23,6 @@ import { useState } from 'react';
import { REGIONS } from '../../constants/regions';
import { TOOLS } from '../../constants/tools';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { logoutPatreon } from '../../core/blitz/logoutPatreon';
import { logoutWargaming } from '../../core/blitz/logoutWargaming';
import { imgur, ImgurSize } from '../../core/blitzkit/imgur';
import { patreonLoginUrl } from '../../core/blitzkit/patreonLoginUrl';
import { BlitzkitWide } from '../../icons/BlitzkitWide';
@@ -32,7 +30,7 @@ import { PatreonIcon } from '../../icons/Patreon';
import { WargamingIcon } from '../../icons/Wargaming';
import strings from '../../lang/en-US.json';
import { theme } from '../../stitches.config';
import { useApp } from '../../stores/app';
import * as App from '../../stores/app';
import { Link } from '../Link';
import * as styles from './index.css';
@@ -41,8 +39,9 @@ export const NAVBAR_HEIGHT = 64;
export default function Navbar() {
const [showHamburgerMenu, setShowHamburgerMenu] = useState(false);
const pathname = usePathname();
const logins = useApp((state) => state.logins);
const logins = App.use((state) => state.logins);
const filteredTools = TOOLS.filter((tool) => !tool.href);
const mutateApp = App.useMutation();
return (
<Box
@@ -162,7 +161,11 @@ export default function Navbar() {
<Button
color="red"
variant="ghost"
onClick={logoutPatreon}
onClick={() =>
mutateApp((draft) => {
draft.logins.patreon = undefined;
})
}
>
Logout
</Button>
@@ -176,7 +179,11 @@ export default function Navbar() {
<Button
color="red"
variant="ghost"
onClick={logoutWargaming}
onClick={() =>
mutateApp((draft) => {
draft.logins.wargaming = undefined;
})
}
>
Logout
</Button>
-20
View File
@@ -1,20 +0,0 @@
import { PatreonAuthResponse } from '../../app/auth/[provider]/page';
import { mutateApp, useApp } from '../../stores/app';
export async function extendAuthPatreon() {
const { patreon } = useApp.getState().logins;
if (!patreon) return;
const data = (await fetch(
`/api/patreon/refresh/${patreon.refreshToken}`,
).then((response) => response.json())) as PatreonAuthResponse;
mutateApp((draft) => {
draft.logins.patreon = {
token: data.access_token,
refreshToken: data.refresh_token,
expires: Date.now() + data.expires_in * 1000,
};
});
}
-59
View File
@@ -1,59 +0,0 @@
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
import { mutateApp, useApp } from '../../stores/app';
import { idToRegion } from './idToRegion';
interface Extension {
data: {
access_token: string;
account_id: number;
expires_at: number;
};
}
export async function extendAuthWargaming() {
const { wargaming } = useApp.getState().logins;
if (!wargaming) return;
// error handling? what's that?
/*
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠙⠛⠛⠛⠿⠻⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⡀⠠⠤⠒⢂⣉⣉⣉⣑⣒⣒⠒⠒⠒⠒⠒⠒⠒⠀⠀⠐⠒⠚⠻⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⡠⠔⠉⣀⠔⠒⠉⣀⣀⠀⠀⠀⣀⡀⠈⠉⠑⠒⠒⠒⠒⠒⠈⠉⠉⠉⠁⠂⠀⠈⠙⢿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀⠔⠁⠠⠖⠡⠔⠊⠀⠀⠀⠀⠀⠀⠀⠐⡄⠀⠀⠀⠀⠀⠀⡄⠀⠀⠀⠀⠉⠲⢄⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⠊⠀⢀⣀⣤⣤⣤⣤⣀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠜⠀⠀⠀⠀⣀⡀⠀⠈⠃⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⡿⠥⠐⠂⠀⠀⠀⠀⡄⠀⠰⢺⣿⣿⣿⣿⣿⣟⠀⠈⠐⢤⠀⠀⠀⠀⠀⠀⢀⣠⣶⣾⣯⠀⠀⠉⠂⠀⠠⠤⢄⣀⠙⢿⣿⣿⣿⣿
⣿⡿⠋⠡⠐⠈⣉⠭⠤⠤⢄⡀⠈⠀⠈⠁⠉⠁⡠⠀⠀⠀⠉⠐⠠⠔⠀⠀⠀⠀⠀⠲⣿⠿⠛⠛⠓⠒⠂⠀⠀⠀⠀⠀⠀⠠⡉⢢⠙⣿⣿⣿
⣿⠀⢀⠁⠀⠊⠀⠀⠀⠀⠀⠈⠁⠒⠂⠀⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⢀⣀⡠⠔⠒⠒⠂⠀⠈⠀⡇⣿⣿⣿⣿⣿
⣿⠀⢸⠀⠀⠀⢀⣀⡠⠋⠓⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⠀⠈⠢⠤⡀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⡠⠀⡇⣿⣿⣿⣿⣿
⣿⡀⠘⠀⠀⠀⠀⠀⠘⡄⠀⠀⠀⠈⠑⡦⢄⣀⠀⠀⠐⠒⠁⢸⠀⠀⠠⠒⠄⠀⠀⠀⠀⠀⢀⠇⠀⣀⡀⠀⠀⢀⢾⡆⠀⠈⡀⠎⣸⣿⣿⣿
⣿⣿⣄⡈⠢⠀⠀⠀⠀⠘⣶⣄⡀⠀⠀⡇⠀⠀⠈⠉⠒⠢⡤⣀⡀⠀⠀⠀⠀⠀⠐⠦⠤⠒⠁⠀⠀⠀⠀⣀⢴⠁⠀⢷⠀⠀⠀⢰⣿⣿⣿⣿
⣿⣿⣿⣿⣇⠂⠀⠀⠀⠀⠈⢂⠀⠈⠹⡧⣀⠀⠀⠀⠀⠀⡇⠀⠀⠉⠉⠉⢱⠒⠒⠒⠒⢖⠒⠒⠂⠙⠏⠀⠘⡀⠀⢸⠀⠀⠀⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠑⠄⠰⠀⠀⠁⠐⠲⣤⣴⣄⡀⠀⠀⠀⠀⢸⠀⠀⠀⠀⢸⠀⠀⠀⠀⢠⠀⣠⣷⣶⣿⠀⠀⢰⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠁⢀⠀⠀⠀⠀⠀⡙⠋⠙⠓⠲⢤⣤⣷⣤⣤⣤⣤⣾⣦⣤⣤⣶⣿⣿⣿⣿⡟⢹⠀⠀⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠑⠀⢄⠀⡰⠁⠀⠀⠀⠀⠀⠈⠉⠁⠈⠉⠻⠋⠉⠛⢛⠉⠉⢹⠁⢀⢇⠎⠀⠀⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⠈⠢⢄⡉⠂⠄⡀⠀⠈⠒⠢⠄⠀⢀⣀⣀⣰⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⢀⣎⠀⠼⠊⠀⠀⠀⠘⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⡀⠉⠢⢄⡈⠑⠢⢄⡀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⢀⠀⠀⠀⠀⠀⢻⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣀⡈⠑⠢⢄⡀⠈⠑⠒⠤⠄⣀⣀⠀⠉⠉⠉⠉⠀⠀⠀⣀⡀⠤⠂⠁⠀⢀⠆⠀⠀⢸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⡀⠁⠉⠒⠂⠤⠤⣀⣀⣉⡉⠉⠉⠉⠉⢀⣀⣀⡠⠤⠒⠈⠀⠀⠀⠀⣸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣶⣤⣤⣤⣤⣀⣀⣤⣤⣤⣶⣾⣿⣿⣿⣿⣿
*/
const response = await fetch(
`https://api.worldoftanks.${idToRegion(
wargaming.id,
)}/wot/auth/prolongate/?application_id=${WARGAMING_APPLICATION_ID}&access_token=${
wargaming.token
}`,
);
const json = (await response.json()) as Extension;
mutateApp((draft) => {
draft.logins.wargaming = {
id: wargaming.id,
token: json.data.access_token,
expires: json.data.expires_at * 1000,
};
});
}
-7
View File
@@ -1,7 +0,0 @@
import { mutateApp } from '../../stores/app';
export function logoutPatreon() {
mutateApp((draft) => {
draft.logins.patreon = undefined;
});
}
-7
View File
@@ -1,7 +0,0 @@
import { mutateApp } from '../../stores/app';
export function logoutWargaming() {
mutateApp((draft) => {
draft.logins.wargaming = undefined;
});
}
+2 -2
View File
@@ -1,10 +1,10 @@
import { use, useMemo } from 'react';
import { useApp } from '../stores/app';
import * as App from '../stores/app';
let cache: Record<string, boolean> = {};
export function useAdExempt() {
const patreon = useApp((state) => state.logins.patreon);
const patreon = App.use((state) => state.logins.patreon);
const promise = useMemo(
() =>
new Promise<boolean>(async (resolve) => {
+1
View File
@@ -0,0 +1 @@
export const CURRENT_POLICIES_AGREEMENT_INDEX = 0;
+14 -17
View File
@@ -1,7 +1,7 @@
import { produce } from 'immer';
import { merge } from 'lodash';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { createNextSafeStore } from '../../core/zustand/createNextSafeStore';
interface WargamingLogin {
id: number;
@@ -25,20 +25,17 @@ export interface App {
policiesAgreementIndex: number;
}
export const CURRENT_POLICIES_AGREEMENT_INDEX = 0;
export const useApp = create<App>()(
persist(
(set) => ({
devBuildAgreementTime: 0,
developerMode: false,
policiesAgreementIndex: -1,
logins: {},
}),
{ name: 'app', merge: (a, b) => merge(b, a) },
),
export const { Provider, use, useMutation, useStore } = createNextSafeStore(
() =>
create<App>()(
persist(
(set) => ({
devBuildAgreementTime: 0,
developerMode: false,
policiesAgreementIndex: -1,
logins: {},
}),
{ name: 'app', merge: (a, b) => merge(b, a) },
),
),
);
export function mutateApp(recipe: (draft: App) => void) {
useApp.setState(produce(recipe));
}