diff --git a/package.json b/package.json index 486d3e0..78a6286 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,12 @@ "packageManager": "pnpm@11.1.2", "scripts": { "dev": "vite", - "compile": "tsc -p tsconfig.json --noEmit --pretty false", + "compile": "tsc --pretty false --noErrorTruncation", "watch": "tsc -p tsconfig.json --noEmit --pretty false -w", "build": "vite build", "start": "vite preview --host 0.0.0.0", "format": "eslint --fix '**/*.ts*' && prettier --write .", - "lint": "eslint '**/*.ts*' && prettier --check ." + "lint": "tsc && eslint '**/*.ts*' && prettier --check ." }, "engines": { "node": ">=24.0.0" diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f0caa49..a6f6763 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import {QueryProvider} from '@/components/query-provider'; import {SessionProvider} from '@/components/session-provider'; import IntlProvider from '@/components/intl-provider'; import {UserAccessHashesProvider} from '@/components/useraccesshashes-provider'; +import {DeferredLinkProvider} from '@/app/redirect/[deeplink]/deferred-link'; export default function RootLayout({ children, @@ -21,12 +22,14 @@ export default function RootLayout({ - - - {children} - - - + + + + {children} + + + + diff --git a/src/app/redirect/[deeplink]/deferred-link.tsx b/src/app/redirect/[deeplink]/deferred-link.tsx new file mode 100644 index 0000000..e10f6b5 --- /dev/null +++ b/src/app/redirect/[deeplink]/deferred-link.tsx @@ -0,0 +1,49 @@ +import { + createContext, + useContext, + useState, + ReactNode, + Dispatch, + SetStateAction, +} from 'react'; + +const DeferredLinkContext = createContext( + undefined, +); + +export interface DeferredLink { + type: 'add-friend'; + userId: number; + token: string; +} + +export interface DeferredLinkState { + link: DeferredLink | undefined; + setLink: Dispatch>; +} + +export function useDeferredLink(): [ + DeferredLinkState['link'], + DeferredLinkState['setLink'], +] { + const context = useContext(DeferredLinkContext); + if (!context) { + throw new Error("Can't use deferred link outside of provider"); + } + return [context.link, context.setLink]; +} + +export interface DeeplinkProviderProps { + children: ReactNode; +} + +export function DeferredLinkProvider({children}: DeeplinkProviderProps) { + const [link, setLink] = useState(); + const state: DeferredLinkState = {link, setLink}; + + return ( + + {children} + + ); +} diff --git a/src/app/redirect/[deeplink]/page.tsx b/src/app/redirect/[deeplink]/page.tsx index 349d77a..b422dbb 100644 --- a/src/app/redirect/[deeplink]/page.tsx +++ b/src/app/redirect/[deeplink]/page.tsx @@ -9,6 +9,7 @@ import {useEffect, useState, ReactNode} from 'react'; import {useSession} from '@/components/session-provider'; import {useBlockingQR} from '@/app/blocking-qr/dialog'; import {StyledDialogWrapper} from '@/components/styled-dialog-wrapper'; +import {useDeferredLink} from './deferred-link'; type State = 'loading' | 'friend-token-expired'; @@ -21,10 +22,16 @@ export default function DeeplinkPage(): ReactNode { const [handling, setHandling] = useState(false); const [state, setState] = useState('loading'); const {deeplink} = useParams(); + const [_, setDeferredLink] = useDeferredLink(); async function addFriend(userId: number, token: string) { setHandling(true); if (session.status === 'guest') { + setDeferredLink({ + type: 'add-friend', + userId, + token, + }); void navigate('/sign-up'); return; } diff --git a/src/app/sign-in/code.tsx b/src/app/sign-in/code.tsx index bdb264f..6fd4beb 100644 --- a/src/app/sign-in/code.tsx +++ b/src/app/sign-in/code.tsx @@ -1,5 +1,6 @@ import {REGEXP_ONLY_DIGITS} from 'input-otp'; import {useSession} from '@/components/session-provider'; +import {useDeferredLink} from '@/app/redirect/[deeplink]/deferred-link'; import {useBlockingQR} from '@/app/blocking-qr/dialog'; import * as Dialog from '@radix-ui/react-dialog'; import {toast} from 'sonner'; @@ -49,8 +50,31 @@ function CodeDialogContent({email}: CodeDialogProps): ReactNode { const backend = useBackend(); const navigate = useNavigate(); const session = useSession(); + const [deferredLink, setDeferredLink] = useDeferredLink(); const blockingQR = useBlockingQR(); + async function handleAddFriend() { + const link = deferredLink; + if (!link) return; + if (link.type !== 'add-friend') return; + const {userId, token} = link; + while (true) { + // If profile account is created, network conditions were good. + // If we have a problem after we already created account, + // it's very hard to rollback. So we just retry indefinitely and + // show no indication on failure since it's very unlikely also. + const result = await backend.addFriend({userId, token}); + if (result.ok) { + if (result.data.type === 'Success') { + blockingQR.setShouldBlock(false); + } + break; + } + await new Promise(resolve => setTimeout(resolve, 1_000)); + } + setDeferredLink(undefined); + } + async function onComplete() { setError(false); if (value.length !== 8) { @@ -58,28 +82,26 @@ function CodeDialogContent({email}: CodeDialogProps): ReactNode { return; } setLoading(true); - try { - const code = Number(value); - const result = await backend.authLogin({email, code}); - if (!result.ok) { - if (result.error.type === 'status') { - setError(true); - } else { - toast.error(t('error-connection')); - } - return; + const code = Number(value); + const result = await backend.authLogin({email, code}); + if (!result.ok) { + if (result.error.type === 'status') { + setError(true); + } else { + toast.error(t('error-connection')); } - backend.storeAuthorization( - result.data.token, - result.data.id.toString(), - ); - session.setAuthed(); - blockingQR.setShouldBlock(false); - void Notifications.nudge(); - void navigate('/'); - } finally { - setLoading(false); + return; } + backend.storeAuthorization( + result.data.token, + result.data.id.toString(), + ); + session.setAuthed(); + blockingQR.setShouldBlock(false); + await handleAddFriend(); + await Notifications.nudge(); + void navigate('/'); + setLoading(false); } return ( diff --git a/src/app/sign-in/page.tsx b/src/app/sign-in/page.tsx index d605aeb..6406d37 100644 --- a/src/app/sign-in/page.tsx +++ b/src/app/sign-in/page.tsx @@ -54,20 +54,17 @@ function EmailContent(): ReactNode { return; } setLoading(true); - try { - const result = await backend.authEmail(locale, {email}); - if (!result.ok) { - if (result.error.type === 'unauthorized') { - setError(t('unknown-email')); - } else { - toast.error(t('error-connection')); - } - return; + const result = await backend.authEmail(locale, {email}); + if (!result.ok) { + if (result.error.type === 'unauthorized') { + setError(t('unknown-email')); + } else { + toast.error(t('error-connection')); } - setOpenCode(true); - } finally { - setLoading(false); + return; } + setOpenCode(true); + setLoading(false); } function onSignUp() { diff --git a/src/app/sign-up/page.tsx b/src/app/sign-up/page.tsx index c37284d..e15c6b5 100644 --- a/src/app/sign-up/page.tsx +++ b/src/app/sign-up/page.tsx @@ -1,6 +1,7 @@ import {Spinner} from '@/components/ui/spinner'; import {Textarea} from '@/components/ui/textarea'; import {User, Link, Heart} from 'lucide-react'; +import {useDeferredLink} from '@/app/redirect/[deeplink]/deferred-link'; import { InputGroup, InputGroupAddon, @@ -18,13 +19,16 @@ import {useSession} from '@/components/session-provider'; import {useTranslations} from 'use-intl'; import {useNavigate} from 'react-router'; import * as Notifications from '@/notifications'; +import {useBlockingQR} from '@/app/blocking-qr/dialog'; import {cn} from '@/lib/utils'; -export default function SignInPage() { +export default function SignUpPage() { const t = useTranslations('sign-up'); const navigate = useNavigate(); const session = useSession(); + const [deferredLink, setDeferredLink] = useDeferredLink(); + const blockingQR = useBlockingQR(); const [loading, setLoading] = useState(false); const [avatarLoading, setAvatarLoading] = useState(false); @@ -57,6 +61,28 @@ export default function SignInPage() { setInterestsError, }); + async function handleAddFriend() { + const link = deferredLink; + if (!link) return; + if (link.type !== 'add-friend') return; + const {userId, token} = link; + while (true) { + // If profile account is created, network conditions were good. + // If we have a problem after we already created account, + // it's very hard to rollback. So we just retry indefinitely and + // show no indication on failure since it's very unlikely also. + const result = await backend.addFriend({userId, token}); + if (result.ok) { + if (result.data.type === 'Success') { + blockingQR.setShouldBlock(false); + } + break; + } + await new Promise(resolve => setTimeout(resolve, 1_000)); + } + setDeferredLink(undefined); + } + async function onSignUp() { const validated = validator(); if (!validated) return; @@ -75,7 +101,8 @@ export default function SignInPage() { result.data.id.toString(), ); session.setAuthed(); - void Notifications.nudge(); + await handleAddFriend(); + await Notifications.nudge(); void navigate('/'); } else { toast.error(t('error-connection')); diff --git a/src/components/intl-provider.tsx b/src/components/intl-provider.tsx index 14de449..7eb47fd 100644 --- a/src/components/intl-provider.tsx +++ b/src/components/intl-provider.tsx @@ -1,6 +1,6 @@ import {IntlProvider as UseIntlProvider} from 'use-intl'; import {useEffect, useState} from 'react'; -import en from '../messages/en.json'; +import type en from '../messages/en.json'; type Messages = typeof en; diff --git a/src/messages/ru.json b/src/messages/ru.json index 0827e0a..86c8db5 100644 --- a/src/messages/ru.json +++ b/src/messages/ru.json @@ -27,6 +27,7 @@ }, "profile": { "edit_profile": "Редактировать", + "no_interests": "У пользователя ещё нет интересов", "log_out": "Выйти", "interests": "Интересы", "open_social": "Связаться", diff --git a/src/router.ts b/src/router.ts deleted file mode 100644 index e69de29..0000000