307 lines
12 KiB
TypeScript
307 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useState, FormEvent, useEffect } from "react";
|
|
import { useAuth } from "@/lib/AuthContext";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { Mail, ArrowLeft, Shield } from "lucide-react";
|
|
import ErrorFeedback from "@/components/ErrorFeedback";
|
|
|
|
const getCasdoorUrl = () => {
|
|
const state = Math.random().toString(36).slice(2, 10);
|
|
const base = process.env.NEXT_PUBLIC_CASDOOR_SERVER_URL || "https://auth.falahos.my";
|
|
const clientId = process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID || "272f91105da5cf984773";
|
|
const redirect = process.env.NEXT_PUBLIC_CASDOOR_REDIRECT_URI ||
|
|
"https://mobile2.falah-os.com/auth/callback";
|
|
return `${base}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirect)}&response_type=code&scope=openid+profile+email&state=${state}`;
|
|
};
|
|
|
|
type AuthMode = "login" | "register";
|
|
type PageState = "select" | "email-form";
|
|
|
|
function AuthPageInner() {
|
|
const { user, login, register, loading: authLoading } = useAuth();
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
|
|
// Redirect to main page if already logged in (e.g. after OAuth popup completes)
|
|
useEffect(() => {
|
|
if (user) router.push("/");
|
|
}, [user, router]);
|
|
|
|
const [pageState, setPageState] = useState<PageState>("select");
|
|
const [mode, setMode] = useState<AuthMode>("register");
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const handleEmailClick = () => {
|
|
setError("");
|
|
setPageState("email-form");
|
|
};
|
|
|
|
const toggleMode = () => {
|
|
setMode((m) => (m === "login" ? "register" : "login"));
|
|
setError("");
|
|
};
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
|
|
if (!email.trim() || !password.trim()) {
|
|
setError("Please fill in all fields");
|
|
return;
|
|
}
|
|
if (mode === "register" && !name.trim()) {
|
|
setError("Please enter your name");
|
|
return;
|
|
}
|
|
if (mode === "register" && password.length < 6) {
|
|
setError("Password must be at least 6 characters");
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
try {
|
|
if (mode === "login") {
|
|
await login(email.trim(), password);
|
|
} else {
|
|
await register(email.trim(), name.trim(), password);
|
|
}
|
|
router.push("/");
|
|
} catch (err: unknown) {
|
|
const message =
|
|
err instanceof Error ? err.message : "Something went wrong. Please try again.";
|
|
setError(message);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// ── Select State ──────────────────────────────────────────────────────────
|
|
if (pageState === "select") {
|
|
return (
|
|
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col items-center justify-center px-5">
|
|
<div className="w-full max-w-sm">
|
|
{/* Logo / Brand */}
|
|
<div className="text-center mb-8">
|
|
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#D4AF37]/20 to-[#D4AF37]/5 border border-[#D4AF37]/30 flex items-center justify-center mx-auto mb-4">
|
|
<span className="text-3xl">☪️</span>
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-white">Sign in with Ummah ID</h1>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Your Ummah ID works across all Falah apps
|
|
</p>
|
|
</div>
|
|
|
|
{/* Sign-in Methods */}
|
|
<div className="space-y-3">
|
|
{/* UmmahID OIDC */}
|
|
<a
|
|
href={getCasdoorUrl()}
|
|
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-green-900/20 border border-green-600/30 active:bg-green-900/30 transition-all touch-manipulation"
|
|
>
|
|
<div className="w-6 h-6 shrink-0 flex items-center justify-center">
|
|
<Shield size={22} className="text-green-400" />
|
|
</div>
|
|
<div className="text-left">
|
|
<span className="text-sm font-medium text-green-400 block">
|
|
Sign in with UmmahID
|
|
</span>
|
|
<span className="text-xs text-gray-600">FalahOS Identity Service</span>
|
|
</div>
|
|
</a>
|
|
|
|
{/* Email */}
|
|
<button
|
|
onClick={handleEmailClick}
|
|
disabled={authLoading}
|
|
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-[#D4AF37]/10 border border-[#D4AF37]/25 active:bg-[#D4AF37]/20 disabled:opacity-50 transition-all touch-manipulation"
|
|
>
|
|
<div className="w-6 h-6 shrink-0 flex items-center justify-center">
|
|
<Mail size={22} className="text-[#D4AF37]" />
|
|
</div>
|
|
<span className="text-sm font-medium text-[#D4AF37]">
|
|
Continue with Email
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Email Form State ──────────────────────────────────────────────────────
|
|
return (
|
|
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col items-center justify-center px-5">
|
|
<div className="w-full max-w-sm">
|
|
{/* Back button */}
|
|
<button
|
|
onClick={() => setPageState("select")}
|
|
className="flex items-center gap-1.5 text-xs text-gray-500 mb-6 active:text-gray-300 transition-colors touch-manipulation"
|
|
>
|
|
<ArrowLeft size={16} />
|
|
All sign-in methods
|
|
</button>
|
|
|
|
{/* Header */}
|
|
<div className="text-center mb-7">
|
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#D4AF37]/20 to-[#D4AF37]/5 border border-[#D4AF37]/30 flex items-center justify-center mx-auto mb-3">
|
|
<Mail size={22} className="text-[#D4AF37]" />
|
|
</div>
|
|
<h1 className="text-xl font-bold text-white">
|
|
{mode === "login" ? "Sign In" : "Create Account"}
|
|
</h1>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
{mode === "login"
|
|
? "Welcome back! Sign in to continue"
|
|
: "Start your Islamic lifestyle journey"}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<ErrorFeedback error={error} kind="auth" onRetry={() => setError("")} context="authentication" />
|
|
)}
|
|
|
|
{/* Form */}
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Name field (register only) */}
|
|
{mode === "register" && (
|
|
<div>
|
|
<label
|
|
htmlFor="auth-name"
|
|
className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider"
|
|
>
|
|
Name
|
|
</label>
|
|
<input
|
|
id="auth-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Your name"
|
|
autoComplete="name"
|
|
className="w-full bg-[#111118] border border-gray-800/60 rounded-xl px-4 py-3.5 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Email */}
|
|
<div>
|
|
<label
|
|
htmlFor="auth-email"
|
|
className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider"
|
|
>
|
|
Email
|
|
</label>
|
|
<input
|
|
id="auth-email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="you@example.com"
|
|
autoComplete="email"
|
|
autoFocus
|
|
className="w-full bg-[#111118] border border-gray-800/60 rounded-xl px-4 py-3.5 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div>
|
|
<label
|
|
htmlFor="auth-password"
|
|
className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider"
|
|
>
|
|
Password
|
|
</label>
|
|
<input
|
|
id="auth-password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder={mode === "register" ? "At least 6 characters" : "••••••••"}
|
|
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
className="w-full bg-[#111118] border border-gray-800/60 rounded-xl px-4 py-3.5 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Submit */}
|
|
<button
|
|
type="submit"
|
|
disabled={submitting}
|
|
className="w-full py-3.5 rounded-xl bg-[#D4AF37] text-[#0a0a0f] font-semibold text-sm active:opacity-80 disabled:opacity-50 transition touch-manipulation"
|
|
>
|
|
{submitting ? (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<span className="w-4 h-4 rounded-full border-2 border-[#0a0a0f]/30 border-t-[#0a0a0f] animate-spin" />
|
|
{mode === "login" ? "Signing in..." : "Creating account..."}
|
|
</span>
|
|
) : mode === "login" ? (
|
|
"Sign In"
|
|
) : (
|
|
"Create Account"
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Toggle mode */}
|
|
<div className="text-center mt-6">
|
|
<p className="text-xs text-gray-600">
|
|
{mode === "login" ? (
|
|
<>
|
|
Don't have an account?{" "}
|
|
<button
|
|
type="button"
|
|
onClick={toggleMode}
|
|
className="text-[#D4AF37] font-medium hover:underline touch-manipulation"
|
|
>
|
|
Create one
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
Already have an account?{" "}
|
|
<button
|
|
type="button"
|
|
onClick={toggleMode}
|
|
className="text-[#D4AF37] font-medium hover:underline touch-manipulation"
|
|
>
|
|
Sign in
|
|
</button>
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* New Member Bonus (register only) */}
|
|
{mode === "register" && (
|
|
<div className="mt-6 rounded-xl bg-[#111118] border border-gray-800/60 p-3 text-center">
|
|
<p className="text-xs text-gray-600 uppercase tracking-wider mb-1">
|
|
New Member Bonus
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
Get <span className="text-[#D4AF37] font-semibold">5,000 FLH</span>{" "}
|
|
free on sign up + 7-day premium trial
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function AuthPage() {
|
|
return (
|
|
<Suspense fallback={
|
|
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center">
|
|
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
|
|
</div>
|
|
}>
|
|
<AuthPageInner />
|
|
</Suspense>
|
|
);
|
|
}
|