Skip to content

Commit a53df22

Browse files
feat: telegram link page
1 parent a796e56 commit a53df22

File tree

11 files changed

+364
-261
lines changed

11 files changed

+364
-261
lines changed
Lines changed: 2 additions & 251 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
"use client";
2-
import {
3-
Dialog,
4-
DialogContent,
5-
DialogHeader,
6-
DialogTitle,
7-
DialogTrigger,
8-
} from "@/components/ui/dialog";
9-
10-
import { Button } from "@/components/ui/button";
11-
import { Label } from "@/components/ui/label";
12-
import { Progress } from "@/components/ui/progress";
13-
import { auth, useSession } from "@/lib/auth";
14-
import { APIError } from "better-auth/api";
15-
import { useEffect, useMemo, useState } from "react";
16-
import { InputWithPrefix } from "@/components/input-prefix";
17-
import { Code } from "@/components/code";
18-
import { toast } from "sonner";
19-
import { ClockAlertIcon } from "lucide-react";
2+
import { useSession } from "@/lib/auth";
203
import { useTRPC } from "@/lib/trpc/client";
214
import { useQuery } from "@tanstack/react-query";
225

@@ -28,7 +11,7 @@ export function Telegram() {
2811
return user.telegramUsername && user.telegramId ? (
2912
<ShowTelegram username={user.telegramUsername} userId={user.telegramId} />
3013
) : (
31-
<LinkTelegram />
14+
<></>
3215
);
3316
}
3417

@@ -52,235 +35,3 @@ function ShowTelegram({
5235
</>
5336
);
5437
}
55-
56-
function LinkTelegram() {
57-
const [open, setOpen] = useState(false);
58-
const { refetch } = useSession();
59-
function handleComplete() {
60-
toast.success("Telegram link completed!");
61-
refetch();
62-
localStorage.removeItem("linktg");
63-
setOpen(false);
64-
}
65-
return (
66-
<Dialog open={open} onOpenChange={setOpen}>
67-
<DialogTrigger asChild>
68-
<Button size="sm" variant="secondary">
69-
Link
70-
</Button>
71-
</DialogTrigger>
72-
<DialogContent>
73-
<DialogHeader>
74-
<DialogTitle>Link your Telegram</DialogTitle>
75-
<div className="flex min-h-40 flex-col justify-center">
76-
<Form onComplete={handleComplete} />
77-
</div>
78-
</DialogHeader>
79-
</DialogContent>
80-
</Dialog>
81-
);
82-
}
83-
84-
function getSaved() {
85-
const saved = localStorage.getItem("linktg");
86-
if (!saved) return null;
87-
const { username, code, ttl, startTime } = JSON.parse(saved) as {
88-
username: string;
89-
code: string;
90-
ttl: number;
91-
startTime: number;
92-
};
93-
94-
const leftTime = ttl - (Date.now() - startTime) / 1000;
95-
if (leftTime <= 0) {
96-
localStorage.removeItem("linktg");
97-
return null;
98-
}
99-
100-
return { username, leftTime, code, ttl };
101-
}
102-
103-
function Form({ onComplete }: { onComplete: () => void }) {
104-
const saved = useMemo(() => getSaved(), []);
105-
const [username, setUsername] = useState<string>(saved?.username ?? "");
106-
const [code, setCode] = useState<string | null>(saved?.code ?? null);
107-
const [ttl, setTTL] = useState<number | null>(saved?.leftTime ?? null);
108-
const [expired, setExpired] = useState<boolean>(false);
109-
110-
async function handleSubmit(
111-
e:
112-
| React.FormEvent<HTMLFormElement>
113-
| React.MouseEvent<HTMLButtonElement, MouseEvent>,
114-
) {
115-
e.stopPropagation();
116-
e.preventDefault();
117-
if (username.length === 0) return;
118-
const res = await auth.telegram.link.start({
119-
telegramUsername: username,
120-
});
121-
122-
if (res.error) return console.error("custom error", res.error);
123-
if (res.data instanceof APIError)
124-
return console.error("better-auth APIError", res.data);
125-
setExpired(false);
126-
setCode(res.data.code);
127-
setTTL(res.data.ttl);
128-
129-
localStorage.setItem(
130-
"linktg",
131-
JSON.stringify({
132-
code: res.data.code,
133-
ttl: res.data.ttl,
134-
startTime: Date.now(),
135-
username,
136-
}),
137-
);
138-
}
139-
140-
function reset() {
141-
setTTL(null);
142-
setCode(null);
143-
setExpired(false);
144-
setUsername("");
145-
localStorage.removeItem("linktg");
146-
}
147-
148-
useEffect(() => {
149-
if (!code || !ttl) return;
150-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
151-
const interval = setInterval(async () => {
152-
const res = await auth.telegram.link.verify({ query: { code } });
153-
154-
if (res.error) return console.error("custom error", res.error);
155-
if (res.data instanceof APIError)
156-
return console.error("better-auth APIError", res.data);
157-
158-
if (res.data.verified) {
159-
clearInterval(interval);
160-
onComplete();
161-
return;
162-
}
163-
164-
if (res.data.expired) {
165-
setExpired(true);
166-
localStorage.removeItem("linktg");
167-
setTTL(null);
168-
setCode(null);
169-
clearInterval(interval);
170-
return;
171-
}
172-
173-
console.log("polling... not verified or expired");
174-
}, 5000);
175-
return () => clearInterval(interval);
176-
}, [code, onComplete, ttl]);
177-
178-
if (expired)
179-
return (
180-
<div className="flex flex-col items-center gap-4 py-8">
181-
<ClockAlertIcon size={64} />
182-
<p>Code Expired!</p>
183-
<Button onClick={handleSubmit}>Regenerate</Button>
184-
</div>
185-
);
186-
187-
return code && ttl ? (
188-
<div className="flex flex-col items-center gap-4 pt-8 pb-4">
189-
<p className="flex items-center justify-between gap-4 text-4xl">
190-
{code.split("").map((c, i) => (
191-
<span key={i}>{c}</span>
192-
))}
193-
</p>
194-
<Timer
195-
ttl={saved?.ttl ?? ttl}
196-
timeLeft={ttl}
197-
onEnd={() => setExpired(true)}
198-
/>
199-
200-
<div className="flex items-center gap-2">
201-
<Button variant="outline">
202-
<a
203-
aria-label="open telegram bot"
204-
href="tg://resolve?domain=pn_ts_dev_bot"
205-
>
206-
Start the bot
207-
</a>
208-
</Button>
209-
<p>and then send</p>
210-
<Code copyOnClick>/link</Code>
211-
</div>
212-
213-
<p className="text-foreground/30 pt-2 text-xs">
214-
Having troubles with this code?{" "}
215-
<button className="underline" onClick={reset}>
216-
Click to reset
217-
</button>
218-
</p>
219-
</div>
220-
) : (
221-
<form onSubmit={handleSubmit} className="flex flex-col items-center gap-4">
222-
<div className="flex items-center gap-4">
223-
<Label htmlFor="username">Username</Label>
224-
<InputWithPrefix
225-
prefix="@"
226-
id="username"
227-
autoComplete="off"
228-
min={1}
229-
value={username}
230-
pattern="^[^@]*"
231-
onChange={(e) => setUsername(e.target.value.replaceAll("@", ""))}
232-
placeholder="example"
233-
/>
234-
</div>
235-
<Button type="submit">Inizia</Button>
236-
</form>
237-
);
238-
}
239-
240-
function Timer({
241-
ttl,
242-
timeLeft: pTimeLeft,
243-
onEnd,
244-
}: {
245-
ttl: number;
246-
timeLeft: number;
247-
onEnd: () => void;
248-
}) {
249-
const ttlMs = ttl * 1000;
250-
const [timeLeft, setTimeLeft] = useState<number>(pTimeLeft * 1000);
251-
252-
const percentage = (timeLeft / ttlMs) * 100;
253-
useEffect(() => {
254-
if (timeLeft === 0) return;
255-
const interval = setInterval(() => {
256-
setTimeLeft((prevTime) => {
257-
if (prevTime <= 100) {
258-
clearInterval(interval);
259-
onEnd();
260-
return 0;
261-
}
262-
return timeLeft - 100;
263-
});
264-
}, 100);
265-
266-
return () => clearInterval(interval);
267-
}, [onEnd, timeLeft]);
268-
269-
return (
270-
<>
271-
<Progress
272-
value={percentage}
273-
className="h-2 w-64"
274-
indicatorClassname={
275-
timeLeft < 31_000 ? "bg-red-600 dark:bg-red-400" : "bg-primary"
276-
}
277-
/>
278-
<span className="text-foreground/70 text-sm">
279-
Time left:
280-
<span className="inline-block w-10 text-end">
281-
{Math.floor(timeLeft / 1000)}s
282-
</span>
283-
</span>
284-
</>
285-
);
286-
}

src/app/layout.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { GeistSans } from "geist/font/sans";
22
import { type Metadata } from "next";
33
import "@/index.css";
44
import { TRPCReactProvider } from "@/lib/trpc/client";
5-
import { Header, HEADER_HEIGHT } from "@/components/header";
5+
import { HEADER_HEIGHT } from "@/components/header";
66
import { ThemeProvider } from "@/components/theme-provider";
7-
import { SidebarProvider } from "@/components/ui/sidebar";
87
import { Toaster } from "@/components/ui/sonner";
8+
import { TooltipProvider } from "@/components/ui/tooltip";
99

1010
const desc = "PoliNetwork Admin Dashboard";
1111

@@ -42,8 +42,10 @@ export default function RootLayout({
4242
storageKey="polinetwork_darkmode"
4343
disableTransitionOnChange
4444
>
45-
<TRPCReactProvider>{children}</TRPCReactProvider>
46-
<Toaster richColors position="bottom-right" />
45+
<TooltipProvider>
46+
<TRPCReactProvider>{children}</TRPCReactProvider>
47+
<Toaster richColors position="bottom-right" />
48+
</TooltipProvider>
4749
</ThemeProvider>
4850
</body>
4951
</html>

src/app/login/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export default async function SignInPage({
1111
searchParams: Promise<{ callbackUrl?: string }>;
1212
}) {
1313
const { callbackUrl } = await searchParams;
14-
const callbackURL = `${getBaseUrl()}${callbackUrl ?? "/dashboard"}`;
14+
const callbackURL = `${getBaseUrl()}${callbackUrl ?? "/login/success"}`;
1515

1616
const session = await getServerSession();
17-
if (session.data?.user) redirect("/dashboard");
17+
if (session.data?.user) redirect("/login/success");
1818

1919
return (
2020
<main className="text-accent container mx-auto flex grow flex-col items-center justify-start space-y-6 px-4 py-8">

src/app/login/success/page.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { redirect } from "next/navigation";
2+
import { getServerSession } from "@/server/auth";
3+
import { getQueryClient, trpc } from "@/lib/trpc/server";
4+
5+
export default async function LoginSuccess() {
6+
const session = await getServerSession();
7+
if (!session.data) redirect("/login");
8+
9+
const tgId = session.data.user.telegramId
10+
if (!tgId) redirect("/onboarding/link");
11+
// if (session?.user.role === USER_ROLE.INACTIVE) ;
12+
// if (session?.user.role === USER_ROLE.DISABLED) redirect("/dashboard/disabled");
13+
14+
const qc = getQueryClient()
15+
const { role } = await qc.fetchQuery(trpc.tg.permissions.getRole.queryOptions({ userId: tgId }))
16+
if (role === "user") redirect("/onboarding/no-role")
17+
if (role === "creator") redirect("/onboarding/unauthorized")
18+
return redirect("/dashboard")
19+
}

src/app/onboarding/layout.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Header } from "@/components/header";
2+
3+
export default function Layout({ children }: { children: React.ReactNode }) {
4+
return (
5+
<div className="scrollbar scrollbar-w-2 scrollbar-track-card scrollbar-thumb-white/20 flex h-screen w-full flex-col items-center justify-start overflow-y-auto">
6+
<Header />
7+
{children}
8+
</div>
9+
);
10+
}

src/app/onboarding/link/page.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Image from "next/image";
2+
import { TelegramLink } from "./telegram";
3+
import loginSvg2 from "@/assets/svg/login-2.svg";
4+
import { Card } from "@/components/ui/card";
5+
import { getServerSession } from "@/server/auth";
6+
import { redirect } from "next/navigation";
7+
import { env } from "@/env";
8+
9+
const BOT_USERNAME =
10+
env.NODE_ENV === "production" ? "pn_ts_dev_bot" : "pn_ts_devlocal_bot";
11+
12+
export default async function OnboardingLink() {
13+
const { data } = await getServerSession();
14+
if (data?.user.telegramId) redirect("/login/success");
15+
16+
return (
17+
<main className="grid grow place-content-center">
18+
<Card className="relative grid h-140 min-w-120 grow grid-rows-[5fr_auto_4fr] items-center">
19+
<div className="flex flex-col gap-y-4 place-self-center justify-self-center">
20+
<Image
21+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
22+
src={loginSvg2}
23+
width={200}
24+
alt="insert credentials"
25+
/>
26+
<div className="text-center">
27+
<p className="text-primary text-lg font-bold dark:text-white">
28+
Link your Telegram account
29+
</p>
30+
<p className="text-muted-foreground text-sm">
31+
This allows to verify your role
32+
</p>
33+
</div>
34+
</div>
35+
<hr />
36+
<TelegramLink botUsername={BOT_USERNAME} />
37+
</Card>
38+
</main>
39+
);
40+
}

0 commit comments

Comments
 (0)