feat: more login progress

This commit is contained in:
TrèsAbhi committed 2024-10-09 21:04:49 -05:00
1 parent 1e64cb9e7d
commit 513105a624
18 files changed
+290 -295

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
'use client';
import { idToRegion } from '@blitzkit/core';
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
import { useEffect, useState } from 'react';
import type { PatreonAuthResponse } from '../pages/auth/[provider]/_index';
import { App } from '../stores/app';
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../stores/app/constants';
type Extension =
| {
status: 'ok';
data: {
access_token: string;
account_id: number;
expires_at: number;
};
}
| {
status: 'error';
};
export function Checks() {
return (
<App.Provider>
<Content />
</App.Provider>
);
}
/**
* Wargaming: 14 (refresh: 7)
* Patreon: 31 (refresh: 15)
*/
function Content() {
const logins = App.use((state) => state.logins);
const mutateApp = App.useMutation();
const appStore = App.useStore();
const policiesAgreementIndex = App.use(
(state) => state.policiesAgreementIndex,
);
const isLegal = window.location.pathname.startsWith('/legal');
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
useEffect(() => {
function unsubscribe() {
window.removeEventListener('scroll', show);
window.removeEventListener('pointermove', show);
window.removeEventListener('pointerdown', show);
}
function show() {
setShowPoliciesAgreement(true);
unsubscribe();
}
if (
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
!isLegal
) {
window.addEventListener('scroll', show);
window.addEventListener('pointermove', show);
window.addEventListener('pointerdown', show);
}
return unsubscribe;
}, [policiesAgreementIndex, isLegal]);
useEffect(() => {
if (logins.wargaming) {
const expiresIn = logins.wargaming.expires - Date.now();
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
if (expiresInDays < 0) {
mutateApp((draft) => {
draft.logins.wargaming = undefined;
});
} else if (expiresInDays < 7) {
const { wargaming } = appStore.getState().logins;
if (!wargaming) return;
fetch(
`https://api.worldoftanks.${idToRegion(
wargaming.id,
)}/wot/auth/prolongate/?application_id=${
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
}&access_token=${wargaming.token}`,
)
.then((response) => response.json() as Promise<Extension>)
.then((json) => {
mutateApp((draft) => {
if (json.status === 'error') {
draft.logins.wargaming = undefined;
} else {
draft.logins.wargaming = {
id: wargaming.id,
token: json.data.access_token,
expires: json.data.expires_at * 1000,
};
}
});
});
}
}
if (logins.patreon) {
const expiresIn = logins.patreon.expires - Date.now();
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
if (expiresInDays < 0) {
mutateApp((draft) => {
draft.logins.patreon = undefined;
});
} else if (expiresInDays < 15) {
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,
};
});
});
}
}
});
if (!showPoliciesAgreement) return null;
return (
<Flex
align="end"
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'var(--color-overlay)',
zIndex: 2,
}}
>
<Flex
p="8"
style={{
width: '100%',
background: 'var(--color-panel-solid)',
}}
justify="center"
>
<Flex
align="start"
gap="4"
style={{
maxWidth: 640 * 2,
}}
direction="column"
>
<Flex direction="column" gap="2">
<Heading>
{policiesAgreementIndex === -1
? "BlitzKit's policies"
: "BlitzKit's policies have changed"}
</Heading>
<Text>
This website utilizes cookies to perform analytics and personalize
your experience. You can learn more through{' '}
<Link href="/docs/legal/privacy-policy">our privacy policy</Link>.
You may opt out of personalized advertizement by visiting{' '}
<Link href="https://optout.aboutads.info/">WebChoices</Link>. By
using BlitzKit, you also agree to our{' '}
<Link href="/docs/legal/terms-of-service">terms of service</Link>.
</Text>
</Flex>
<Button
mt="2"
onClick={() => {
appStore.setState({
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
});
setShowPoliciesAgreement(false);
}}
>
I agree
</Button>
</Flex>
</Flex>
</Flex>
);
}
@@ -1,5 +1,4 @@
import { patreonLoginUrl } from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { PersonIcon } from '@radix-ui/react-icons';
import {
Button,
@@ -14,8 +13,6 @@ import {
} from '@radix-ui/themes';
import { Suspense } from 'react';
import { App } from '../../stores/app';
import { $patreonLogin } from '../../stores/patreonLogin';
import { $wargamingLogin } from '../../stores/wargamingLogin';
import { BlitzKitTheme } from '../BlitzKitTheme';
import { PatreonIcon } from '../PatreonIcon';
import { WargamingAccountName } from '../WargamingAccountName';
@@ -23,8 +20,16 @@ import { WargamingIcon } from '../WargamingIcon';
import { WargamingLoginButton } from '../WargamingLoginButton';
export function NavbarAccount() {
const patreon = useStore($patreonLogin);
const wargaming = useStore($wargamingLogin);
return (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
const logins = App.use((state) => state.logins);
const mutateApp = App.useMutation();
return (
<App.Provider>
@@ -41,13 +46,13 @@ export function NavbarAccount() {
width="320px"
onOpenAutoFocus={(event) => event.preventDefault()}
>
{(patreon.token || wargaming.token) && (
{(logins.patreon || logins.wargaming) && (
<Flex direction="column" gap="2">
<Text align="center" color="gray">
Logged in accounts
</Text>
{patreon.token && (
{logins.patreon && (
<Flex align="center" gap="2" justify="center">
<PatreonIcon width={15} height={15} />
<Text>Patreon</Text>
@@ -55,7 +60,9 @@ export function NavbarAccount() {
color="red"
variant="ghost"
onClick={() => {
$patreonLogin.setKey('token', undefined);
mutateApp((draft) => {
draft.logins.patreon = undefined;
});
}}
>
Logout
@@ -63,7 +70,7 @@ export function NavbarAccount() {
</Flex>
)}
{wargaming.token && (
{logins.wargaming && (
<Flex align="center" gap="2" justify="center">
<WargamingIcon width={15} height={15} />
<Text>
@@ -77,7 +84,9 @@ export function NavbarAccount() {
color="red"
variant="ghost"
onClick={() => {
$wargamingLogin.setKey('token', undefined);
mutateApp((draft) => {
draft.logins.wargaming = undefined;
});
}}
>
Logout
@@ -87,19 +96,19 @@ export function NavbarAccount() {
</Flex>
)}
{!patreon.token !== !wargaming.token && (
{!logins.patreon !== !logins.wargaming && (
<Inset side="x">
<Separator my="4" size="4" />
</Inset>
)}
{(!patreon.token || !wargaming.token) && (
{(!logins.patreon || !logins.wargaming) && (
<Flex direction="column" gap="2">
<Text align="center" color="gray">
Log in with...
</Text>
{!patreon.token && (
{!logins.patreon && (
<Link style={{ width: '100%' }} href={patreonLoginUrl()}>
<Button style={{ width: '100%' }} color="tomato">
<PatreonIcon width={15} height={15} /> Patreon
@@ -107,7 +116,7 @@ export function NavbarAccount() {
</Link>
)}
{!wargaming.token && <WargamingLoginButton />}
{!logins.wargaming && <WargamingLoginButton />}
</Flex>
)}
</Popover.Content>
+10 -3
View File
@@ -1,5 +1,4 @@
import { assertSecret, patreonLoginUrl } from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { ArrowRightIcon } from '@radix-ui/react-icons';
import {
Badge,
@@ -11,12 +10,20 @@ import {
Text,
} from '@radix-ui/themes';
import { useAdExempt } from '../hooks/useAdExempt';
import { $patreonLogin } from '../stores/patreonLogin';
import { App } from '../stores/app';
import { PatreonIcon } from './PatreonIcon';
export function Plugs() {
return (
<App.Provider>
<Content />
</App.Provider>
);
}
function Content() {
const exempt = useAdExempt();
const hasPatreon = useStore($patreonLogin).token !== undefined;
const hasPatreon = App.use((state) => state.logins.patreon !== undefined);
return (
<Flex
@@ -9,11 +9,12 @@ import {
Text,
} from '@radix-ui/themes';
import { useState } from 'react';
import { App } from '../../../../../../stores/app';
import { Duel } from '../../../../../../stores/duel';
import { $wargamingLogin } from '../../../../../../stores/wargamingLogin';
import { Stars } from '../../Stars';
export function VoteCaster() {
const appStore = App.useStore();
const tank = Duel.use((state) => state.protagonist.tank);
const [easiness, setEasiness] = useState<StarsInt | null>(null);
const [firepower, setFirepower] = useState<StarsInt | null>(null);
@@ -97,9 +98,9 @@ export function VoteCaster() {
survivability === null)
}
onClick={() => {
const wargaming = $wargamingLogin.get();
const { wargaming } = appStore.getState().logins;
if (!wargaming.token) {
if (!wargaming) {
return setShowDialogue(false);
}
@@ -1,10 +1,9 @@
import { formatCompact } from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
import { memo } from 'react';
import { useTankVotes } from '../../../../../hooks/useTankVotes';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { $wargamingLogin } from '../../../../../stores/wargamingLogin';
import { WargamingLoginButton } from '../../../../WargamingLoginButton';
import { StarRow } from './components/StarRow';
import { VoteCaster } from './components/VoteCaster';
@@ -13,7 +12,7 @@ export const Votes = memo(
() => {
const id = Duel.use((state) => state.protagonist.tank.id);
const votes = useTankVotes(id);
const wargaming = useStore($wargamingLogin);
const wargaming = App.use((state) => state.logins.wargaming);
return (
<Flex direction="column" gap="3">
@@ -1,22 +1,19 @@
import { getAccountInfo, idToRegion } from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { Text } from '@radix-ui/themes';
import { useMemo } from 'react';
import usePromise from 'react-promise-suspense';
import { $wargamingLogin } from '../stores/wargamingLogin';
import { App } from '../stores/app';
export function WargamingAccountName() {
const wargaming = useStore($wargamingLogin);
const wargaming = App.use((state) => state.logins.wargaming);
const promise = useMemo(async () => {
if (!wargaming.token) return null;
if (!wargaming) return null;
const data = await getAccountInfo(
idToRegion(Number(wargaming.id)),
Number(wargaming.id),
idToRegion(wargaming.id),
wargaming.id,
[],
{
access_token: wargaming.token,
},
{ access_token: wargaming.token },
);
return data?.nickname as string | undefined;
+3 -4
View File
@@ -1,16 +1,15 @@
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { $patreonLogin } from '../stores/patreonLogin';
import { App } from '../stores/app';
let cache: Record<string, boolean | Promise<boolean>> = {};
export function useAdExempt() {
const patreon = useStore($patreonLogin);
const patreon = App.use((state) => state.logins.patreon);
const [exempt, setExempt] = useState(false);
useEffect(() => {
(async () => {
if (!patreon.token) return setExempt(false);
if (!patreon) return setExempt(false);
if (!(patreon.token in cache)) {
cache[patreon.token] = new Promise<boolean>(async (resolve) => {
+3 -4
View File
@@ -3,18 +3,17 @@ import {
type BlitzkitResponse,
type TankVotes,
} from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { $wargamingLogin } from '../stores/wargamingLogin';
import { App } from '../stores/app';
const cache: Record<string, Promise<TankVotes> | TankVotes> = {};
export function useTankVotes(id: number) {
const [votes, setVotes] = useState<TankVotes | null>(null);
const wargaming = useStore($wargamingLogin);
const wargaming = App.use((state) => state.logins.wargaming);
useEffect(() => {
if (!wargaming.token) return;
if (!wargaming) return;
if (id in cache) {
Promise.all([cache[id]]).then(([votes]) => setVotes(votes));
@@ -1,6 +1,7 @@
---
import { Flex } from '@radix-ui/themes';
import { BlitzKitTheme } from '../components/BlitzKitTheme';
import { Checks } from '../components/Checks';
import Footer from '../components/Footer.astro';
import Navbar from '../components/Navbar.astro';
import Party3 from '../components/Party3.astro';
@@ -43,11 +44,9 @@ const isRoot = pathname === '/';
<Flex direction="column" minHeight="100vh">
<Navbar />
<!-- <Checks /> -->
<Checks client:only />
<!-- <Suspense fallback={(<PageLoader />)}> -->
<slot />
<!-- </Suspense> -->
<Footer />
</Flex>
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { $patreonLogin } from '../../../stores/patreonLogin';
import { App } from '../../../stores/app';
import { $wargamingLogin } from '../../../stores/wargamingLogin';
export const AUTH_PROVIDERS = ['wargaming', 'patreon'] as const;
@@ -25,6 +25,8 @@ interface AuthorizeProps {
}
export function Authorize({ provider }: AuthorizeProps) {
const mutateApp = App.useMutation();
useEffect(() => {
(async () => {
const searchParams = new URLSearchParams(window.location.search);
@@ -56,10 +58,12 @@ export function Authorize({ provider }: AuthorizeProps) {
);
const data = (await response.json()) as PatreonAuthResponse;
$patreonLogin.set({
token: data.access_token,
refreshToken: data.refresh_token,
expires: `${Date.now() + data.expires_in * 1000}`,
mutateApp((draft) => {
draft.logins.patreon = {
token: data.access_token,
refreshToken: data.refresh_token,
expires: Date.now() + data.expires_in * 1000,
};
});
break;
@@ -1,10 +1,13 @@
import { PageWrapper } from '../../../components/PageWrapper';
import { TankSearch } from '../../../components/TankSearch';
import { App } from '../../../stores/app';
export function Page() {
return (
<PageWrapper color="purple" size={1028}>
<TankSearch />
<App.Provider>
<TankSearch />
</App.Provider>
</PageWrapper>
);
}
+16 -4
View File
@@ -1,14 +1,26 @@
import { merge } from 'lodash-es';
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { createContextualStore } from '../../core/zustand/createContextualStore';
interface AppStore {
developerMode: boolean;
policiesAgreementIndex: number;
logins: {
wargaming?: { token: string; id: number; expires: number };
patreon?: { token: string; refreshToken: string; expires: number };
};
}
export const App = createContextualStore(() =>
create<AppStore>()(() => ({
developerMode: false,
policiesAgreementIndex: 0,
})),
create<AppStore>()(
persist(
subscribeWithSelector<AppStore>(() => ({
developerMode: false,
policiesAgreementIndex: -1,
logins: {},
})),
{ name: 'app', merge: (a, b) => merge(b, a) },
),
),
);
@@ -1,15 +0,0 @@
import { persistentMap } from '@nanostores/persistent';
type PatreonLogin =
| {
token: undefined;
}
| {
token: string;
refreshToken: string;
expires: string;
};
export const $patreonLogin = persistentMap<PatreonLogin>('patreon:', {
token: undefined,
});
@@ -1,15 +0,0 @@
import { persistentMap } from '@nanostores/persistent';
type WargamingLogin =
| {
token: undefined;
}
| {
token: string;
id: string;
expires: string;
};
export const $wargamingLogin = persistentMap<WargamingLogin>('wargaming:', {
token: undefined,
});