Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f33a407
fix(hackathons): single-column teams tab and primary-colored pager (#…
Benjtalkshow May 15, 2026
335758e
feat(hackathons): track-based prize structure + submission polish + t…
0xdevcollins May 16, 2026
1269fdf
Feat/submission visibility hidden until results (#566)
0xdevcollins May 16, 2026
80c7f2f
fix(hackathons): always open submissions in a new tab (#568)
Benjtalkshow May 18, 2026
1e3907e
merge: production into main
Benjtalkshow May 18, 2026
0b143cf
feat(judging): organizer dashboard — coverage, preview, per-track res…
0xdevcollins May 20, 2026
39ff6ee
feat(judging): unallocated partner funds become a warning, not a bloc…
0xdevcollins May 20, 2026
01705f8
merge: production into main (resolve AllocationPreviewCard conflict)
0xdevcollins May 21, 2026
c13e9ea
fix(rewards): show track winners on the organizer rewards page (#576)
0xdevcollins May 21, 2026
6101e01
fix(rewards): polish publish-wizard preview to industry-standard layo…
0xdevcollins May 21, 2026
7e6b842
feat(hackathons): add judging dataset to export dropdown (#577)
Benjtalkshow May 21, 2026
e32eae4
merge: production into main (resolve WinnersGrid conflict)
0xdevcollins May 21, 2026
6ecdad4
fix(rewards): match track winners by submissionId, not participantId …
0xdevcollins May 21, 2026
3fb4121
fix(auth): send absolute callbackURL on Google sign-up (#584)
0xdevcollins May 21, 2026
a5db3c4
fix(submissions): unbreak submission detail page + publish hackathon …
Benjtalkshow May 22, 2026
72f76fc
fix(blog): drop duplicate h1 from hackathon winners post (#588)
Benjtalkshow May 22, 2026
db18f9f
merge: production into main
Benjtalkshow May 22, 2026
781f15f
feat(didit): drive verification UI from /didit/status, fix In Review …
0xdevcollins May 25, 2026
e083d17
Feat/didit verification status frontend (#592)
0xdevcollins May 25, 2026
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