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