diff --git a/.prettierignore b/.prettierignore
index 52c3fe144..41647210d 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,4 +1,8 @@
database.types.ts
playwright-report
*.hbs
-*.md
\ No newline at end of file
+*.md
+dist
+build
+.next
+next-env.d.ts
\ No newline at end of file
diff --git a/apps/dev-tool/app/lib/connectivity-service.ts b/apps/dev-tool/app/lib/connectivity-service.ts
index 921674e5c..16eaed136 100644
--- a/apps/dev-tool/app/lib/connectivity-service.ts
+++ b/apps/dev-tool/app/lib/connectivity-service.ts
@@ -19,10 +19,9 @@ class ConnectivityService {
};
}
- const anonKey = await getVariable(
- 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
- this.mode,
- );
+ const anonKey =
+ (await getVariable('NEXT_PUBLIC_SUPABASE_ANON_KEY', this.mode)) ||
+ (await getVariable('NEXT_PUBLIC_SUPABASE_PUBLIC_KEY', this.mode));
if (!anonKey) {
return {
@@ -71,10 +70,9 @@ class ConnectivityService {
const endpoint = `${url}/rest/v1/accounts`;
- const apikey = await getVariable(
- 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
- this.mode,
- );
+ const apikey =
+ (await getVariable('NEXT_PUBLIC_SUPABASE_ANON_KEY', this.mode)) ||
+ (await getVariable('NEXT_PUBLIC_SUPABASE_PUBLIC_KEY', this.mode));
if (!apikey) {
return {
@@ -83,7 +81,9 @@ class ConnectivityService {
};
}
- const adminKey = await getVariable('SUPABASE_SERVICE_ROLE_KEY', this.mode);
+ const adminKey =
+ (await getVariable('SUPABASE_SERVICE_ROLE_KEY', this.mode)) ||
+ (await getVariable('SUPABASE_SECRET_KEY', this.mode));
if (!adminKey) {
return {
diff --git a/apps/dev-tool/app/translations/lib/translations-loader.ts b/apps/dev-tool/app/translations/lib/translations-loader.ts
index bc75e6d14..1d90adb5d 100644
--- a/apps/dev-tool/app/translations/lib/translations-loader.ts
+++ b/apps/dev-tool/app/translations/lib/translations-loader.ts
@@ -13,7 +13,12 @@ export type Translations = {
export async function loadTranslations() {
const localesPath = join(process.cwd(), '../web/public/locales');
- const locales = readdirSync(localesPath);
+ const localesDirents = readdirSync(localesPath, { withFileTypes: true });
+
+ const locales = localesDirents
+ .filter((dirent) => dirent.isDirectory())
+ .map((dirent) => dirent.name);
+
const translations: Translations = {};
for (const locale of locales) {
diff --git a/apps/dev-tool/next.config.ts b/apps/dev-tool/next.config.ts
index aa34a5fdb..d26e2e010 100644
--- a/apps/dev-tool/next.config.ts
+++ b/apps/dev-tool/next.config.ts
@@ -3,9 +3,7 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
transpilePackages: ['@kit/ui', '@kit/shared'],
- experimental: {
- reactCompiler: true,
- },
+ reactCompiler: true,
devIndicators: {
position: 'bottom-right',
},
diff --git a/apps/dev-tool/package.json b/apps/dev-tool/package.json
index 74655d556..a42dd491c 100644
--- a/apps/dev-tool/package.json
+++ b/apps/dev-tool/package.json
@@ -4,20 +4,20 @@
"private": true,
"scripts": {
"clean": "git clean -xdf .next .turbo node_modules",
- "dev": "next dev --turbo --port=3010 | pino-pretty -c",
- "format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
+ "dev": "next dev --port=3010 | pino-pretty -c",
+ "format": "prettier --check --write \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\""
},
"dependencies": {
- "@ai-sdk/openai": "^2.0.42",
- "@faker-js/faker": "^10.0.0",
+ "@ai-sdk/openai": "^2.0.53",
+ "@faker-js/faker": "^10.1.0",
"@hookform/resolvers": "^5.2.2",
- "@tanstack/react-query": "5.90.2",
- "ai": "5.0.59",
- "lucide-react": "^0.544.0",
- "next": "15.5.5",
- "nodemailer": "^7.0.6",
- "react": "19.1.1",
- "react-dom": "19.1.1",
+ "@tanstack/react-query": "5.90.5",
+ "ai": "5.0.76",
+ "lucide-react": "^0.546.0",
+ "next": "16.0.0",
+ "nodemailer": "^7.0.9",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
"rxjs": "^7.8.2"
},
"devDependencies": {
@@ -29,16 +29,16 @@
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
- "@tailwindcss/postcss": "^4.1.14",
- "@types/node": "^24.6.2",
+ "@tailwindcss/postcss": "^4.1.15",
+ "@types/node": "catalog:",
"@types/nodemailer": "7.0.2",
- "@types/react": "19.1.16",
- "@types/react-dom": "19.1.9",
- "babel-plugin-react-compiler": "19.1.0-rc.3",
- "pino-pretty": "13.0.0",
- "react-hook-form": "^7.63.0",
+ "@types/react": "catalog:",
+ "@types/react-dom": "19.2.2",
+ "babel-plugin-react-compiler": "1.0.0",
+ "pino-pretty": "13.1.2",
+ "react-hook-form": "^7.65.0",
"recharts": "2.15.3",
- "tailwindcss": "4.1.14",
+ "tailwindcss": "4.1.15",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.3",
"zod": "^3.25.74"
diff --git a/apps/e2e/package.json b/apps/e2e/package.json
index 874e1830c..99183a19d 100644
--- a/apps/e2e/package.json
+++ b/apps/e2e/package.json
@@ -11,9 +11,9 @@
},
"author": "Makerkit",
"devDependencies": {
- "@playwright/test": "^1.55.1",
- "@supabase/supabase-js": "2.58.0",
- "@types/node": "^24.6.2",
+ "@playwright/test": "^1.56.1",
+ "@supabase/supabase-js": "2.76.1",
+ "@types/node": "catalog:",
"dotenv": "17.2.3",
"node-html-parser": "^7.0.1",
"totp-generator": "^2.0.0"
diff --git a/apps/e2e/tests/admin/admin.spec.ts b/apps/e2e/tests/admin/admin.spec.ts
index adc092604..eeb55a872 100644
--- a/apps/e2e/tests/admin/admin.spec.ts
+++ b/apps/e2e/tests/admin/admin.spec.ts
@@ -103,9 +103,6 @@ test.describe('Admin', () => {
),
]);
- // TODO: remove when https://github.com/makerkit/next-supabase-saas-kit-turbo/issues/356 is solved
- await page.reload();
-
await expect(page.getByText('Banned').first()).toBeVisible();
await page.context().clearCookies();
@@ -154,9 +151,6 @@ test.describe('Admin', () => {
await page.waitForTimeout(250);
- // TODO: remove when https://github.com/makerkit/next-supabase-saas-kit-turbo/issues/356 is solved
- await page.reload();
-
// Verify ban badge is removed
await expect(page.getByText('Banned')).not.toBeVisible();
diff --git a/apps/e2e/tests/authentication/password-reset.spec.ts b/apps/e2e/tests/authentication/password-reset.spec.ts
index fffe2cab7..09fe87ff9 100644
--- a/apps/e2e/tests/authentication/password-reset.spec.ts
+++ b/apps/e2e/tests/authentication/password-reset.spec.ts
@@ -29,22 +29,15 @@ test.describe('Password Reset Flow', () => {
subject: 'Reset your password',
});
- await page.waitForURL('/update-password');
+ await page.waitForURL(new RegExp('/update-password?.*'));
await auth.updatePassword(newPassword);
- await page
- .locator('a', {
- hasText: 'Back to Home Page',
- })
- .click();
-
await page.waitForURL('/home');
}).toPass();
await page.context().clearCookies();
-
- await page.waitForURL('/');
+ await page.reload();
await page.goto('/auth/sign-in');
await auth.loginAsUser({
diff --git a/apps/e2e/tests/invitations/invitations.po.ts b/apps/e2e/tests/invitations/invitations.po.ts
index ddf6efc13..30da37006 100644
--- a/apps/e2e/tests/invitations/invitations.po.ts
+++ b/apps/e2e/tests/invitations/invitations.po.ts
@@ -115,19 +115,37 @@ export class InvitationsPageObject {
async acceptInvitation() {
console.log('Accepting invitation...');
- await Promise.all([
- this.page
- .locator('[data-test="join-team-form"] button[type="submit"]')
- .click(),
- this.page.waitForResponse((response) => {
- return (
- response.url().includes('/join') &&
- response.request().method() === 'POST'
- );
- }),
- ]);
+ const click = this.page
+ .locator('[data-test="join-team-form"] button[type="submit"]')
+ .click();
- console.log('Invitation accepted');
+ const response = this.page.waitForResponse((response) => {
+ return (
+ response.url().includes('/join') &&
+ response.request().method() === 'POST'
+ );
+ });
+
+ await Promise.all([click, response]);
+
+ // wait for animation to complete
+ await this.page.waitForTimeout(500);
+
+ // skip authentication setup
+ const skipIdentitiesButton = this.page.locator(
+ '[data-test="skip-identities-button"]',
+ );
+
+ if (
+ await skipIdentitiesButton.isVisible({
+ timeout: 1000,
+ })
+ ) {
+ await skipIdentitiesButton.click();
+ }
+
+ // wait for redirect to account home
+ await this.page.waitForURL(new RegExp('/home/[a-z0-9-]+'));
}
private getInviteForm() {
diff --git a/apps/web/app/(marketing)/_components/site-header-account-section.tsx b/apps/web/app/(marketing)/_components/site-header-account-section.tsx
index 573dc557e..ac688079a 100644
--- a/apps/web/app/(marketing)/_components/site-header-account-section.tsx
+++ b/apps/web/app/(marketing)/_components/site-header-account-section.tsx
@@ -21,10 +21,12 @@ const ModeToggle = dynamic(
{ ssr: false },
);
-const MobileModeToggle = dynamic(() =>
- import('@kit/ui/mobile-mode-toggle').then((mod) => ({
- default: mod.MobileModeToggle,
- })),
+const MobileModeToggle = dynamic(
+ () =>
+ import('@kit/ui/mobile-mode-toggle').then((mod) => ({
+ default: mod.MobileModeToggle,
+ })),
+ { ssr: false },
);
const paths = {
diff --git a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx
index a6cd90549..e565fd263 100644
--- a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx
+++ b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx
@@ -29,54 +29,76 @@ function Node({
const url = `${prefix}/${node.slug}`;
const label = node.label ? node.label : node.title;
- const Container = (props: React.PropsWithChildren) => {
- if (node.collapsible) {
- return (
-
- {props.children}
-
- );
- }
-
- return props.children;
- };
-
- const ContentContainer = (props: React.PropsWithChildren) => {
- if (node.collapsible) {
- return {props.children};
- }
-
- return props.children;
- };
-
- const Trigger = () => {
- if (node.collapsible) {
- return (
-
-
-
- {label}
-
-
-
-
- );
- }
-
- return ;
- };
-
return (
-
-
+
+
-
+
-
-
+
+
);
}
+function NodeContentContainer({
+ node,
+ children,
+}: {
+ node: Cms.ContentItem;
+ children: React.ReactNode;
+}) {
+ if (node.collapsible) {
+ return {children};
+ }
+
+ return children;
+}
+
+function NodeContainer({
+ node,
+ prefix,
+ children,
+}: {
+ node: Cms.ContentItem;
+ prefix: string;
+ children: React.ReactNode;
+}) {
+ if (node.collapsible) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return children;
+}
+
+function NodeTrigger({
+ node,
+ label,
+ url,
+}: {
+ node: Cms.ContentItem;
+ label: string;
+ url: string;
+}) {
+ if (node.collapsible) {
+ return (
+
+
+
+ {label}
+
+
+
+
+ );
+ }
+
+ return ;
+}
+
function Tree({
pages,
level,
diff --git a/apps/web/app/(marketing)/docs/_components/floating-docs-navigation.tsx b/apps/web/app/(marketing)/docs/_components/floating-docs-navigation.tsx
index e6063a6c5..53936bb64 100644
--- a/apps/web/app/(marketing)/docs/_components/floating-docs-navigation.tsx
+++ b/apps/web/app/(marketing)/docs/_components/floating-docs-navigation.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useEffectEvent, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation';
@@ -21,27 +21,26 @@ export function FloatingDocumentationNavigation(
const [isVisible, setIsVisible] = useState(false);
- const enableScrolling = (element: HTMLElement) =>
- (element.style.overflowY = '');
+ const enableScrolling = useEffectEvent(
+ () => body && (body.style.overflowY = ''),
+ );
- const disableScrolling = (element: HTMLElement) =>
- (element.style.overflowY = 'hidden');
+ const disableScrolling = useEffectEvent(
+ () => body && (body.style.overflowY = 'hidden'),
+ );
// enable/disable body scrolling when the docs are toggled
useEffect(() => {
- if (!body) {
- return;
- }
-
if (isVisible) {
- disableScrolling(body);
+ disableScrolling();
} else {
- enableScrolling(body);
+ enableScrolling();
}
- }, [isVisible, body]);
+ }, [isVisible]);
// hide docs when navigating to another page
useEffect(() => {
+ // eslint-disable-next-line react-hooks/set-state-in-effect
setIsVisible(false);
}, [activePath]);
diff --git a/apps/web/app/auth/sign-in/page.tsx b/apps/web/app/auth/sign-in/page.tsx
index 522738810..015c3c778 100644
--- a/apps/web/app/auth/sign-in/page.tsx
+++ b/apps/web/app/auth/sign-in/page.tsx
@@ -12,7 +12,6 @@ import { withI18n } from '~/lib/i18n/with-i18n';
interface SignInPageProps {
searchParams: Promise<{
- invite_token?: string;
next?: string;
}>;
}
diff --git a/apps/web/app/global-error.tsx b/apps/web/app/global-error.tsx
index d62500474..afb172435 100644
--- a/apps/web/app/global-error.tsx
+++ b/apps/web/app/global-error.tsx
@@ -26,14 +26,14 @@ const GlobalErrorPage = ({
-
+
);
};
-function GlobalErrorPageContent({ reset }: { reset: () => void }) {
+function GlobalErrorContent({ reset }: { reset: () => void }) {
const user = useUser();
return (
diff --git a/apps/web/app/home/(user)/settings/page.tsx b/apps/web/app/home/(user)/settings/page.tsx
index 29adf4a6d..e09806e51 100644
--- a/apps/web/app/home/(user)/settings/page.tsx
+++ b/apps/web/app/home/(user)/settings/page.tsx
@@ -19,10 +19,10 @@ const features = {
const providers = authConfig.providers.oAuth;
const callbackPath = pathsConfig.auth.callback;
-const accountHomePath = pathsConfig.app.accountHome;
+const accountSettingsPath = pathsConfig.app.accountSettings;
const paths = {
- callback: callbackPath + `?next=${accountHomePath}`,
+ callback: callbackPath + `?next=${accountSettingsPath}`,
};
export const generateMetadata = async () => {
diff --git a/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx b/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx
index 4eb97476a..b7ed8f43a 100644
--- a/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx
+++ b/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx
@@ -64,7 +64,6 @@ export function TeamAccountNavigationMenu(props: {
diff --git a/apps/web/app/home/[account]/billing/page.tsx b/apps/web/app/home/[account]/billing/page.tsx
index 5b2838111..cd291cfbd 100644
--- a/apps/web/app/home/[account]/billing/page.tsx
+++ b/apps/web/app/home/[account]/billing/page.tsx
@@ -61,30 +61,7 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const canManageBilling =
workspace.account.permissions.includes('billing.manage');
- const Checkout = () => {
- if (!canManageBilling) {
- return ;
- }
-
- return (
-
- );
- };
-
- const BillingPortal = () => {
- if (!canManageBilling || !customerId) {
- return null;
- }
-
- return (
-
- );
- };
+ const shouldShowBillingPortal = canManageBilling && customerId;
return (
<>
@@ -97,7 +74,15 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
-
+ }
+ >
+
+
@@ -124,7 +109,9 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
}}
-
+ {shouldShowBillingPortal ? (
+
+ ) : null}
>
@@ -148,3 +135,20 @@ function CannotManageBillingAlert() {
);
}
+
+function BillingPortalForm({
+ accountId,
+ account,
+}: {
+ accountId: string;
+ account: string;
+}) {
+ return (
+
+ );
+}
diff --git a/apps/web/app/identities/page.tsx b/apps/web/app/identities/page.tsx
new file mode 100644
index 000000000..4ea49a1cb
--- /dev/null
+++ b/apps/web/app/identities/page.tsx
@@ -0,0 +1,152 @@
+import { Metadata } from 'next';
+
+import Link from 'next/link';
+import { redirect } from 'next/navigation';
+
+import type { Provider } from '@supabase/supabase-js';
+
+import { LinkAccountsList } from '@kit/accounts/personal-account-settings';
+import { AuthLayoutShell } from '@kit/auth/shared';
+import { requireUser } from '@kit/supabase/require-user';
+import { getSupabaseServerClient } from '@kit/supabase/server-client';
+import { Button } from '@kit/ui/button';
+import { Heading } from '@kit/ui/heading';
+import { Trans } from '@kit/ui/trans';
+
+import { AppLogo } from '~/components/app-logo';
+import authConfig from '~/config/auth.config';
+import pathsConfig from '~/config/paths.config';
+import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
+import { withI18n } from '~/lib/i18n/with-i18n';
+
+export const meta = async (): Promise => {
+ const i18n = await createI18nServerInstance();
+
+ return {
+ title: i18n.t('auth:setupAccount'),
+ };
+};
+
+type IdentitiesPageProps = {
+ searchParams: Promise<{ next?: string }>;
+};
+
+/**
+ * @name IdentitiesPage
+ * @description Displays linked accounts and available authentication methods.
+ */
+async function IdentitiesPage(props: IdentitiesPageProps) {
+ const {
+ nextPath,
+ showPasswordOption,
+ showEmailOption,
+ oAuthProviders,
+ enableIdentityLinking,
+ } = await fetchData(props);
+
+ return (
+
+
+
+ );
+}
+
+export default withI18n(IdentitiesPage);
+
+/**
+ * @name IdentitiesStep
+ * @description Displays linked accounts and available authentication methods.
+ * LinkAccountsList component handles all authentication options including OAuth and Email/Password.
+ */
+function IdentitiesStep(props: {
+ nextPath: string;
+ showPasswordOption: boolean;
+ showEmailOption: boolean;
+ enableIdentityLinking: boolean;
+ oAuthProviders: Provider[];
+}) {
+ return (
+
+
+
+
+
+ );
+}
+
+async function fetchData(props: IdentitiesPageProps) {
+ const searchParams = await props.searchParams;
+ const client = getSupabaseServerClient();
+ const auth = await requireUser(client);
+
+ // If not authenticated, redirect to sign in
+ if (!auth.data) {
+ throw redirect(pathsConfig.auth.signIn);
+ }
+
+ // Get the next path from URL params (where to redirect after setup)
+ const nextPath = searchParams.next || pathsConfig.app.home;
+
+ // Available auth methods to add
+ const showPasswordOption = authConfig.providers.password;
+
+ // Show email option if password, magic link, or OTP is enabled
+ const showEmailOption =
+ authConfig.providers.password ||
+ authConfig.providers.magicLink ||
+ authConfig.providers.otp;
+
+ const oAuthProviders = authConfig.providers.oAuth;
+ const enableIdentityLinking = authConfig.enableIdentityLinking;
+
+ return {
+ nextPath,
+ showPasswordOption,
+ showEmailOption,
+ oAuthProviders,
+ enableIdentityLinking,
+ };
+}
diff --git a/apps/web/app/join/page.tsx b/apps/web/app/join/page.tsx
index 30acb5f8f..d391d9ddb 100644
--- a/apps/web/app/join/page.tsx
+++ b/apps/web/app/join/page.tsx
@@ -14,6 +14,7 @@ import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
import { AppLogo } from '~/components/app-logo';
+import authConfig from '~/config/auth.config';
import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
@@ -21,6 +22,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
interface JoinTeamAccountPageProps {
searchParams: Promise<{
invite_token?: string;
+ type?: 'invite' | 'magic-link';
email?: string;
}>;
}
@@ -127,6 +129,26 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
invitation.account.slug,
);
+ // Determine if we should show the account setup step (Step 2)
+ // Decision logic:
+ // 1. Only show for new accounts (linkType === 'invite')
+ // 2. Only if we have auth options available (password OR OAuth)
+ // 3. Users can always skip and set up auth later in account settings
+ const linkType = searchParams.type;
+ const supportsPasswordSignUp = authConfig.providers.password;
+ const supportsOAuthProviders = authConfig.providers.oAuth.length > 0;
+ const isNewAccount = linkType === 'invite';
+
+ const shouldSetupAccount =
+ isNewAccount && (supportsPasswordSignUp || supportsOAuthProviders);
+
+ // Determine redirect destination after joining:
+ // - If shouldSetupAccount: redirect to /identities with next param (Step 2)
+ // - Otherwise: redirect directly to team home (skip Step 2)
+ const nextPath = shouldSetupAccount
+ ? `/identities?next=${encodeURIComponent(accountHome)}`
+ : accountHome;
+
const email = auth.data.email ?? '';
return (
@@ -137,7 +159,7 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
invitation={invitation}
paths={{
signOutNext,
- accountHome,
+ nextPath,
}}
/>
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 20e10574e..47a8d354a 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -5,7 +5,7 @@ import { Toaster } from '@kit/ui/sonner';
import { RootProviders } from '~/components/root-providers';
import { getFontsClassName } from '~/lib/fonts';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
-import { generateRootMetadata } from '~/lib/root-metdata';
+import { generateRootMetadata } from '~/lib/root-metadata';
import { getRootTheme } from '~/lib/root-theme';
import '../styles/globals.css';
diff --git a/apps/web/components/analytics-provider.tsx b/apps/web/components/analytics-provider.tsx
index df12b694d..6f70f8b71 100644
--- a/apps/web/components/analytics-provider.tsx
+++ b/apps/web/components/analytics-provider.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useCallback, useEffect } from 'react';
+import { useCallback, useEffect, useEffectEvent } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
@@ -28,19 +28,38 @@ function useAnalyticsMapping(
) {
const appEvents = useAppEvents();
+ const subscribeToAppEvent = useEffectEvent(
+ (
+ eventType: AppEventType,
+ handler: (event: AppEvent>) => unknown,
+ ) => {
+ appEvents.on(eventType, handler);
+ },
+ );
+
+ const unsubscribeFromAppEvent = useEffectEvent(
+ (
+ eventType: AppEventType,
+ handler: (event: AppEvent>) => unknown,
+ ) => {
+ appEvents.off(eventType, handler);
+ },
+ );
+
useEffect(() => {
const subscriptions = Object.entries(mapping).map(
([eventType, handler]) => {
- appEvents.on(eventType as AppEventType, handler);
+ subscribeToAppEvent(eventType as AppEventType, handler);
- return () => appEvents.off(eventType as AppEventType, handler);
+ return () =>
+ unsubscribeFromAppEvent(eventType as AppEventType, handler);
},
);
return () => {
subscriptions.forEach((unsubscribe) => unsubscribe());
};
- }, [appEvents, mapping]);
+ }, [mapping]);
}
/**
@@ -96,9 +115,14 @@ function useReportPageView(reportAnalyticsFn: (url: string) => unknown) {
const pathname = usePathname();
const searchParams = useSearchParams();
- useEffect(() => {
+ const callAnalyticsOnPathChange = useEffectEvent(() => {
const url = [pathname, searchParams.toString()].filter(Boolean).join('?');
- reportAnalyticsFn(url);
- }, [pathname, reportAnalyticsFn, searchParams]);
+ return reportAnalyticsFn(url);
+ });
+
+ useEffect(() => {
+ callAnalyticsOnPathChange();
+ // call whenever the pathname changes
+ }, [pathname]);
}
diff --git a/apps/web/components/auth-provider.tsx b/apps/web/components/auth-provider.tsx
index 9efc5331a..fda62a3eb 100644
--- a/apps/web/components/auth-provider.tsx
+++ b/apps/web/components/auth-provider.tsx
@@ -8,8 +8,6 @@ import { useMonitoring } from '@kit/monitoring/hooks';
import { useAppEvents } from '@kit/shared/events';
import { useAuthChangeListener } from '@kit/supabase/hooks/use-auth-change-listener';
-import pathsConfig from '~/config/paths.config';
-
export function AuthProvider(props: React.PropsWithChildren) {
const dispatchEvent = useDispatchAppEventFromAuthEvent();
@@ -23,7 +21,6 @@ export function AuthProvider(props: React.PropsWithChildren) {
);
useAuthChangeListener({
- appHomePath: pathsConfig.app.home,
onEvent,
});
diff --git a/apps/web/lib/root-metdata.ts b/apps/web/lib/root-metadata.ts
similarity index 100%
rename from apps/web/lib/root-metdata.ts
rename to apps/web/lib/root-metadata.ts
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index 8755d8a61..6ee5f40ba 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -28,9 +28,7 @@ const config = {
reactStrictMode: true,
/** Enables hot reloading for local packages without a build step */
transpilePackages: INTERNAL_PACKAGES,
- images: {
- remotePatterns: getRemotePatterns(),
- },
+ images: getImagesConfig(),
logging: {
fetches: {
fullUrl: true,
@@ -52,10 +50,10 @@ const config = {
: {
position: 'bottom-right',
},
+ reactCompiler: ENABLE_REACT_COMPILER,
experimental: {
mdxRs: true,
- reactCompiler: ENABLE_REACT_COMPILER,
- clientSegmentCache: true,
+ turbopackFileSystemCacheForDev: true,
optimizePackageImports: [
'recharts',
'lucide-react',
@@ -72,7 +70,6 @@ const config = {
},
},
/** We already do linting and typechecking as separate tasks in CI */
- eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
};
@@ -80,8 +77,8 @@ export default withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})(config);
-function getRemotePatterns() {
- /** @type {import('next').NextConfig['remotePatterns']} */
+/** @returns {import('next').NextConfig['images']} */
+function getImagesConfig() {
const remotePatterns = [];
if (SUPABASE_URL) {
@@ -93,18 +90,28 @@ function getRemotePatterns() {
});
}
- return IS_PRODUCTION
- ? remotePatterns
- : [
- {
- protocol: 'http',
- hostname: '127.0.0.1',
- },
- {
- protocol: 'http',
- hostname: 'localhost',
- },
- ];
+ if (IS_PRODUCTION) {
+ return {
+ remotePatterns,
+ };
+ }
+
+ remotePatterns.push(
+ ...[
+ {
+ protocol: 'http',
+ hostname: '127.0.0.1',
+ },
+ {
+ protocol: 'http',
+ hostname: 'localhost',
+ },
+ ],
+ );
+
+ return {
+ remotePatterns,
+ };
}
async function getRedirects() {
diff --git a/apps/web/package.json b/apps/web/package.json
index ae2f91789..abab05079 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -7,12 +7,12 @@
"scripts": {
"analyze": "ANALYZE=true pnpm run build",
"build": "next build",
- "build:test": "NODE_ENV=test next build --turbopack",
+ "build:test": "NODE_ENV=test next build",
"clean": "git clean -xdf .next .turbo node_modules",
- "dev": "next dev --turbo | pino-pretty -c",
+ "dev": "next dev | pino-pretty -c",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
- "format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"",
+ "format": "prettier --check \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\"",
"start": "next start",
"start:test": "NODE_ENV=test next start",
"typecheck": "tsc --noEmit",
@@ -54,20 +54,20 @@
"@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
"@marsidev/react-turnstile": "^1.3.1",
- "@nosecone/next": "1.0.0-beta.12",
+ "@nosecone/next": "1.0.0-beta.13",
"@radix-ui/react-icons": "^1.3.2",
- "@supabase/supabase-js": "2.58.0",
- "@tanstack/react-query": "5.90.2",
+ "@supabase/supabase-js": "2.76.1",
+ "@tanstack/react-query": "5.90.5",
"@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0",
- "lucide-react": "^0.544.0",
- "next": "15.5.5",
+ "lucide-react": "^0.546.0",
+ "next": "16.0.0",
"next-sitemap": "^4.2.3",
"next-themes": "0.4.6",
- "react": "19.1.1",
- "react-dom": "19.1.1",
- "react-hook-form": "^7.63.0",
- "react-i18next": "^16.0.0",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "react-hook-form": "^7.65.0",
+ "react-i18next": "^16.1.4",
"recharts": "2.15.3",
"tailwind-merge": "^3.3.1",
"zod": "^3.25.74"
@@ -76,17 +76,17 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
- "@next/bundle-analyzer": "15.5.5",
- "@tailwindcss/postcss": "^4.1.14",
- "@types/node": "^24.6.2",
- "@types/react": "19.1.16",
- "@types/react-dom": "19.1.9",
- "babel-plugin-react-compiler": "19.1.0-rc.3",
+ "@next/bundle-analyzer": "16.0.0",
+ "@tailwindcss/postcss": "^4.1.15",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "19.2.2",
+ "babel-plugin-react-compiler": "1.0.0",
"cssnano": "^7.1.1",
- "pino-pretty": "13.0.0",
+ "pino-pretty": "13.1.2",
"prettier": "^3.6.2",
- "supabase": "2.48.3",
- "tailwindcss": "4.1.14",
+ "supabase": "2.53.6",
+ "tailwindcss": "4.1.15",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.3"
},
diff --git a/apps/web/middleware.ts b/apps/web/proxy.ts
similarity index 97%
rename from apps/web/middleware.ts
rename to apps/web/proxy.ts
index 4762c71a2..63174f90d 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/proxy.ts
@@ -23,7 +23,7 @@ const getUser = (request: NextRequest, response: NextResponse) => {
return supabase.auth.getClaims();
};
-export async function middleware(request: NextRequest) {
+export async function proxy(request: NextRequest) {
const secureHeaders = await createResponseWithSecureHeaders();
const response = NextResponse.next(secureHeaders);
@@ -164,9 +164,7 @@ function getPatterns() {
pattern: new URLPattern({ pathname: '/home/*?' }),
handler: async (req: NextRequest, res: NextResponse) => {
const { data } = await getUser(req, res);
-
- const origin = req.nextUrl.origin;
- const next = req.nextUrl.pathname;
+ const { origin, pathname: next } = req.nextUrl;
// If user is not logged in, redirect to sign in page.
if (!data?.claims) {
diff --git a/apps/web/public/locales/en/account.json b/apps/web/public/locales/en/account.json
index 91dda887a..ca67e24df 100644
--- a/apps/web/public/locales/en/account.json
+++ b/apps/web/public/locales/en/account.json
@@ -126,15 +126,24 @@
"accountLinked": "Account linked",
"unlinkAccount": "Unlink Account",
"failedToLinkAccount": "Failed to link account",
- "availableAccounts": "Available Accounts",
- "availableAccountsDescription": "Connect other authentication providers to your account",
- "alreadyLinkedAccountsDescription": "You have already linked these accounts",
+ "availableMethods": "Available Methods",
+ "availableMethodsDescription": "Connect your account to one or more of the following methods to sign in",
+ "linkedMethods": "Sign-in methods linked to your account",
+ "alreadyLinkedMethodsDescription": "You have already linked these accounts",
"confirmUnlinkAccount": "You are unlinking this provider.",
"unlinkAccountConfirmation": "Are you sure you want to unlink this provider from your account? This action cannot be undone.",
"unlinkingAccount": "Unlinking account...",
"accountUnlinked": "Account successfully unlinked",
"linkEmailPassword": "Email & Password",
- "linkEmailPasswordDescription": "Add an email and password to your account for additional sign-in options",
- "noAccountsAvailable": "No additional accounts available to link",
- "linkAccountDescription": "Link account to sign in with {{provider}}"
+ "linkEmailPasswordDescription": "Add password authentication to your account",
+ "noAccountsAvailable": "No other method is available at this time",
+ "linkAccountDescription": "Link account to sign in with {{provider}}",
+ "updatePasswordDescription": "Add password authentication to your account",
+ "setEmailAddress": "Set Email Address",
+ "setEmailDescription": "Add an email address to your account",
+ "setEmailSuccess": "Email set successfully",
+ "setEmailSuccessMessage": "We sent you an email to confirm your email address. Please check your inbox and click on the link to confirm your email address.",
+ "setEmailLoading": "Setting your email...",
+ "setEmailError": "Email not set. Please try again",
+ "emailNotChanged": "Your email address has not changed"
}
diff --git a/apps/web/public/locales/en/auth.json b/apps/web/public/locales/en/auth.json
index 1da985cf3..4709b5b75 100644
--- a/apps/web/public/locales/en/auth.json
+++ b/apps/web/public/locales/en/auth.json
@@ -78,6 +78,8 @@
"methodOauthWithProvider": "{{provider}}",
"methodDefault": "another method",
"existingAccountHint": "You previously signed in with {{method}}. Already have an account?",
+ "linkAccountToSignIn": "Link account to sign in",
+ "linkAccountToSignInDescription": "Add one or more sign-in methods to your account",
"errors": {
"Invalid login credentials": "The credentials entered are invalid",
"User already registered": "This credential is already in use. Please try with another one.",
diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json
index 12adeaa8b..ba269a664 100644
--- a/apps/web/public/locales/en/common.json
+++ b/apps/web/public/locales/en/common.json
@@ -41,8 +41,9 @@
"contactUs": "Contact Us",
"loading": "Loading. Please wait...",
"yourAccounts": "Your Accounts",
- "continue": "Continue",
+ "continueKey": "Continue",
"skip": "Skip",
+ "info": "Info",
"signedInAs": "Signed in as",
"pageOfPages": "Page {{page}} of {{total}}",
"showingRecordCount": "Showing {{pageSize}} of {{totalCount}} rows",
diff --git a/apps/web/public/locales/en/teams.json b/apps/web/public/locales/en/teams.json
index 21080823d..ef405e48d 100644
--- a/apps/web/public/locales/en/teams.json
+++ b/apps/web/public/locales/en/teams.json
@@ -151,8 +151,8 @@
"renewInvitationErrorDescription": "We encountered an error renewing the invitation. Please try again.",
"signInWithDifferentAccount": "Sign in with a different account",
"signInWithDifferentAccountDescription": "If you wish to accept the invitation with a different account, please sign out and back in with the account you wish to use.",
- "acceptInvitationHeading": "Accept Invitation to join {{accountName}}",
- "acceptInvitationDescription": "You have been invited to join the team {{accountName}}. If you wish to accept the invitation, please click the button below.",
+ "acceptInvitationHeading": "Join {{accountName}}",
+ "acceptInvitationDescription": "Click the button below to accept the invitation to join {{accountName}}",
"continueAs": "Continue as {{email}}",
"joinTeamAccount": "Join Team",
"joiningTeam": "Joining team...",
diff --git a/package.json b/package.json
index 02aee0533..c2e80638b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "next-supabase-saas-kit-turbo",
- "version": "2.18.3",
+ "version": "2.19.0",
"private": true,
"sideEffects": false,
"engines": {
@@ -37,7 +37,7 @@
"env:validate": "turbo gen validate-env"
},
"prettier": "@kit/prettier-config",
- "packageManager": "pnpm@10.17.1",
+ "packageManager": "pnpm@10.19.0",
"devDependencies": {
"@manypkg/cli": "^0.25.1",
"@turbo/gen": "^2.5.8",
diff --git a/packages/analytics/package.json b/packages/analytics/package.json
index a08864967..d12dc6631 100644
--- a/packages/analytics/package.json
+++ b/packages/analytics/package.json
@@ -17,7 +17,7 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
- "@types/node": "^24.6.2"
+ "@types/node": "catalog:"
},
"typesVersions": {
"*": {
diff --git a/packages/billing/gateway/package.json b/packages/billing/gateway/package.json
index 3dda03882..4d3206d86 100644
--- a/packages/billing/gateway/package.json
+++ b/packages/billing/gateway/package.json
@@ -26,14 +26,14 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
- "@supabase/supabase-js": "2.58.0",
- "@types/react": "19.1.16",
+ "@supabase/supabase-js": "2.76.1",
+ "@types/react": "catalog:",
"date-fns": "^4.1.0",
- "lucide-react": "^0.544.0",
- "next": "15.5.5",
- "react": "19.1.1",
- "react-hook-form": "^7.63.0",
- "react-i18next": "^16.0.0",
+ "lucide-react": "^0.546.0",
+ "next": "16.0.0",
+ "react": "19.2.0",
+ "react-hook-form": "^7.65.0",
+ "react-i18next": "^16.1.4",
"zod": "^3.25.74"
},
"typesVersions": {
diff --git a/packages/billing/gateway/src/components/embedded-checkout.tsx b/packages/billing/gateway/src/components/embedded-checkout.tsx
index 9b2540872..c681a126c 100644
--- a/packages/billing/gateway/src/components/embedded-checkout.tsx
+++ b/packages/billing/gateway/src/components/embedded-checkout.tsx
@@ -1,11 +1,27 @@
-import { Suspense, forwardRef, lazy, memo, useMemo } from 'react';
+import { Suspense, lazy } from 'react';
import { Enums } from '@kit/supabase/database';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
type BillingProvider = Enums<'billing_provider'>;
-const Fallback = ;
+// Create lazy components at module level (not during render)
+const StripeCheckoutLazy = lazy(async () => {
+ const { StripeCheckout } = await import('@kit/stripe/components');
+ return { default: StripeCheckout };
+});
+
+const LemonSqueezyCheckoutLazy = lazy(async () => {
+ const { LemonSqueezyEmbeddedCheckout } = await import(
+ '@kit/lemon-squeezy/components'
+ );
+ return { default: LemonSqueezyEmbeddedCheckout };
+});
+
+type CheckoutProps = {
+ onClose: (() => unknown) | undefined;
+ checkoutToken: string;
+};
export function EmbeddedCheckout(
props: React.PropsWithChildren<{
@@ -14,100 +30,54 @@ export function EmbeddedCheckout(
onClose?: () => void;
}>,
) {
- const CheckoutComponent = useMemo(
- () => loadCheckoutComponent(props.provider),
- [props.provider],
- );
-
return (
<>
-
+ }>
+
+
>
);
}
-function loadCheckoutComponent(provider: BillingProvider) {
- switch (provider) {
- case 'stripe': {
- return buildLazyComponent(() => {
- return import('@kit/stripe/components').then(({ StripeCheckout }) => {
- return {
- default: StripeCheckout,
- };
- });
- });
- }
-
- case 'lemon-squeezy': {
- return buildLazyComponent(() => {
- return import('@kit/lemon-squeezy/components').then(
- ({ LemonSqueezyEmbeddedCheckout }) => {
- return {
- default: LemonSqueezyEmbeddedCheckout,
- };
- },
- );
- });
- }
-
- case 'paddle': {
- throw new Error('Paddle is not yet supported');
- }
-
- default:
- throw new Error(`Unsupported provider: ${provider as string}`);
- }
-}
-
-function buildLazyComponent<
- Component extends React.ComponentType<{
- onClose: (() => unknown) | undefined;
- checkoutToken: string;
- }>,
->(
- load: () => Promise<{
- default: Component;
- }>,
- fallback = Fallback,
+function CheckoutSelector(
+ props: CheckoutProps & { provider: BillingProvider },
) {
- let LoadedComponent: ReturnType> | null = null;
-
- const LazyComponent = forwardRef<
- React.ElementRef<'div'>,
- {
- onClose: (() => unknown) | undefined;
- checkoutToken: string;
- }
- >(function LazyDynamicComponent(props, ref) {
- if (!LoadedComponent) {
- LoadedComponent = lazy(load);
- }
-
- return (
-
- {/* @ts-expect-error: weird TS */}
-
-
- );
- });
+ );
- return memo(LazyComponent);
+ case 'lemon-squeezy':
+ return (
+
+ );
+
+ case 'paddle':
+ throw new Error('Paddle is not yet supported');
+
+ default:
+ throw new Error(`Unsupported provider: ${props.provider as string}`);
+ }
}
function BlurryBackdrop() {
return (
diff --git a/packages/billing/gateway/src/components/plan-picker.tsx b/packages/billing/gateway/src/components/plan-picker.tsx
index b55b0be18..6653cd84f 100644
--- a/packages/billing/gateway/src/components/plan-picker.tsx
+++ b/packages/billing/gateway/src/components/plan-picker.tsx
@@ -316,7 +316,7 @@ export function PlanPicker(
@@ -415,6 +415,7 @@ function PlanDetails({
const isRecurring = selectedPlan.paymentType === 'recurring';
// trick to force animation on re-render
+ // eslint-disable-next-line react-hooks/purity
const key = Math.random();
return (
diff --git a/packages/billing/gateway/src/components/pricing-table.tsx b/packages/billing/gateway/src/components/pricing-table.tsx
index 7697b9db4..e64d27172 100644
--- a/packages/billing/gateway/src/components/pricing-table.tsx
+++ b/packages/billing/gateway/src/components/pricing-table.tsx
@@ -422,7 +422,7 @@ function PlanIntervalSwitcher(
const selected = plan === props.interval;
const className = cn(
- 'animate-in fade-in !outline-hidden rounded-full transition-all focus:!ring-0',
+ 'animate-in fade-in rounded-full !outline-hidden transition-all focus:!ring-0',
{
'border-r-transparent': index === 0,
['hover:text-primary text-muted-foreground']: !selected,
diff --git a/packages/billing/lemon-squeezy/package.json b/packages/billing/lemon-squeezy/package.json
index 5af090177..a14629253 100644
--- a/packages/billing/lemon-squeezy/package.json
+++ b/packages/billing/lemon-squeezy/package.json
@@ -24,9 +24,9 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
- "@types/react": "19.1.16",
- "next": "15.5.5",
- "react": "19.1.1",
+ "@types/react": "catalog:",
+ "next": "16.0.0",
+ "react": "19.2.0",
"zod": "^3.25.74"
},
"typesVersions": {
diff --git a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts
index 327b8bed0..1a1938eda 100644
--- a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts
+++ b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-billing-strategy.service.ts
@@ -56,8 +56,6 @@ export class LemonSqueezyBillingStrategyService
const { data: response, error } = await createLemonSqueezyCheckout(params);
if (error ?? !response?.data.id) {
- console.log(error);
-
logger.error(
{
...ctx,
diff --git a/packages/billing/stripe/package.json b/packages/billing/stripe/package.json
index e649f2ea3..b76fe6c7b 100644
--- a/packages/billing/stripe/package.json
+++ b/packages/billing/stripe/package.json
@@ -15,9 +15,9 @@
"./components": "./src/components/index.ts"
},
"dependencies": {
- "@stripe/react-stripe-js": "^5.0.0",
- "@stripe/stripe-js": "^8.0.0",
- "stripe": "^19.0.0"
+ "@stripe/react-stripe-js": "^5.2.0",
+ "@stripe/stripe-js": "^8.1.0",
+ "stripe": "^19.1.0"
},
"devDependencies": {
"@kit/billing": "workspace:*",
@@ -27,10 +27,10 @@
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
- "@types/react": "19.1.16",
+ "@types/react": "catalog:",
"date-fns": "^4.1.0",
- "next": "15.5.5",
- "react": "19.1.1",
+ "next": "16.0.0",
+ "react": "19.2.0",
"zod": "^3.25.74"
},
"typesVersions": {
diff --git a/packages/cms/core/package.json b/packages/cms/core/package.json
index 3fe499a86..a74edfc33 100644
--- a/packages/cms/core/package.json
+++ b/packages/cms/core/package.json
@@ -20,7 +20,7 @@
"@kit/shared": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/wordpress": "workspace:*",
- "@types/node": "^24.6.2"
+ "@types/node": "catalog:"
},
"typesVersions": {
"*": {
diff --git a/packages/cms/keystatic/package.json b/packages/cms/keystatic/package.json
index 4b436dcc6..9372d95bd 100644
--- a/packages/cms/keystatic/package.json
+++ b/packages/cms/keystatic/package.json
@@ -26,9 +26,9 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
- "@types/node": "^24.6.2",
- "@types/react": "19.1.16",
- "react": "19.1.1",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "react": "19.2.0",
"zod": "^3.25.74"
},
"typesVersions": {
diff --git a/packages/cms/wordpress/package.json b/packages/cms/wordpress/package.json
index df970866e..0f09be6df 100644
--- a/packages/cms/wordpress/package.json
+++ b/packages/cms/wordpress/package.json
@@ -20,8 +20,8 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
- "@types/node": "^24.6.2",
- "@types/react": "19.1.16",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
"wp-types": "^4.68.1"
},
"typesVersions": {
diff --git a/packages/database-webhooks/package.json b/packages/database-webhooks/package.json
index 8d2dea594..175b2294d 100644
--- a/packages/database-webhooks/package.json
+++ b/packages/database-webhooks/package.json
@@ -21,7 +21,7 @@
"@kit/stripe": "workspace:*",
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
- "@supabase/supabase-js": "2.58.0",
+ "@supabase/supabase-js": "2.76.1",
"zod": "^3.25.74"
},
"typesVersions": {
diff --git a/packages/email-templates/package.json b/packages/email-templates/package.json
index 4633c1865..1b7de5774 100644
--- a/packages/email-templates/package.json
+++ b/packages/email-templates/package.json
@@ -13,7 +13,7 @@
".": "./src/index.ts"
},
"dependencies": {
- "@react-email/components": "0.5.5"
+ "@react-email/components": "0.5.7"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
diff --git a/packages/email-templates/src/emails/invite.email.tsx b/packages/email-templates/src/emails/invite.email.tsx
index e59ba7245..a55fcf4d7 100644
--- a/packages/email-templates/src/emails/invite.email.tsx
+++ b/packages/email-templates/src/emails/invite.email.tsx
@@ -103,7 +103,7 @@ export async function renderInviteEmail(props: Props) {
)}
-
+
diff --git a/packages/email-templates/src/emails/otp.email.tsx b/packages/email-templates/src/emails/otp.email.tsx
index ebb6986ea..534b6ce3b 100644
--- a/packages/email-templates/src/emails/otp.email.tsx
+++ b/packages/email-templates/src/emails/otp.email.tsx
@@ -69,9 +69,9 @@ export async function renderOtpEmail(props: Props) {
{otpText}
-
+
diff --git a/packages/features/accounts/package.json b/packages/features/accounts/package.json
index c2b0ec70a..e1f871305 100644
--- a/packages/features/accounts/package.json
+++ b/packages/features/accounts/package.json
@@ -34,17 +34,17 @@
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@radix-ui/react-icons": "^1.3.2",
- "@supabase/supabase-js": "2.58.0",
- "@tanstack/react-query": "5.90.2",
- "@types/react": "19.1.16",
- "@types/react-dom": "19.1.9",
- "lucide-react": "^0.544.0",
- "next": "15.5.5",
+ "@supabase/supabase-js": "2.76.1",
+ "@tanstack/react-query": "5.90.5",
+ "@types/react": "catalog:",
+ "@types/react-dom": "19.2.2",
+ "lucide-react": "^0.546.0",
+ "next": "16.0.0",
"next-themes": "0.4.6",
- "react": "19.1.1",
- "react-dom": "19.1.1",
- "react-hook-form": "^7.63.0",
- "react-i18next": "^16.0.0",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "react-hook-form": "^7.65.0",
+ "react-i18next": "^16.1.4",
"zod": "^3.25.74"
},
"prettier": "@kit/prettier-config",
diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx
index 195dc374e..349db393d 100644
--- a/packages/features/accounts/src/components/account-selector.tsx
+++ b/packages/features/accounts/src/components/account-selector.tsx
@@ -68,27 +68,9 @@ export function AccountSelector({
return selectedAccount ?? PERSONAL_ACCOUNT_SLUG;
}, [selectedAccount]);
- const Icon = (props: { item: string }) => {
- return (
-
- );
- };
-
const selected = accounts.find((account) => account.value === value);
const pictureUrl = personalData.data?.picture_url;
- const PersonalAccountAvatar = () =>
- pictureUrl ? (
-
- ) : (
-
- );
-
return (
<>
@@ -117,7 +99,7 @@ export function AccountSelector({
'gap-x-2': !collapsed,
})}
>
-
+
-
+
onAccountChange(undefined)}
value={PERSONAL_ACCOUNT_SLUG}
>
@@ -185,7 +168,7 @@ export function AccountSelector({
-
+
@@ -206,7 +189,7 @@ export function AccountSelector({
data-name={account.label}
data-slug={account.value}
className={cn(
- 'group my-1 flex justify-between transition-colors',
+ 'group my-1 flex justify-between shadow-none transition-colors',
{
['bg-muted']: value === account.value,
},
@@ -222,7 +205,7 @@ export function AccountSelector({
}}
>
-
+
))}
@@ -286,8 +269,24 @@ export function AccountSelector({
function UserAvatar(props: { pictureUrl?: string }) {
return (
-
+
);
}
+
+function Icon({ selected }: { selected: boolean }) {
+ return (
+
+ );
+}
+
+function PersonalAccountAvatar({ pictureUrl }: { pictureUrl?: string | null }) {
+ return pictureUrl ? (
+
+ ) : (
+
+ );
+}
diff --git a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx
index e20a58985..6c8a69948 100644
--- a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx
+++ b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx
@@ -156,23 +156,26 @@ export function PersonalAccountSettingsContainer(
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
diff --git a/packages/features/accounts/src/components/personal-account-settings/email/update-email-form.tsx b/packages/features/accounts/src/components/personal-account-settings/email/update-email-form.tsx
index 61137bc4d..f8fa94942 100644
--- a/packages/features/accounts/src/components/personal-account-settings/email/update-email-form.tsx
+++ b/packages/features/accounts/src/components/personal-account-settings/email/update-email-form.tsx
@@ -27,26 +27,46 @@ import { Trans } from '@kit/ui/trans';
import { UpdateEmailSchema } from '../../../schema/update-email.schema';
-function createEmailResolver(currentEmail: string, errorMessage: string) {
- return zodResolver(
- UpdateEmailSchema.withTranslation(errorMessage).refine((schema) => {
- return schema.email !== currentEmail;
- }),
- );
+function createEmailResolver(
+ currentEmail: string | null,
+ emailsNotMatchingMessage: string,
+ emailNotChangedMessage: string,
+) {
+ const schema = UpdateEmailSchema.withTranslation(emailsNotMatchingMessage);
+
+ // If there's a current email, ensure the new email is different
+ if (currentEmail) {
+ return zodResolver(
+ schema.refine(
+ (data) => {
+ return data.email !== currentEmail;
+ },
+ {
+ path: ['email'],
+ message: emailNotChangedMessage,
+ },
+ ),
+ );
+ }
+
+ // If no current email, just validate the schema
+ return zodResolver(schema);
}
export function UpdateEmailForm({
email,
callbackPath,
+ onSuccess,
}: {
- email: string;
+ email?: string | null;
callbackPath: string;
+ onSuccess?: () => void;
}) {
const { t } = useTranslation('account');
const updateUserMutation = useUpdateUser();
+ const isSettingEmail = !email;
const updateEmail = ({ email }: { email: string }) => {
- // then, we update the user's email address
const promise = async () => {
const redirectTo = new URL(
callbackPath,
@@ -54,17 +74,25 @@ export function UpdateEmailForm({
).toString();
await updateUserMutation.mutateAsync({ email, redirectTo });
+
+ if (onSuccess) {
+ onSuccess();
+ }
};
toast.promise(promise, {
- success: t(`updateEmailSuccess`),
- loading: t(`updateEmailLoading`),
- error: t(`updateEmailError`),
+ success: t(isSettingEmail ? 'setEmailSuccess' : 'updateEmailSuccess'),
+ loading: t(isSettingEmail ? 'setEmailLoading' : 'updateEmailLoading'),
+ error: t(isSettingEmail ? 'setEmailError' : 'updateEmailError'),
});
};
const form = useForm({
- resolver: createEmailResolver(email, t('emailNotMatching')),
+ resolver: createEmailResolver(
+ email ?? null,
+ t('emailNotMatching'),
+ t('emailNotChanged'),
+ ),
defaultValues: {
email: '',
repeatEmail: '',
@@ -83,11 +111,23 @@ export function UpdateEmailForm({
-
+
-
+
@@ -107,7 +147,11 @@ export function UpdateEmailForm({
data-test={'account-email-form-email-input'}
required
type={'email'}
- placeholder={t('account:newEmail')}
+ placeholder={t(
+ isSettingEmail
+ ? 'account:emailAddress'
+ : 'account:newEmail',
+ )}
{...field}
/>
@@ -147,7 +191,13 @@ export function UpdateEmailForm({
diff --git a/packages/features/accounts/src/components/personal-account-settings/link-accounts/link-accounts-list.tsx b/packages/features/accounts/src/components/personal-account-settings/link-accounts/link-accounts-list.tsx
index 3694c69ed..8048a0106 100644
--- a/packages/features/accounts/src/components/personal-account-settings/link-accounts/link-accounts-list.tsx
+++ b/packages/features/accounts/src/components/personal-account-settings/link-accounts/link-accounts-list.tsx
@@ -1,11 +1,14 @@
'use client';
+import { Suspense, useState } from 'react';
+
+import { usePathname } from 'next/navigation';
+
import type { Provider, UserIdentity } from '@supabase/supabase-js';
-import { CheckCircle } from 'lucide-react';
-
import { useLinkIdentityWithProvider } from '@kit/supabase/hooks/use-link-identity-with-provider';
import { useUnlinkUserIdentity } from '@kit/supabase/hooks/use-unlink-user-identity';
+import { useUser } from '@kit/supabase/hooks/use-user';
import { useUserIdentities } from '@kit/supabase/hooks/use-user-identities';
import {
AlertDialog,
@@ -19,6 +22,14 @@ import {
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@kit/ui/dialog';
import { If } from '@kit/ui/if';
import {
Item,
@@ -35,9 +46,21 @@ import { toast } from '@kit/ui/sonner';
import { Spinner } from '@kit/ui/spinner';
import { Trans } from '@kit/ui/trans';
-export function LinkAccountsList(props: { providers: Provider[] }) {
+import { UpdateEmailForm } from '../email/update-email-form';
+import { UpdatePasswordForm } from '../password/update-password-form';
+
+interface LinkAccountsListProps {
+ providers: Provider[];
+ showPasswordOption?: boolean;
+ showEmailOption?: boolean;
+ enabled?: boolean;
+ redirectTo?: string;
+}
+
+export function LinkAccountsList(props: LinkAccountsListProps) {
const unlinkMutation = useUnlinkUserIdentity();
const linkMutation = useLinkIdentityWithProvider();
+ const pathname = usePathname();
const {
identities,
@@ -46,14 +69,40 @@ export function LinkAccountsList(props: { providers: Provider[] }) {
isLoading: isLoadingIdentities,
} = useUserIdentities();
- // Only show providers from the allowed list that aren't already connected
- const availableProviders = props.providers.filter(
- (provider) => !isProviderConnected(provider),
+ // Get user email from email identity
+ const emailIdentity = identities.find(
+ (identity) => identity.provider === 'email',
+ );
+
+ const userEmail = (emailIdentity?.identity_data?.email as string) || '';
+
+ // If enabled, display available providers
+ const availableProviders = props.enabled
+ ? props.providers.filter((provider) => !isProviderConnected(provider))
+ : [];
+
+ const user = useUser();
+ const amr = user.data ? user.data.amr : [];
+
+ const isConnectedWithPassword = amr.some(
+ (item: { method: string }) => item.method === 'password',
);
// Show all connected identities, even if their provider isn't in the allowed providers list
const connectedIdentities = identities;
+ const canLinkEmailAccount = !emailIdentity && props.showEmailOption;
+
+ const canLinkPassword =
+ emailIdentity && props.showPasswordOption && !isConnectedWithPassword;
+
+ const shouldDisplayAvailableAccountsSection =
+ canLinkEmailAccount || canLinkPassword || availableProviders.length;
+
+ /**
+ * @name handleUnlinkAccount
+ * @param identity
+ */
const handleUnlinkAccount = (identity: UserIdentity) => {
const promise = unlinkMutation.mutateAsync(identity);
@@ -64,6 +113,10 @@ export function LinkAccountsList(props: { providers: Provider[] }) {
});
};
+ /**
+ * @name handleLinkAccount
+ * @param provider
+ */
const handleLinkAccount = (provider: Provider) => {
const promise = linkMutation.mutateAsync(provider);
@@ -83,33 +136,32 @@ export function LinkAccountsList(props: { providers: Provider[] }) {
}
return (
-
- {/* Linked Accounts Section */}
+
0}>
-
+
{connectedIdentities.map((identity) => (
-
-
+
-
-
+
+
+
-
+
-
-
-
+
{identity.provider}
@@ -174,22 +226,35 @@ export function LinkAccountsList(props: { providers: Provider[] }) {
- {/* Available Accounts Section */}
- 0}>
+ }
+ >
-
+
+
+
+
+
+
+
+
+
{availableProviders.map((provider) => (
-
-
-
-
-
-
-
);
}
+
+function NoAccountsAvailable() {
+ return (
+
+
+
+
+
+ );
+}
+
+function UpdateEmailDialog(props: { redirectTo: string }) {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+ }
+ >
+ {
+ setOpen(false);
+ }}
+ />
+
+
+
+ );
+}
+
+function UpdatePasswordDialog(props: {
+ redirectTo: string;
+ userEmail: string;
+}) {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+ }
+ >
+
{
+ setOpen(false);
+ }}
+ />
+
+
+
+ );
+}
diff --git a/packages/features/accounts/src/components/personal-account-settings/password/update-password-form.tsx b/packages/features/accounts/src/components/personal-account-settings/password/update-password-form.tsx
index 77cc2127f..7c0092552 100644
--- a/packages/features/accounts/src/components/personal-account-settings/password/update-password-form.tsx
+++ b/packages/features/accounts/src/components/personal-account-settings/password/update-password-form.tsx
@@ -2,9 +2,11 @@
import { useState } from 'react';
+import type { PostgrestError } from '@supabase/supabase-js';
+
import { zodResolver } from '@hookform/resolvers/zod';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
-import { Check, Lock } from 'lucide-react';
+import { Check, Lock, XIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -33,9 +35,11 @@ import { PasswordUpdateSchema } from '../../../schema/update-password.schema';
export const UpdatePasswordForm = ({
email,
callbackPath,
+ onSuccess,
}: {
email: string;
callbackPath: string;
+ onSuccess?: () => void;
}) => {
const { t } = useTranslation('account');
const updateUserMutation = useUpdateUser();
@@ -46,6 +50,7 @@ export const UpdatePasswordForm = ({
const promise = updateUserMutation
.mutateAsync({ password, redirectTo })
+ .then(onSuccess)
.catch((error) => {
if (
typeof error === 'string' &&
@@ -57,11 +62,13 @@ export const UpdatePasswordForm = ({
}
});
- toast.promise(() => promise, {
- success: t(`updatePasswordSuccess`),
- error: t(`updatePasswordError`),
- loading: t(`updatePasswordLoading`),
- });
+ toast
+ .promise(() => promise, {
+ success: t(`updatePasswordSuccess`),
+ error: t(`updatePasswordError`),
+ loading: t(`updatePasswordLoading`),
+ })
+ .unwrap();
};
const updatePasswordCallback = async ({
@@ -99,6 +106,10 @@ export const UpdatePasswordForm = ({
+
+ {(error) => }
+
+
@@ -177,6 +188,27 @@ export const UpdatePasswordForm = ({
);
};
+function ErrorAlert({ error }: { error: { code: string } }) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
function SuccessAlert() {
return (
diff --git a/packages/features/admin/package.json b/packages/features/admin/package.json
index 0e87d051d..29b1835b4 100644
--- a/packages/features/admin/package.json
+++ b/packages/features/admin/package.json
@@ -20,15 +20,15 @@
"@kit/ui": "workspace:*",
"@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
- "@supabase/supabase-js": "2.58.0",
- "@tanstack/react-query": "5.90.2",
+ "@supabase/supabase-js": "2.76.1",
+ "@tanstack/react-query": "5.90.5",
"@tanstack/react-table": "^8.21.3",
- "@types/react": "19.1.16",
- "lucide-react": "^0.544.0",
- "next": "15.5.5",
- "react": "19.1.1",
- "react-dom": "19.1.1",
- "react-hook-form": "^7.63.0",
+ "@types/react": "catalog:",
+ "lucide-react": "^0.546.0",
+ "next": "16.0.0",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "react-hook-form": "^7.65.0",
"zod": "^3.25.74"
},
"exports": {
diff --git a/packages/features/admin/src/components/admin-accounts-table.tsx b/packages/features/admin/src/components/admin-accounts-table.tsx
index 20911d122..05d8448fb 100644
--- a/packages/features/admin/src/components/admin-accounts-table.tsx
+++ b/packages/features/admin/src/components/admin-accounts-table.tsx
@@ -6,7 +6,7 @@ import { usePathname, useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { ColumnDef } from '@tanstack/react-table';
import { EllipsisVertical } from 'lucide-react';
-import { useForm } from 'react-hook-form';
+import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { Tables } from '@kit/supabase/database';
@@ -103,6 +103,8 @@ function AccountsTableFilters(props: {
router.push(url);
};
+ const type = useWatch({ control: form.control, name: 'type' });
+
return (