fix(web,bot): drop next-themes, extend QR validity, fix retry CTA
next-themes hydration mismatch - Removed the next-themes wrapper, ThemeProvider component, and the Settings appearance card — there's no theme-toggle UI anywhere in the app, so the library was just adding a pre-hydration `<script>` that triggered React 19's "script tag while rendering" warning and the `<html>` class swap caused the hydration mismatch. - Sonner Toaster now uses a fixed `theme="light"` instead of useTheme. - Layout drops `suppressHydrationWarning` on `<html>` since we no longer mutate it on mount. QR refs exhausted before the user could scan - Pass `qrTimeout: 60_000` to makeWASocket so each QR (first AND subsequent) lasts a full minute. Default was 60 s for the first and 20 s for each subsequent → ~6 refs × default = ~2.5 min before Baileys gave up. With 60 s flat, the user has the full ~5 min window matching pair-handler's PAIR_TIMEOUT_MS. Pairing-timed-out screen - "Try again" used to link to /accounts/new (creates a new account instead of re-pairing the existing one). Link now points to the existing /accounts/[id] detail page where the operator can hit Re-pair. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d10c72551
commit
234e8aa690
@ -46,6 +46,11 @@ export async function startSession(params: {
|
|||||||
auth: state,
|
auth: state,
|
||||||
browser: Browsers.macOS("Safari"),
|
browser: Browsers.macOS("Safari"),
|
||||||
syncFullHistory: false,
|
syncFullHistory: false,
|
||||||
|
// Default: 60 s for the first QR, 20 s for subsequent ones (~5 refs ≈
|
||||||
|
// 2.5 min total before Baileys gives up). Set both legs to 60 s so
|
||||||
|
// the operator has the full ~5 min pairing window to scan, matching
|
||||||
|
// pair-handler's PAIR_TIMEOUT_MS.
|
||||||
|
qrTimeout: 60_000,
|
||||||
logger: logger.child({ accountId, component: "baileys" }) as never,
|
logger: logger.child({ accountId, component: "baileys" }) as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { GeistSans } from "geist/font/sans";
|
import { GeistSans } from "geist/font/sans";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
|
||||||
import { AppShell } from "@/components/app-shell";
|
import { AppShell } from "@/components/app-shell";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
@ -21,12 +20,13 @@ export const viewport: Viewport = {
|
|||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning className={GeistSans.className}>
|
// Light theme is the only theme — no client-side theme switcher and
|
||||||
|
// therefore no pre-hydration `<script>` from a theme library. That
|
||||||
|
// avoids the "script tag while rendering" warning under React 19.
|
||||||
|
<html lang="en" className={GeistSans.className}>
|
||||||
<body>
|
<body>
|
||||||
<ThemeProvider>
|
<AppShell>{children}</AppShell>
|
||||||
<AppShell>{children}</AppShell>
|
<Toaster richColors position="top-right" />
|
||||||
<Toaster richColors position="top-right" />
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { getSeededOperator } from "@/lib/operator";
|
import { getSeededOperator } from "@/lib/operator";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
|
||||||
|
|
||||||
export default async function SettingsPage() {
|
export default async function SettingsPage() {
|
||||||
const op = await getSeededOperator();
|
const op = await getSeededOperator();
|
||||||
@ -24,16 +23,6 @@ export default async function SettingsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Appearance</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex items-center justify-between">
|
|
||||||
<div className="text-sm text-muted-foreground">Theme</div>
|
|
||||||
<ThemeToggle />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<p className="text-center text-xs text-muted-foreground">
|
<p className="text-center text-xs text-muted-foreground">
|
||||||
cm WhatsApp Bot · self-hosted
|
cm WhatsApp Bot · self-hosted
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -207,12 +207,14 @@ export function PairLive({ accountId, label }: PairLiveProps) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-base font-semibold">Pairing timed out</p>
|
<p className="text-base font-semibold">Pairing timed out</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
The QR code expired before a device was linked.
|
The QR window closed before a device was linked.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild size="sm">
|
<Button asChild size="sm">
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||||
<Link href={"/accounts/new" as any}>Try again</Link>
|
<Link href={`/accounts/${accountId}` as any}>
|
||||||
|
Back to account — try again
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
|
|
||||||
type ThemeProviderProps = ComponentProps<typeof NextThemesProvider>;
|
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
||||||
return (
|
|
||||||
<NextThemesProvider
|
|
||||||
attribute="class"
|
|
||||||
defaultTheme="system"
|
|
||||||
enableSystem
|
|
||||||
disableTransitionOnChange
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</NextThemesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import { Moon, Sun, Monitor } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
export function ThemeToggle() {
|
|
||||||
const { setTheme, theme } = useTheme();
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
{theme === "dark" ? (
|
|
||||||
<Moon className="size-4" />
|
|
||||||
) : theme === "light" ? (
|
|
||||||
<Sun className="size-4" />
|
|
||||||
) : (
|
|
||||||
<Monitor className="size-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-2 capitalize">{theme ?? "system"}</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
||||||
<Sun className="mr-2 size-4" /> Light
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
||||||
<Moon className="mr-2 size-4" /> Dark
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
||||||
<Monitor className="mr-2 size-4" /> System
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,15 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme="light"
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
icons={{
|
icons={{
|
||||||
success: (
|
success: (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user