mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(waitlist): add signed_up status to indicate who signed up after being approved off of waitlist
This commit is contained in:
@@ -56,6 +56,7 @@ function SignupFormContent({
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
const [showValidationError, setShowValidationError] = useState(false)
|
||||
const [email, setEmail] = useState('')
|
||||
const [waitlistToken, setWaitlistToken] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
@@ -63,8 +64,38 @@ function SignupFormContent({
|
||||
if (emailParam) {
|
||||
setEmail(emailParam)
|
||||
}
|
||||
|
||||
// Check for waitlist token
|
||||
const tokenParam = searchParams.get('token')
|
||||
if (tokenParam) {
|
||||
setWaitlistToken(tokenParam)
|
||||
// Verify the token and get the email
|
||||
verifyWaitlistToken(tokenParam)
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
// Verify waitlist token and pre-fill email
|
||||
const verifyWaitlistToken = async (token: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify-waitlist-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success && data.email) {
|
||||
setEmail(data.email)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error verifying waitlist token:', error)
|
||||
// Continue regardless of errors - we don't want to block sign up
|
||||
}
|
||||
}
|
||||
|
||||
// Validate password and return array of error messages
|
||||
const validatePassword = (passwordValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
// Define types inline since types.ts was deleted
|
||||
export type WaitlistStatus = 'pending' | 'approved' | 'rejected'
|
||||
export type WaitlistStatus = 'pending' | 'approved' | 'rejected' | 'signed_up'
|
||||
|
||||
export interface WaitlistEntry {
|
||||
id: string
|
||||
|
||||
@@ -346,50 +346,66 @@ export function WaitlistTable() {
|
||||
{/* Filter bar - similar to logs.tsx */}
|
||||
<div className="border-b px-6">
|
||||
<div className="flex flex-wrap items-center gap-2 py-3">
|
||||
<FilterButton
|
||||
active={status === 'all'}
|
||||
onClick={() => handleStatusChange('all')}
|
||||
icon={<UserIcon className="h-4 w-4" />}
|
||||
label="All"
|
||||
className={
|
||||
status === 'all'
|
||||
? 'bg-blue-100 text-blue-900 hover:bg-blue-200 hover:text-blue-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<FilterButton
|
||||
active={status === 'pending'}
|
||||
onClick={() => handleStatusChange('pending')}
|
||||
icon={<UserIcon className="h-4 w-4" />}
|
||||
label="Pending"
|
||||
className={
|
||||
status === 'pending'
|
||||
? 'bg-amber-100 text-amber-900 hover:bg-amber-200 hover:text-amber-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<FilterButton
|
||||
active={status === 'approved'}
|
||||
onClick={() => handleStatusChange('approved')}
|
||||
icon={<UserCheckIcon className="h-4 w-4" />}
|
||||
label="Approved"
|
||||
className={
|
||||
status === 'approved'
|
||||
? 'bg-green-100 text-green-900 hover:bg-green-200 hover:text-green-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<FilterButton
|
||||
active={status === 'rejected'}
|
||||
onClick={() => handleStatusChange('rejected')}
|
||||
icon={<UserXIcon className="h-4 w-4" />}
|
||||
label="Rejected"
|
||||
className={
|
||||
status === 'rejected'
|
||||
? 'bg-red-100 text-red-900 hover:bg-red-200 hover:text-red-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Filter buttons */}
|
||||
<div className="flex items-center gap-2 overflow-x-auto">
|
||||
<FilterButton
|
||||
active={status === 'all'}
|
||||
onClick={() => handleStatusChange('all')}
|
||||
icon={<UserIcon className="h-4 w-4" />}
|
||||
label="All"
|
||||
className={
|
||||
status === 'all'
|
||||
? 'bg-blue-100 text-blue-900 hover:bg-blue-200 hover:text-blue-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<FilterButton
|
||||
active={status === 'pending'}
|
||||
onClick={() => handleStatusChange('pending')}
|
||||
icon={<UserIcon className="h-4 w-4" />}
|
||||
label="Pending"
|
||||
className={
|
||||
status === 'pending'
|
||||
? 'bg-amber-100 text-amber-900 hover:bg-amber-200 hover:text-amber-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<FilterButton
|
||||
active={status === 'approved'}
|
||||
onClick={() => handleStatusChange('approved')}
|
||||
icon={<UserCheckIcon className="h-4 w-4" />}
|
||||
label="Approved"
|
||||
className={
|
||||
status === 'approved'
|
||||
? 'bg-green-100 text-green-900 hover:bg-green-200 hover:text-green-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<FilterButton
|
||||
active={status === 'rejected'}
|
||||
onClick={() => handleStatusChange('rejected')}
|
||||
icon={<UserXIcon className="h-4 w-4" />}
|
||||
label="Rejected"
|
||||
className={
|
||||
status === 'rejected'
|
||||
? 'bg-red-100 text-red-900 hover:bg-red-200 hover:text-red-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<FilterButton
|
||||
active={status === 'signed_up'}
|
||||
onClick={() => handleStatusChange('signed_up')}
|
||||
icon={<CheckIcon className="h-4 w-4" />}
|
||||
label="Signed Up"
|
||||
className={
|
||||
status === 'signed_up'
|
||||
? 'bg-purple-100 text-purple-900 hover:bg-purple-200 hover:text-purple-900'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -557,24 +573,45 @@ export function WaitlistTable() {
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
entry.status === 'approved'
|
||||
? 'default'
|
||||
: entry.status === 'rejected'
|
||||
? 'destructive'
|
||||
: 'secondary'
|
||||
}
|
||||
className={
|
||||
entry.status === 'approved'
|
||||
? 'bg-green-100 text-green-800 border border-green-200 hover:bg-green-200'
|
||||
: entry.status === 'rejected'
|
||||
? 'bg-red-100 text-red-800 border border-red-200 hover:bg-red-200'
|
||||
: 'bg-amber-100 text-amber-800 border border-amber-200 hover:bg-amber-200'
|
||||
}
|
||||
>
|
||||
{entry.status}
|
||||
</Badge>
|
||||
{/* Status badge */}
|
||||
<div className="py-2.5 px-4">
|
||||
{entry.status === 'pending' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-amber-100 text-amber-800 border border-amber-200 hover:bg-amber-200"
|
||||
>
|
||||
<InfoIcon className="mr-1 h-3 w-3" />
|
||||
Pending
|
||||
</Badge>
|
||||
)}
|
||||
{entry.status === 'approved' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-green-100 text-green-800 border border-green-200 hover:bg-green-200"
|
||||
>
|
||||
<UserCheckIcon className="mr-1 h-3 w-3" />
|
||||
Approved
|
||||
</Badge>
|
||||
)}
|
||||
{entry.status === 'rejected' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-red-100 text-red-800 border border-red-200 hover:bg-red-200"
|
||||
>
|
||||
<UserXIcon className="mr-1 h-3 w-3" />
|
||||
Rejected
|
||||
</Badge>
|
||||
)}
|
||||
{entry.status === 'signed_up' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-purple-100 text-purple-800 border border-purple-200 hover:bg-purple-200"
|
||||
>
|
||||
<CheckIcon className="mr-1 h-3 w-3" />
|
||||
Signed Up
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
|
||||
@@ -9,7 +9,7 @@ const logger = new Logger('WaitlistAPI')
|
||||
const getQuerySchema = z.object({
|
||||
page: z.coerce.number().optional().default(1),
|
||||
limit: z.coerce.number().optional().default(20),
|
||||
status: z.enum(['all', 'pending', 'approved', 'rejected']).optional(),
|
||||
status: z.enum(['all', 'pending', 'approved', 'rejected', 'signed_up']).optional(),
|
||||
search: z.string().optional(),
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Logger } from '@/lib/logs/console-logger'
|
||||
import { markWaitlistUserAsSignedUp } from '@/lib/waitlist/service'
|
||||
import { verifyToken } from '@/lib/waitlist/token'
|
||||
|
||||
const logger = new Logger('VerifyWaitlistToken')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { token } = body
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Token is required',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify token
|
||||
const decodedToken = await verifyToken(token)
|
||||
|
||||
if (!decodedToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Invalid or expired token',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if it's a waitlist approval token
|
||||
if (decodedToken.type !== 'waitlist-approval') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'Invalid token type',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const email = decodedToken.email
|
||||
|
||||
// Mark the user as signed up
|
||||
const result = await markWaitlistUserAsSignedUp(email)
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn(`Failed to mark user as signed up: ${result.message}`, { email })
|
||||
// Continue even if this fails - we still want to allow the user to sign up
|
||||
} else {
|
||||
logger.info(`Successfully marked waitlist user as signed up`, { email })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
email: email,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error verifying waitlist token:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'An error occurred while verifying the token',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { db } from '@/db'
|
||||
import { waitlist } from '@/db/schema'
|
||||
|
||||
// Define types for better type safety
|
||||
export type WaitlistStatus = 'pending' | 'approved' | 'rejected'
|
||||
export type WaitlistStatus = 'pending' | 'approved' | 'rejected' | 'signed_up'
|
||||
|
||||
export interface WaitlistEntry {
|
||||
id: string
|
||||
@@ -310,3 +310,46 @@ export async function verifyWaitlistToken(
|
||||
return { valid: false }
|
||||
}
|
||||
}
|
||||
|
||||
// Mark a user as signed up after they create an account
|
||||
export async function markWaitlistUserAsSignedUp(
|
||||
email: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const { user, normalizedEmail } = await findUserByEmail(email)
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'User not found in waitlist',
|
||||
}
|
||||
}
|
||||
|
||||
if (user.status !== 'approved') {
|
||||
return {
|
||||
success: false,
|
||||
message: 'User is not in approved status',
|
||||
}
|
||||
}
|
||||
|
||||
// Update status to signed_up
|
||||
await db
|
||||
.update(waitlist)
|
||||
.set({
|
||||
status: 'signed_up',
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(waitlist.email, normalizedEmail))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'User marked as signed up',
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking waitlist user as signed up:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'An error occurred while updating user status',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user