Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 9 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,12 +22,14 @@ export default function RootLayout({
<UserAccessHashesProvider>
<QueryProvider>
<AppContextProvider>
<IntlProvider>
<RootContainer>
{children}
<Toaster richColors />
</RootContainer>
</IntlProvider>
<DeferredLinkProvider>
<IntlProvider>
<RootContainer>
{children}
<Toaster richColors />
</RootContainer>
</IntlProvider>
</DeferredLinkProvider>
</AppContextProvider>
</QueryProvider>
</UserAccessHashesProvider>
Expand Down
49 changes: 49 additions & 0 deletions src/app/redirect/[deeplink]/deferred-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
createContext,
useContext,
useState,
ReactNode,
Dispatch,
SetStateAction,
} from 'react';

const DeferredLinkContext = createContext<DeferredLinkState | undefined>(
undefined,
);

export interface DeferredLink {
type: 'add-friend';
userId: number;
token: string;
}

export interface DeferredLinkState {
link: DeferredLink | undefined;
setLink: Dispatch<SetStateAction<DeferredLink | undefined>>;
}

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<DeferredLink | undefined>();
const state: DeferredLinkState = {link, setLink};

return (
<DeferredLinkContext.Provider value={state}>
{children}
</DeferredLinkContext.Provider>
);
}
7 changes: 7 additions & 0 deletions src/app/redirect/[deeplink]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -21,10 +22,16 @@ export default function DeeplinkPage(): ReactNode {
const [handling, setHandling] = useState(false);
const [state, setState] = useState<State>('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;
}
Expand Down
62 changes: 42 additions & 20 deletions src/app/sign-in/code.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -49,37 +50,58 @@ function CodeDialogContent({email}: CodeDialogProps): ReactNode {
const backend = useBackend();
const navigate = useNavigate();
const session = useSession();
const [deferredLink, setDeferredLink] = useDeferredLink();
const blockingQR = useBlockingQR();

async function handleAddFriend() {
Comment thread
y9san9 marked this conversation as resolved.
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) {
setError(true);
return;
}
setLoading(true);
try {
Comment thread
y9san9 marked this conversation as resolved.
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 (
Expand Down
21 changes: 9 additions & 12 deletions src/app/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
31 changes: 29 additions & 2 deletions src/app/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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() {
Comment thread
y9san9 marked this conversation as resolved.
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);
Expand Down Expand Up @@ -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;
Expand All @@ -75,7 +101,8 @@ export default function SignInPage() {
result.data.id.toString(),
);
session.setAuthed();
void Notifications.nudge();
await handleAddFriend();
await Notifications.nudge();
Comment thread
y9san9 marked this conversation as resolved.
void navigate('/');
} else {
toast.error(t('error-connection'));
Expand Down
2 changes: 1 addition & 1 deletion src/components/intl-provider.tsx
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
},
"profile": {
"edit_profile": "Редактировать",
"no_interests": "У пользователя ещё нет интересов",
"log_out": "Выйти",
"interests": "Интересы",
"open_social": "Связаться",
Expand Down
Empty file removed src/router.ts
Empty file.
Loading