From 791babe430231c99f5282e122f0d955bd5bded98 Mon Sep 17 00:00:00 2001 From: isra el Date: Fri, 4 Apr 2025 22:59:15 +0300 Subject: [PATCH] chore: improve customer support and account deletion request flows --- api/src/app.module.ts | 2 + api/src/auth/auth.module.ts | 6 +- api/src/auth/guards/optional-auth.guard.ts | 57 +++++++ api/src/billing/billing.service.ts | 12 +- .../templates/account-deletion-request.hbs | 80 ++++++++++ .../customer-support-confirmation.hbs | 61 ++++++++ api/src/support/support.controller.ts | 58 +++++++ api/src/support/support.module.ts | 26 ++++ api/src/support/support.service.ts | 141 ++++++++++++++++++ api/src/users/schemas/user.schema.ts | 6 + .../(components)/account-deletion-alert.tsx | 55 +++++++ .../(components)/account-settings.tsx | 7 +- .../(components)/dashboard-layout.tsx | 4 + web/app/api/customer-support/route.ts | 59 -------- web/app/api/request-account-deletion/route.ts | 103 ------------- web/components/shared/customer-support.tsx | 84 +++++++---- web/config/api.ts | 4 + 17 files changed, 568 insertions(+), 197 deletions(-) create mode 100644 api/src/auth/guards/optional-auth.guard.ts create mode 100644 api/src/mail/templates/account-deletion-request.hbs create mode 100644 api/src/mail/templates/customer-support-confirmation.hbs create mode 100644 api/src/support/support.controller.ts create mode 100644 api/src/support/support.module.ts create mode 100644 api/src/support/support.service.ts create mode 100644 web/app/(app)/dashboard/(components)/account-deletion-alert.tsx delete mode 100644 web/app/api/customer-support/route.ts delete mode 100644 web/app/api/request-account-deletion/route.ts diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 9d9bd55..722e158 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -18,6 +18,7 @@ import { ScheduleModule } from '@nestjs/schedule' import { BillingModule } from './billing/billing.module' import { ConfigModule, ConfigService } from '@nestjs/config' import { BullModule } from '@nestjs/bull' +import { SupportModule } from './support/support.module' @Injectable() export class LoggerMiddleware implements NestMiddleware { @@ -56,6 +57,7 @@ export class LoggerMiddleware implements NestMiddleware { GatewayModule, WebhookModule, BillingModule, + SupportModule, ], controllers: [], providers: [ diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts index ec834ff..65df8ba 100644 --- a/api/src/auth/auth.module.ts +++ b/api/src/auth/auth.module.ts @@ -14,6 +14,8 @@ import { } from './schemas/password-reset.schema' import { AccessLog, AccessLogSchema } from './schemas/access-log.schema' import { EmailVerification, EmailVerificationSchema } from './schemas/email-verification.schema' +import { AuthGuard } from './guards/auth.guard' +import { OptionalAuthGuard } from './guards/optional-auth.guard' @Module({ imports: [ @@ -44,7 +46,7 @@ import { EmailVerification, EmailVerificationSchema } from './schemas/email-veri MailModule, ], controllers: [AuthController], - providers: [AuthService, JwtStrategy, MongooseModule], - exports: [AuthService, JwtModule], + providers: [AuthService, JwtStrategy, AuthGuard, OptionalAuthGuard, MongooseModule], + exports: [AuthService, JwtModule, AuthGuard, OptionalAuthGuard], }) export class AuthModule {} diff --git a/api/src/auth/guards/optional-auth.guard.ts b/api/src/auth/guards/optional-auth.guard.ts new file mode 100644 index 0000000..025f505 --- /dev/null +++ b/api/src/auth/guards/optional-auth.guard.ts @@ -0,0 +1,57 @@ +import { + CanActivate, + ExecutionContext, + Injectable, +} from '@nestjs/common' +import { JwtService } from '@nestjs/jwt' +import { UsersService } from '../../users/users.service' +import { AuthService } from '../auth.service' +import * as bcrypt from 'bcryptjs' + +@Injectable() +// Guard for optionally authenticating users by either jwt token or api key +export class OptionalAuthGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private usersService: UsersService, + private authService: AuthService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest() + let userId + const apiKeyString = request.headers['x-api-key'] || request.query.apiKey + if (request.headers.authorization?.startsWith('Bearer ')) { + const bearerToken = request.headers.authorization.split(' ')[1] + try { + const payload = this.jwtService.verify(bearerToken) + userId = payload.sub + } catch (e) { + // Ignore token verification errors + return true + } + } else if (apiKeyString) { + const regex = new RegExp(`^${apiKeyString.substr(0, 17)}`, 'g') + const apiKey = await this.authService.findApiKey({ + apiKey: { $regex: regex }, + $or: [{ revokedAt: null }, { revokedAt: { $exists: false } }], + }) + + if (apiKey && bcrypt.compareSync(apiKeyString, apiKey.hashedApiKey)) { + userId = apiKey.user + request.apiKey = apiKey + } + } + + if (userId) { + const user = await this.usersService.findOne({ _id: userId }) + if (user) { + request.user = user + this.authService.trackAccessLog({ request }) + } + } + + // Always return true as authentication is optional + return true + } +} \ No newline at end of file diff --git a/api/src/billing/billing.service.ts b/api/src/billing/billing.service.ts index 80df07a..cf6deb5 100644 --- a/api/src/billing/billing.service.ts +++ b/api/src/billing/billing.service.ts @@ -132,8 +132,8 @@ export class BillingService { const customPlanSubscription = await this.subscriptionModel.findOne({ user: user._id, - plan: { $in: customPlans.map((plan) => plan._id) }, - isActive: true, + plan: { $in: customPlans.map((plan) => plan._id) }, + isActive: true, }) if (customPlanSubscription) { @@ -395,12 +395,18 @@ export class BillingService { bulkSendLimit: plan.bulkSendLimit, monthlyLimit: plan.monthlyLimit, }, - HttpStatus.BAD_REQUEST, + HttpStatus.TOO_MANY_REQUESTS, ) } return true } catch (error) { + if ( + error instanceof HttpException && + error.getStatus() === HttpStatus.TOO_MANY_REQUESTS + ) { + throw error + } console.error('canPerformAction: Exception in canPerformAction') console.error(JSON.stringify(error)) return true diff --git a/api/src/mail/templates/account-deletion-request.hbs b/api/src/mail/templates/account-deletion-request.hbs new file mode 100644 index 0000000..33e8134 --- /dev/null +++ b/api/src/mail/templates/account-deletion-request.hbs @@ -0,0 +1,80 @@ + + + + + Account Deletion Request + + + +
+ {{!-- --}} +

Account Deletion Request

+
+ +
+

Hello {{name}},

+ +

We have received your request to delete your TextBee account. We're + sorry to see you go.

+ +
+

Important: + Your account has been marked for deletion and will be processed within + 7 days. During this period:

+
    +
  • You can still log in and access your account until the deletion is + completed
  • +
  • After the deletion is complete, all your data will be permanently + removed
  • +
  • This action cannot be undone once processed
  • +
+
+ +
+
Reason for deletion:
+

{{#if message}}{{message}}{{else}}No reason provided{{/if}}

+
+ +
+
Account Information:
+

Name: {{name}}

+

Email: {{email}}

+
+ +
+

Changed Your Mind?

+

If you didn't request this deletion or want to + keep your account, you can easily cancel this request!

+

Simply reply to this email as soon as possible and we'll immediately + stop the deletion process.

+

Your account and all your data will remain intact. No further action + will be needed.

+
+
+ + + + \ No newline at end of file diff --git a/api/src/mail/templates/customer-support-confirmation.hbs b/api/src/mail/templates/customer-support-confirmation.hbs new file mode 100644 index 0000000..631a5b8 --- /dev/null +++ b/api/src/mail/templates/customer-support-confirmation.hbs @@ -0,0 +1,61 @@ + + + + + Support Request Confirmation + + + +
+ {{!-- --}} +

Support Request Submitted

+
+ +
+

Hello {{name}},

+ +

Thank you for contacting our support team. We have received your + message and will get back to you as soon as possible.

+ +
+
Category:
+

{{category}}

+ +
Your Message:
+

{{message}}

+
+ +
+
Your Contact Information:
+

Name: {{name}}

+

Email: {{email}}

+ {{#if phone}} +

Phone: {{phone}}

+ {{else}} +

Phone: Not provided

+ {{/if}} +
+ +

we will review your request and respond to the email address you + provided. If you have any additional information to share, please reply + to this email.

+
+ + + + \ No newline at end of file diff --git a/api/src/support/support.controller.ts b/api/src/support/support.controller.ts new file mode 100644 index 0000000..5a3fd9c --- /dev/null +++ b/api/src/support/support.controller.ts @@ -0,0 +1,58 @@ +import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common' +import { + CreateSupportMessageDto, + SupportCategory, +} from './dto/create-support-message.dto' +import { SupportService } from './support.service' +import { JwtAuthGuard } from '../auth/jwt-auth.guard' +import { Request } from 'express' +import { OptionalAuthGuard } from '../auth/guards/optional-auth.guard' + +@Controller('support') +export class SupportController { + constructor(private readonly supportService: SupportService) {} + + @UseGuards(OptionalAuthGuard) + @Post('customer-support') + async createSupportMessage( + @Body() createSupportMessageDto: CreateSupportMessageDto, + @Req() req: Request, + ) { + const ip = req.ip || (req.headers['x-forwarded-for'] as string) + const userAgent = req.headers['user-agent'] as string + + // Add request metadata + createSupportMessageDto.ip = ip + createSupportMessageDto.userAgent = userAgent + + // If user is authenticated, associate the support request with the user + if (req.user) { + createSupportMessageDto.user = req.user['_id'] + } + + return this.supportService.createSupportMessage(createSupportMessageDto) + } + + @UseGuards(JwtAuthGuard) + @Post('request-account-deletion') + async requestAccountDeletion( + @Body() body: { message: string }, + @Req() req: Request, + ) { + const ip = req.ip || (req.headers['x-forwarded-for'] as string) + const userAgent = req.headers['user-agent'] as string + const user = req.user + + const createSupportMessageDto: CreateSupportMessageDto = { + user: user['_id'], + name: user['name'], + email: user['email'], + category: SupportCategory.ACCOUNT_DELETION, + message: body.message, + ip, + userAgent, + } + + return this.supportService.requestAccountDeletion(createSupportMessageDto) + } +} diff --git a/api/src/support/support.module.ts b/api/src/support/support.module.ts new file mode 100644 index 0000000..cc47894 --- /dev/null +++ b/api/src/support/support.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common' +import { MongooseModule } from '@nestjs/mongoose' +import { + SupportMessage, + SupportMessageSchema, +} from './schemas/support-message.schema' +import { SupportService } from './support.service' +import { SupportController } from './support.controller' +import { UsersModule } from 'src/users/users.module' +import { BillingModule } from 'src/billing/billing.module' +import { MailModule } from 'src/mail/mail.module' +import { AuthModule } from 'src/auth/auth.module' +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: SupportMessage.name, schema: SupportMessageSchema }, + ]), + UsersModule, + BillingModule, + MailModule, + AuthModule, + ], + controllers: [SupportController], + providers: [SupportService], +}) +export class SupportModule {} diff --git a/api/src/support/support.service.ts b/api/src/support/support.service.ts new file mode 100644 index 0000000..ddd15f6 --- /dev/null +++ b/api/src/support/support.service.ts @@ -0,0 +1,141 @@ +import { + Injectable, + ConflictException, + NotFoundException, +} from '@nestjs/common' +import { InjectModel } from '@nestjs/mongoose' +import { isValidObjectId, Model, Types } from 'mongoose' +import { + SupportMessage, + SupportMessageDocument, +} from './schemas/support-message.schema' +import { User, UserDocument } from '../users/schemas/user.schema' +import { + CreateSupportMessageDto, + SupportCategory, +} from './dto/create-support-message.dto' +import { MailService } from '../mail/mail.service' + +@Injectable() +export class SupportService { + constructor( + @InjectModel(SupportMessage.name) + private supportMessageModel: Model, + @InjectModel(User.name) private userModel: Model, + private readonly mailService: MailService, + ) {} + + async createSupportMessage( + createSupportMessageDto: CreateSupportMessageDto, + ): Promise<{ message: string }> { + try { + // Create and save the support message + const createdMessage = new this.supportMessageModel( + createSupportMessageDto, + ) + const savedMessage = await createdMessage.save() + + // Determine if the user is registered + let user = null + if ( + createSupportMessageDto.user && + isValidObjectId(createSupportMessageDto.user) + ) { + user = await this.userModel.findById(createSupportMessageDto.user) + } + + // Send confirmation email to user + await this.mailService.sendEmailFromTemplate({ + to: createSupportMessageDto.email, + cc: process.env.ADMIN_EMAIL, + subject: `Support Request Submitted: ${createSupportMessageDto.category}-${savedMessage._id}`, + template: 'customer-support-confirmation', + context: { + name: createSupportMessageDto.name, + email: createSupportMessageDto.email, + phone: createSupportMessageDto.phone || 'Not provided', + category: createSupportMessageDto.category, + message: createSupportMessageDto.message, + appLogoUrl: + process.env.APP_LOGO_URL || 'https://textbee.dev/logo.png', + currentYear: new Date().getFullYear(), + }, + }) + + return { message: 'Support request submitted successfully' } + } catch (error) { + console.error('Error creating support message:', error) + throw error + } + } + + // Method for account deletion requests + async requestAccountDeletion( + createSupportMessageDto: CreateSupportMessageDto, + ): Promise<{ message: string }> { + try { + // Check if user exists + if ( + !createSupportMessageDto.user || + !isValidObjectId(createSupportMessageDto.user) + ) { + throw new NotFoundException('User not found') + } + + const userId = new Types.ObjectId(createSupportMessageDto.user.toString()) + const user = await this.userModel.findById(userId) + + if (!user) { + throw new NotFoundException('User not found') + } + + // Check if user has already requested deletion + const existingRequest = await this.supportMessageModel.findOne({ + user: userId, + category: SupportCategory.ACCOUNT_DELETION, + }) + + if (existingRequest) { + throw new ConflictException( + 'Account deletion has already been requested', + ) + } + + // Create and save the support message + const createdMessage = new this.supportMessageModel( + createSupportMessageDto, + ) + const savedMessage = await createdMessage.save() + + // Update user's account deletion requested timestamp + await this.userModel.updateOne( + { _id: userId }, + { + accountDeletionRequestedAt: new Date(), + accountDeletionReason: createSupportMessageDto.message || null, + }, + ) + + // Send confirmation email + await this.mailService.sendEmailFromTemplate({ + to: user.email, + cc: process.env.ADMIN_EMAIL, + subject: `Account Deletion Request: ${savedMessage._id}`, + template: 'account-deletion-request', + context: { + name: user.name, + email: user.email, + message: createSupportMessageDto.message || 'No reason provided', + appLogoUrl: + process.env.APP_LOGO_URL || 'https://textbee.dev/logo.png', + currentYear: new Date().getFullYear(), + }, + }) + + return { message: 'Account deletion request submitted successfully' } + } catch (error) { + console.error('Error requesting account deletion:', error) + throw error + } + } +} diff --git a/api/src/users/schemas/user.schema.ts b/api/src/users/schemas/user.schema.ts index 2c88a06..dac9e13 100644 --- a/api/src/users/schemas/user.schema.ts +++ b/api/src/users/schemas/user.schema.ts @@ -37,6 +37,12 @@ export class User { @Prop({ type: Boolean, default: false }) isBanned: boolean + + @Prop({ type: Date }) + accountDeletionRequestedAt: Date + + @Prop({ type: String }) + accountDeletionReason: string } export const UserSchema = SchemaFactory.createForClass(User) diff --git a/web/app/(app)/dashboard/(components)/account-deletion-alert.tsx b/web/app/(app)/dashboard/(components)/account-deletion-alert.tsx new file mode 100644 index 0000000..49d5af3 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/account-deletion-alert.tsx @@ -0,0 +1,55 @@ +import { Alert, AlertDescription } from '@/components/ui/alert' +import { ApiEndpoints } from '@/config/api' +import httpBrowserClient from '@/lib/httpBrowserClient' +import { useQuery } from '@tanstack/react-query' +import { AlertTriangle } from 'lucide-react' + +export default function AccountDeletionAlert() { + const { + data: userData, + isLoading: isLoadingUserData, + error: userDataError, + } = useQuery({ + queryKey: ['whoAmI'], + queryFn: () => + httpBrowserClient + .get(ApiEndpoints.auth.whoAmI()) + .then((res) => res.data.data), + }) + + if (isLoadingUserData || !userData || userDataError) { + return null + } + + // Only show the alert if the user has requested account deletion + if (!userData.accountDeletionRequestedAt) { + return null + } + + // Calculate days remaining until deletion (assuming 7-day window) + const deletionDate = new Date(userData.accountDeletionRequestedAt) + deletionDate.setDate(deletionDate.getDate() + 7) + const daysRemaining = Math.max( + 0, + Math.ceil( + (deletionDate.getTime() - new Date().getTime()) / (1000 * 3600 * 24) + ) + ) + + return ( + + + +
+ Your account is pending deletion.{' '} + Your data will be permanently deleted{' '} + {daysRemaining > 0 + ? `in ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''}.` + : 'very soon.'}{' '} + If you would like to cancel this request, please email{' '} + support@textbee.dev. +
+
+
+ ) +} diff --git a/web/app/(app)/dashboard/(components)/account-settings.tsx b/web/app/(app)/dashboard/(components)/account-settings.tsx index bdde2fc..6836698 100644 --- a/web/app/(app)/dashboard/(components)/account-settings.tsx +++ b/web/app/(app)/dashboard/(components)/account-settings.tsx @@ -188,7 +188,7 @@ export default function AccountSettings() { isSuccess: isRequestAccountDeletionSuccess, } = useMutation({ mutationFn: () => - axios.post('/api/request-account-deletion', { + httpBrowserClient.post(ApiEndpoints.support.requestAccountDeletion(), { message: deleteReason, }), onSuccess: () => { @@ -714,8 +714,9 @@ export default function AccountSettings() { {requestAccountDeletionError && (

- {requestAccountDeletionError.message || - 'Failed to submit account deletion request'} + {(requestAccountDeletionError as any).response?.data?.message || + requestAccountDeletionError.message || + 'Failed to submit account deletion request'}

)} diff --git a/web/app/(app)/dashboard/(components)/dashboard-layout.tsx b/web/app/(app)/dashboard/(components)/dashboard-layout.tsx index cdac13b..dbb8bf5 100644 --- a/web/app/(app)/dashboard/(components)/dashboard-layout.tsx +++ b/web/app/(app)/dashboard/(components)/dashboard-layout.tsx @@ -15,6 +15,7 @@ import { JoinCommunityModal } from '@/components/shared/join-community-modal' import { ContributeModal } from '@/components/shared/contribute-modal' import UpgradeToProAlert from './upgrade-to-pro-alert' import VerifyEmailAlert from './verify-email-alert' +import AccountDeletionAlert from './account-deletion-alert' export default function Dashboard({ children, @@ -67,6 +68,7 @@ export default function Dashboard({ + @@ -74,12 +76,14 @@ export default function Dashboard({ + + diff --git a/web/app/api/customer-support/route.ts b/web/app/api/customer-support/route.ts deleted file mode 100644 index dc703eb..0000000 --- a/web/app/api/customer-support/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - NextRequest, - NextResponse, - userAgent, - userAgentFromString, -} from 'next/server' - -import prismaClient from '@/lib/prismaClient' -import { sendMail } from '@/lib/mail' - -export async function POST(req: NextRequest) { - const ip = req.ip || req.headers.get('x-forwarded-for') - const { browser, device, os, isBot, ua } = userAgent(req) - // const userAgentString = userAgentFromString(ua) - - const body = await req.json() - - try { - const result = await prismaClient.supportMessage.create({ - data: { - ...body, - ip, - userAgent: ua, - }, - }) - - // send email to user - await sendMail({ - to: body.email, - cc: process.env.ADMIN_EMAIL, - subject: `Support request submitted: ${body.category}-${result.id}`, - html: `
-      

Support request submitted

-

Thank you for contacting us. We will get back to you soon.

-

Here is a copy of your message:

-
-

Category


${body.category} -

Message


${body.message} - -

Contact Information

-

Name: ${body.name}

-

Email: ${body.email}

-

Phone: ${body.phone || 'N/A'}

-
`, - }) - - return NextResponse.json({ - message: 'Support request submitted', - }) - } catch (error) { - console.error(error) - return NextResponse.json( - { - message: `Support request failed to submit : ${error.message}`, - }, - { status: 400 } - ) - } -} diff --git a/web/app/api/request-account-deletion/route.ts b/web/app/api/request-account-deletion/route.ts deleted file mode 100644 index 3e18ed1..0000000 --- a/web/app/api/request-account-deletion/route.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - NextRequest, - NextResponse, - userAgent, - userAgentFromString, -} from 'next/server' - -import prismaClient from '@/lib/prismaClient' -import { sendMail } from '@/lib/mail' -import { getServerSession, User } from 'next-auth' -import { authOptions } from '@/lib/auth' - -export async function POST(req: NextRequest) { - const ip = req.ip || req.headers.get('x-forwarded-for') - const { browser, device, os, isBot, ua } = userAgent(req) - // const userAgentString = userAgentFromString(ua) - - const body = await req.json() - - const session = await getServerSession(authOptions as any) - if (!session) { - return NextResponse.json( - { - message: 'You must be logged in to request account deletion', - }, - { status: 401 } - ) - } - // @ts-ignore - const currentUser = session?.user as User - - if (!currentUser) { - return NextResponse.json( - { - message: 'You must be logged in to request account deletion', - }, - { status: 401 } - ) - } - - const category = 'account-deletion' - const message = body.message ?? 'No message provided' - - try { - // check if the user has already requested account deletion - const existingRequest = await prismaClient.supportMessage.findFirst({ - where: { - user: currentUser.id, - category: 'account-deletion', - }, - }) - - if (existingRequest) { - return NextResponse.json( - { - message: 'You have already requested account deletion', - }, - { status: 400 } - ) - } - - const result = await prismaClient.supportMessage.create({ - data: { - user: currentUser.id, - category, - message, - ip, - userAgent: ua, - }, - }) - - // send email to user - await sendMail({ - to: currentUser.email, - cc: process.env.ADMIN_EMAIL, - subject: `Account deletion request submitted: ${category}-${result.id}`, - html: `
-      

Account deletion request submitted

-

Thank you for contacting us. We will get back to you soon.

-

Here is a copy of your message:

-
-

Category


${category} -

Message


${message} - -

Contact Information

-

Name: ${currentUser.name}

-

Email: ${currentUser.email}

-
`, - }) - - return NextResponse.json({ - message: 'Support request submitted', - }) - } catch (error) { - console.error(error) - return NextResponse.json( - { - message: `Support request failed to submit : ${error.message}`, - }, - { status: 400 } - ) - } -} diff --git a/web/components/shared/customer-support.tsx b/web/components/shared/customer-support.tsx index acd835d..02dae65 100644 --- a/web/components/shared/customer-support.tsx +++ b/web/components/shared/customer-support.tsx @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button' import { Dialog, DialogContent, + DialogDescription, DialogHeader, DialogTitle, DialogTrigger, @@ -31,13 +32,14 @@ import { useForm } from 'react-hook-form' import { z } from 'zod' import { zodResolver } from '@hookform/resolvers/zod' import { toast } from '@/hooks/use-toast' -import axios from 'axios' +import httpBrowserClient from '@/lib/httpBrowserClient' +import { ApiEndpoints } from '@/config/api' const SupportFormSchema = z.object({ name: z.string().min(1, { message: 'Name is required' }), email: z.string().email({ message: 'Invalid email address' }), phone: z.string().optional(), - category: z.enum(['general', 'technical'], { + category: z.enum(['general', 'technical', 'billing-and-payments', 'other'], { message: 'Support category is required', }), message: z.string().min(1, { message: 'Message is required' }), @@ -45,6 +47,10 @@ const SupportFormSchema = z.object({ export default function SupportButton() { const [open, setOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isSubmitSuccessful, setIsSubmitSuccessful] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const form = useForm({ resolver: zodResolver(SupportFormSchema), defaultValues: { @@ -57,23 +63,41 @@ export default function SupportButton() { }) const onSubmit = async (data: any) => { + setIsSubmitting(true) + setErrorMessage(null) + try { - const response = await axios.post('/api/customer-support', data) + // Use the existing httpBrowserClient to call the NestJS endpoint + const response = await httpBrowserClient.post( + ApiEndpoints.support.customerSupport(), + data + ) - const result = response.data + setIsSubmitSuccessful(true) toast({ title: 'Support request submitted', - description: result.message, + description: response.data.message || 'We will get back to you soon.', }) + + // Wait 3 seconds before closing the dialog + setTimeout(() => { + setOpen(false) + }, 3000) } catch (error) { - form.setError('root.serverError', { - message: 'Error submitting support request', - }) + console.error('Error submitting support request:', error) + + setErrorMessage( + 'Error submitting support request. Please try again later.' + ) + toast({ title: 'Error submitting support request', description: 'Please try again later', + variant: 'destructive', }) + } finally { + setIsSubmitting(false) } } @@ -81,6 +105,8 @@ export default function SupportButton() { setOpen(open) if (!open) { form.reset() + setIsSubmitSuccessful(false) + setErrorMessage(null) } } @@ -98,19 +124,23 @@ export default function SupportButton() { Contact Support + + Fill out the form below and we'll get back to you as soon as + possible. +
( Support Category @@ -132,7 +166,7 @@ export default function SupportButton() { ( Name @@ -146,7 +180,7 @@ export default function SupportButton() { ( Email @@ -164,7 +198,7 @@ export default function SupportButton() { ( Phone (Optional) @@ -178,7 +212,7 @@ export default function SupportButton() { ( Message @@ -193,27 +227,23 @@ export default function SupportButton() { )} /> - {form.formState.isSubmitSuccessful && ( + {isSubmitSuccessful && (
We received your message, we will get back to you soon.
)} - {form.formState.errors.root?.serverError && ( - <> - {' '} - {form.formState.errors.root.serverError.message} - + {errorMessage && ( +
+ {errorMessage} +
)} -