Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
@@ -1,3 +0,0 @@
|
||||
import baseConfig from '@kit/eslint-config/base.js';
|
||||
|
||||
export default baseConfig;
|
||||
@@ -1,43 +1,39 @@
|
||||
{
|
||||
"name": "@kit/otp",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
".": "./src/api/index.ts",
|
||||
"./components": "./src/components/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@kit/email-templates": "workspace:*",
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/mailers": "workspace:*",
|
||||
"@kit/next": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"private": true,
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/api/index.ts",
|
||||
"./components": "./src/components/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hookform/resolvers": "catalog:",
|
||||
"@kit/email-templates": "workspace:*",
|
||||
"@kit/mailers": "workspace:*",
|
||||
"@kit/next": "workspace:*",
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"next-safe-action": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -61,17 +62,28 @@ export function VerifyOtpForm({
|
||||
}: VerifyOtpFormProps) {
|
||||
// Track the current step (email entry or OTP verification)
|
||||
const [step, setStep] = useState<'email' | 'otp'>('email');
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// Track errors
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Track verification success
|
||||
const [, setVerificationSuccess] = useState(false);
|
||||
|
||||
const { execute: executeSendOtp, isPending } = useAction(sendOtpEmailAction, {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
setStep('otp');
|
||||
setError(null);
|
||||
} else {
|
||||
setError(data?.error || 'Failed to send OTP. Please try again.');
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setError('An unexpected error occurred. Please try again.');
|
||||
},
|
||||
});
|
||||
|
||||
// Email form
|
||||
const emailForm = useForm({
|
||||
resolver: zodResolver(SendOtpSchema),
|
||||
defaultValues: {
|
||||
values: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
@@ -88,28 +100,14 @@ export function VerifyOtpForm({
|
||||
const handleSendOtp = () => {
|
||||
setError(null);
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await sendOtpEmailAction({
|
||||
purpose,
|
||||
email,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setStep('otp');
|
||||
} else {
|
||||
setError(result.error || 'Failed to send OTP. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred. Please try again.');
|
||||
console.error('Error sending OTP:', err);
|
||||
}
|
||||
executeSendOtp({
|
||||
purpose,
|
||||
email,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle OTP verification
|
||||
const handleVerifyOtp = (data: z.infer<typeof VerifyOtpSchema>) => {
|
||||
setVerificationSuccess(true);
|
||||
const handleVerifyOtp = (data: z.output<typeof VerifyOtpSchema>) => {
|
||||
onSuccess(data.otp);
|
||||
};
|
||||
|
||||
@@ -124,7 +122,7 @@ export function VerifyOtpForm({
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans
|
||||
i18nKey="common:otp.requestVerificationCodeDescription"
|
||||
i18nKey="common.otp.requestVerificationCodeDescription"
|
||||
values={{ email }}
|
||||
/>
|
||||
</p>
|
||||
@@ -132,10 +130,10 @@ export function VerifyOtpForm({
|
||||
|
||||
<If condition={Boolean(error)}>
|
||||
<Alert variant="destructive">
|
||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||
<TriangleAlert className="h-4 w-4" />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey="common:otp.errorSendingCode" />
|
||||
<Trans i18nKey="common.otp.errorSendingCode" />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
@@ -153,10 +151,10 @@ export function VerifyOtpForm({
|
||||
{isPending ? (
|
||||
<>
|
||||
<Spinner className="mr-2 h-4 w-4" />
|
||||
<Trans i18nKey="common:otp.sendingCode" />
|
||||
<Trans i18nKey="common.otp.sendingCode" />
|
||||
</>
|
||||
) : (
|
||||
<Trans i18nKey="common:otp.sendVerificationCode" />
|
||||
<Trans i18nKey="common.otp.sendVerificationCode" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -166,7 +164,7 @@ export function VerifyOtpForm({
|
||||
<Form {...otpForm}>
|
||||
<div className="flex w-full flex-col items-center gap-y-8">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="common:otp.codeSentToEmail" values={{ email }} />
|
||||
<Trans i18nKey="common.otp.codeSentToEmail" values={{ email }} />
|
||||
</div>
|
||||
|
||||
<form
|
||||
@@ -175,10 +173,10 @@ export function VerifyOtpForm({
|
||||
>
|
||||
<If condition={Boolean(error)}>
|
||||
<Alert variant="destructive">
|
||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||
<TriangleAlert className="h-4 w-4" />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey="common:error" />
|
||||
<Trans i18nKey="common.error" />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
@@ -212,7 +210,7 @@ export function VerifyOtpForm({
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey="common:otp.enterCodeFromEmail" />
|
||||
<Trans i18nKey="common.otp.enterCodeFromEmail" />
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -229,7 +227,7 @@ export function VerifyOtpForm({
|
||||
disabled={isPending}
|
||||
onClick={() => setStep('email')}
|
||||
>
|
||||
<Trans i18nKey="common:otp.requestNewCode" />
|
||||
<Trans i18nKey="common.otp.requestNewCode" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -240,10 +238,10 @@ export function VerifyOtpForm({
|
||||
{isPending ? (
|
||||
<>
|
||||
<Spinner className="mr-2 h-4 w-4" />
|
||||
<Trans i18nKey="common:otp.verifying" />
|
||||
<Trans i18nKey="common.otp.verifying" />
|
||||
</>
|
||||
) : (
|
||||
<Trans i18nKey="common:otp.verifyCode" />
|
||||
<Trans i18nKey="common.otp.verifyCode" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { renderOtpEmail } from '@kit/email-templates';
|
||||
import { getMailer } from '@kit/mailers';
|
||||
@@ -6,14 +6,14 @@ import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
const EMAIL_SENDER = z
|
||||
.string({
|
||||
required_error: 'EMAIL_SENDER is required',
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1)
|
||||
.parse(process.env.EMAIL_SENDER);
|
||||
|
||||
const PRODUCT_NAME = z
|
||||
.string({
|
||||
required_error: 'PRODUCT_NAME is required',
|
||||
error: 'PRODUCT_NAME is required',
|
||||
})
|
||||
.min(1)
|
||||
.parse(process.env.NEXT_PUBLIC_PRODUCT_NAME);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
@@ -25,8 +25,9 @@ const SendOtpEmailSchema = z.object({
|
||||
/**
|
||||
* Server action to generate an OTP and send it via email
|
||||
*/
|
||||
export const sendOtpEmailAction = enhanceAction(
|
||||
async function (data: z.infer<typeof SendOtpEmailSchema>, user) {
|
||||
export const sendOtpEmailAction = authActionClient
|
||||
.inputSchema(SendOtpEmailSchema)
|
||||
.action(async ({ parsedInput: data, ctx: { user } }) => {
|
||||
const logger = await getLogger();
|
||||
const ctx = { name: 'send-otp-email', userId: user.id };
|
||||
const email = user.email;
|
||||
@@ -87,9 +88,4 @@ export const sendOtpEmailAction = enhanceAction(
|
||||
error instanceof Error ? error.message : 'Failed to send OTP email',
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: SendOtpEmailSchema,
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user