Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions app/api/didit/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';

/**
* Didit redirect callback: user is sent here after completing verification on Didit.
* We redirect them to Settings → Identity so they see their updated status.
* The backend receives the final result via webhook; this route only handles the browser redirect.
*/
const KNOWN_STATES = new Set([
'not_started',
'in_progress',
'in_review',
'approved',
'declined',
'abandoned',
'expired',
]);

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const verification = searchParams.get('verification') ?? 'complete';
const params = request.nextUrl.searchParams;

// Backend (`GET /api/didit/callback` on NestJS) forwards an authoritative
// `state` from the DB. Didit's hosted flow may instead arrive here directly
// with `?status=Approved` (legacy) or `?verification=complete` (legacy).
// We normalize all of those, but only the new `state` is trusted as a
// status — the rest just route the user back to settings.
const stateRaw = params.get('state');
const state = stateRaw && KNOWN_STATES.has(stateRaw) ? stateRaw : 'in_review';

const baseUrl = request.nextUrl.origin;
const redirectUrl = new URL('/me/settings', baseUrl);
redirectUrl.searchParams.set('verification', verification);
const redirectUrl = new URL('/me/settings', request.nextUrl.origin);
redirectUrl.searchParams.set('verification', state);
redirectUrl.searchParams.set('tab', 'identity');
const sessionId = params.get('session_id');
if (sessionId) redirectUrl.searchParams.set('session_id', sessionId);

return NextResponse.redirect(redirectUrl, 302);
}
63 changes: 59 additions & 4 deletions app/me/settings/SettingsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { useSearchParams } from 'next/navigation';
import { VerificationSubmittedModal } from '@/components/didit/VerificationSubmittedModal';
import { User } from '@/types/user';
import { getMe } from '@/lib/api/auth';
import {
getDiditStatus,
type VerificationState,
type VerificationStatus,
} from '@/lib/api/didit';
import { GetMeResponse } from '@/lib/api/types';
import { Skeleton } from '@/components/ui/skeleton';
import Settings from '@/components/profile/update/Settings';
Expand All @@ -15,9 +20,32 @@ import { IdentityVerificationSection } from '@/components/didit/IdentityVerifica
import { useRef } from 'react';
import { Loader2 } from 'lucide-react';

const KNOWN_VERIFICATION_STATES = new Set<VerificationState>([
'not_started',
'in_progress',
'in_review',
'approved',
'declined',
'abandoned',
'expired',
]);

const SettingsContent = () => {
const searchParams = useSearchParams();
const fromVerification = searchParams.get('verification') === 'complete';
const verificationParam = searchParams.get('verification');
// Modal is shown when the user just returned from the Didit hosted flow:
// the legacy callback used `verification=complete`, the new callback
// forwards the resolved `state` (e.g. `verification=in_review`).
const fromVerification = Boolean(verificationParam);
const initialModalState: VerificationState | null =
verificationParam &&
KNOWN_VERIFICATION_STATES.has(verificationParam as VerificationState)
? (verificationParam as VerificationState)
: verificationParam === 'complete'
? 'in_review'
: null;
const [verificationModalStatus, setVerificationModalStatus] =
useState<VerificationStatus | null>(null);
const [userData, setUserData] = useState<GetMeResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [showVerificationModal, setShowVerificationModal] =
Expand All @@ -38,13 +66,35 @@ const SettingsContent = () => {
}, []);

useEffect(() => {
// Only set isLoading true on the very first fetch
if (!hasLoadedOnce.current) {
setIsLoading(true);
}
fetchUserData();
}, [fetchUserData]);

useEffect(() => {
if (!fromVerification) return;
let cancelled = false;
getDiditStatus()
.then(status => {
if (cancelled) return;
setVerificationModalStatus(status);
})
.catch(() => {
if (cancelled) return;
if (initialModalState) {
setVerificationModalStatus({
state: initialModalState,
canStartNew: false,
message: '',
});
}
});
return () => {
cancelled = true;
};
}, [fromVerification, initialModalState]);

// Only show skeleton on first load — not on background refetches
if (isLoading && !hasLoadedOnce.current) {
return (
Expand All @@ -58,6 +108,7 @@ const SettingsContent = () => {
<VerificationSubmittedModal
open={showVerificationModal}
onClose={() => setShowVerificationModal(false)}
status={verificationModalStatus}
/>
<div className=''>
{/* Header */}
Expand All @@ -70,7 +121,11 @@ const SettingsContent = () => {
</p>
</div>
<Tabs
defaultValue={fromVerification ? 'identity' : 'profile'}
defaultValue={
searchParams.get('tab') === 'identity' || fromVerification
? 'identity'
: 'profile'
}
className='w-full'
>
<TabsList className='inline-flex h-auto gap-6 bg-transparent p-0'>
Expand Down Expand Up @@ -171,7 +226,7 @@ const SettingsContent = () => {
)}
</TabsContent>
<TabsContent value='identity' className='space-y-6'>
<IdentityVerificationSection user={userData} />
<IdentityVerificationSection />
</TabsContent>
</Tabs>
</div>
Expand Down
Loading
Loading