diff --git a/src/app/request-student-account/page.tsx b/src/app/request-student-account/page.tsx
new file mode 100644
index 00000000..447b9062
--- /dev/null
+++ b/src/app/request-student-account/page.tsx
@@ -0,0 +1,27 @@
+import type { Metadata } from 'next/types';
+
+import { Container } from '@/components/elements/container';
+import { Section } from '@/components/elements/section';
+import { Layout } from '@/components/layout';
+import { LoginModal } from '@/components/modules/login-modal';
+import { buildMetadata } from '@/lib/utils/build-metadata';
+import { StudentAccountContent } from './student-account-content';
+
+export const metadata: Metadata = buildMetadata({
+ title: 'Student Discount | HTTP Toolkit',
+ description:
+ 'HTTP Toolkit Pro is free for students and faculty at accredited universities and colleges. Renew each year while you study.',
+});
+
+export default function RequestStudentAccountPage() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/request-student-account/student-account-content.tsx b/src/app/request-student-account/student-account-content.tsx
new file mode 100644
index 00000000..6d1f3b10
--- /dev/null
+++ b/src/app/request-student-account/student-account-content.tsx
@@ -0,0 +1,290 @@
+'use client';
+
+import { useCallback, useEffect, useState } from 'react';
+import { observer } from 'mobx-react-lite';
+import { styled } from '@linaria/react';
+
+import { screens } from '@/styles/tokens';
+import { Button } from '@/components/elements/button';
+import { Gradient } from '@/components/elements/gradient';
+import { Heading } from '@/components/elements/heading';
+import { Spinner } from '@/components/elements/icon';
+import Stack from '@/components/elements/stack';
+import { Text } from '@/components/elements/text';
+import { ContactForm } from '@/components/sections/contact-form';
+import { SuccessHero } from '@/components/sections/success-hero';
+import { accountStore } from '@/lib/store/account-store';
+
+const ACCOUNTS_API_BASE = process.env.NEXT_PUBLIC_ACCOUNTS_API
+ ?? 'https://accounts.httptoolkit.tech/api';
+
+type PageState =
+ | 'initial'
+ | 'verifying'
+ | 'success'
+ | 'not_academic'
+ | 'already_active'
+ | 'error';
+
+interface VerificationResult {
+ school?: string;
+ expiry?: number;
+}
+
+const StyledPageWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 32px;
+ max-width: 620px;
+ margin: 0 auto;
+`;
+
+const StyledSpinner = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ & svg {
+ width: 48px;
+ height: 48px;
+ animation: student-spin 1s linear infinite;
+ }
+
+ @keyframes student-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+`;
+
+const StyledGradientLeft = styled.div`
+ position: absolute;
+ max-width: 100%;
+ top: -180px;
+ left: 0;
+ height: 780px;
+ pointer-events: none;
+
+ @media (min-width: ${screens['lg']}) {
+ top: -7px;
+ }
+`;
+
+const StyledFallbackWrapper = styled.div`
+ width: 100%;
+ max-width: 620px;
+ margin: 0 auto;
+`;
+
+function getAccessToken(): string | undefined {
+ // Read directly from localStorage because @httptoolkit/accounts does not
+ // export its internal getToken() helper. This mirrors how the package
+ // itself stores and reads tokens (see auth.js line 34).
+ try {
+ const raw = localStorage.getItem('tokens');
+ if (!raw) return undefined;
+ return JSON.parse(raw)?.accessToken;
+ } catch {
+ return undefined;
+ }
+}
+
+function formatExpiry(timestamp?: number): string {
+ if (!timestamp) return '';
+ return new Date(timestamp).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+}
+
+export const StudentAccountContent = observer(() => {
+ const [pageState, setPageState] = useState('initial');
+ const [result, setResult] = useState({});
+ const [errorMessage, setErrorMessage] = useState('');
+
+ const requestStudentAccount = useCallback(async () => {
+ setPageState('verifying');
+
+ const accessToken = getAccessToken();
+ if (!accessToken) {
+ setPageState('error');
+ setErrorMessage('No authentication token found. Please try logging in again.');
+ return;
+ }
+
+ try {
+ const response = await fetch(`${ACCOUNTS_API_BASE}/request-student-account`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setResult({ school: data.school, expiry: data.expiry });
+ setPageState('success');
+ } else if (response.status === 403) {
+ setPageState('not_academic');
+ } else if (response.status === 409) {
+ const data = await response.json().catch(() => ({}));
+ setResult({ expiry: data.expiry });
+ setPageState('already_active');
+ } else {
+ setPageState('error');
+ setErrorMessage('Something went wrong. Please try again later.');
+ }
+ } catch {
+ setPageState('error');
+ setErrorMessage('Could not reach the server. Please check your connection and try again.');
+ }
+ }, []);
+
+ useEffect(() => {
+ if (accountStore.isLoggedIn && pageState === 'initial') {
+ requestStudentAccount();
+ }
+ }, [accountStore.isLoggedIn, pageState, requestStudentAccount]);
+
+ const handleLoginClick = useCallback(() => {
+ accountStore.login();
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ {pageState === 'initial' && (
+
+
+
+ Student Discount
+
+
+ HTTP Toolkit Pro is free for students and faculty at accredited
+ universities and colleges. Log in with your academic email
+ address (.edu, .ac.uk, etc.) to get started. Your access lasts
+ one year and can be renewed as long as you're still studying.
+
+
+
+
+ )}
+
+ {pageState === 'verifying' && (
+
+
+
+ Verifying your email...
+
+
+ Checking whether your email is associated with an academic institution.
+
+
+
+
+
+
+ )}
+
+ {pageState === 'success' && (
+
+
+ Your academic email has been verified
+ {result.school ? ` (${result.school})` : ''} and
+ your HTTP Toolkit Pro subscription is now active
+ {result.expiry ? ` until ${formatExpiry(result.expiry)}` : ' for one year'}.
+
+
+ When your access expires, come back to this page to renew it
+ for another year. Download HTTP Toolkit and log in with your
+ account to get started.
+
+
+ }
+ callToAction={
+
+ }
+ />
+ )}
+
+ {pageState === 'already_active' && (
+
+ You already have an active student subscription
+ {result.expiry ? ` until ${formatExpiry(result.expiry)}` : ''}.
+ You can renew when less than 2 months remain. Download HTTP
+ Toolkit and log in with your account to use it.
+
+ }
+ callToAction={
+
+ }
+ />
+ )}
+
+ {pageState === 'not_academic' && (
+
+
+
+
+
+ Email not recognized
+
+
+ We couldn't verify your email ({accountStore.user.email}) as belonging to
+ an academic institution. If you believe this is a mistake,
+ use the form below to contact us and we'll review it manually.
+
+
+
+
+
+
+
+ )}
+
+ {pageState === 'error' && (
+
+
+
+ Something went wrong
+
+
+ {errorMessage}
+
+
+
+
+ )}
+ >
+ );
+});
diff --git a/src/components/sections/contact-form/index.tsx b/src/components/sections/contact-form/index.tsx
index 514bc581..7ef61d8f 100644
--- a/src/components/sections/contact-form/index.tsx
+++ b/src/components/sections/contact-form/index.tsx
@@ -20,20 +20,55 @@ const StyledContactFormWrapper = styled.div`
}
`;
-export const ContactForm = () => {
+interface ContactFormProps {
+ action?: string;
+ submitLabel?: string;
+ defaultValues?: {
+ name?: string;
+ email?: string;
+ message?: string;
+ };
+ placeholders?: {
+ name?: string;
+ email?: string;
+ message?: string;
+ };
+}
+
+export const ContactForm = ({
+ action = 'https://accounts.httptoolkit.tech/api/contact-form',
+ submitLabel = 'Submit the form',
+ defaultValues,
+ placeholders,
+}: ContactFormProps) => {
return (
-
diff --git a/src/content/data/footer-columns.ts b/src/content/data/footer-columns.ts
index bc860349..1cf33298 100644
--- a/src/content/data/footer-columns.ts
+++ b/src/content/data/footer-columns.ts
@@ -21,6 +21,7 @@ const {
PROD_FOR_LINUX,
PROD_FOR_MAC_OS,
PROD_FOR_WINDOW,
+ REQUEST_STUDENT_ACCOUNT,
} = pageRoutes;
export interface FooterColumn {
@@ -37,7 +38,7 @@ export interface FooterColumn {
export const footerColumns: FooterColumn[] = [
{
title: 'Product',
- links: [PROD_FOR_MAC_OS, PROD_FOR_WINDOW, PROD_FOR_LINUX, PRICING],
+ links: [PROD_FOR_MAC_OS, PROD_FOR_WINDOW, PROD_FOR_LINUX, PRICING, REQUEST_STUDENT_ACCOUNT],
subHeading: [
{
title: 'Projects',
diff --git a/src/lib/constants/routes.ts b/src/lib/constants/routes.ts
index 1af68106..40cacfe7 100644
--- a/src/lib/constants/routes.ts
+++ b/src/lib/constants/routes.ts
@@ -99,6 +99,10 @@ export const pageRoutes = {
href: '/http-toolkit-for-linux/',
label: 'HTTP Toolkit for Linux',
},
+ REQUEST_STUDENT_ACCOUNT: {
+ href: '/request-student-account/',
+ label: 'Student Discount',
+ },
};
export interface PageRoute {