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
20 changes: 18 additions & 2 deletions apps/web/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ const loginSchema = z.object({

type LoginFields = z.infer<typeof loginSchema>;

const DEFAULT_LOGIN_DESTINATION = "/home";

function getSafeRedirectDestination(value: string | null): string | null {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
return null;
}

if (value === "/login" || value.startsWith("/login?")) {
return null;
}

return value;
}

function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
Expand All @@ -34,8 +48,10 @@ function LoginForm() {
const onSubmit = async (data: LoginFields) => {
setServerError(null);
try {
const res = await api.post<{ redirectTo: string }>("/auth/login", data);
const destination = searchParams.get("redirect") ?? res.redirectTo;
await api.post("/auth/login", data);
const destination =
getSafeRedirectDestination(searchParams.get("redirect")) ??
DEFAULT_LOGIN_DESTINATION;
router.push(destination);
} catch (err) {
setServerError(
Expand Down
44 changes: 34 additions & 10 deletions apps/web/src/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ function calcAge(dobString: string): number {
return age;
}

const NIGERIAN_PHONE_ERROR = "Enter a valid Nigerian mobile number.";

function normalizeNigerianPhone(input: string): string | null {
const value = input.trim();

if (/^0[789]\d{9}$/.test(value)) {
return `+234${value.slice(1)}`;
}

if (/^[789]\d{9}$/.test(value)) {
return `+234${value}`;
}

if (/^\+234[789]\d{9}$/.test(value)) {
return value;
}

return null;
}

// ─── Zod schemas per step ─────────────────────────────────────────────────────

const step2Schema = z.object({
Expand All @@ -57,10 +77,11 @@ const step2Schema = z.object({
const step3Schema = z.object({
phone: z
.string()
.regex(
/^\+234[789]\d{8}$/,
"Enter a valid Nigerian number (e.g. +2348012345678)",
),
.trim()
.refine((value) => normalizeNigerianPhone(value) !== null, {
message: NIGERIAN_PHONE_ERROR,
})
.transform((value) => normalizeNigerianPhone(value) ?? value),
});

const step4Schema = z.object({
Expand All @@ -69,7 +90,10 @@ const step4Schema = z.object({
.min(1, "Date of birth is required")
.refine((v) => !isNaN(new Date(v).getTime()), "Enter a valid date")
.refine((v) => new Date(v) < new Date(), "Date must be in the past")
.refine((v) => calcAge(v) >= 13, "You must be 13 or older to join twizrr"),
.refine(
(v) => calcAge(v) >= 13,
"You must meet the age requirement to join twizrr",
),
});

const step5Schema = z.object({
Expand Down Expand Up @@ -375,8 +399,8 @@ export default function RegisterPage() {
type="tel"
inputMode="tel"
autoComplete="tel"
placeholder="+2348012345678"
helperText="Include country code: +234 followed by 10 digits"
placeholder="Enter your phone number"
helperText="Use a valid Nigerian mobile number."
error={form3.formState.errors.phone?.message}
{...form3.register("phone")}
/>
Expand All @@ -401,7 +425,7 @@ export default function RegisterPage() {
Date of birth
</h2>
<p className="text-sm text-muted-foreground font-cabinet mb-6">
Required — you must be 13 or older to use twizrr.
Required to confirm account eligibility.
</p>

<form
Expand Down Expand Up @@ -450,7 +474,7 @@ export default function RegisterPage() {
label="Display name"
type="text"
autoComplete="name"
placeholder="Amina Okafor"
placeholder="Your name"
error={form5.formState.errors.displayName?.message}
{...form5.register("displayName")}
/>
Expand All @@ -459,7 +483,7 @@ export default function RegisterPage() {
label="Username"
type="text"
autoComplete="username"
placeholder="aminaokafor"
placeholder="your_username"
helperText="3–30 characters, lowercase letters, numbers, underscores only. You can change your username once in the first 7 days."
error={form5.formState.errors.username?.message}
{...form5.register("username")}
Expand Down
63 changes: 46 additions & 17 deletions apps/web/src/app/(store)/store/setup/StoreSetupClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { AlertTriangle, ArrowRight, CheckCircle2, Store } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { PageLoadingState } from "@/components/ui/brand-loader";
import { Input } from "@/components/ui/Input";
import { Skeleton } from "@/components/ui/Skeleton";

type StoreType = "DIGITAL" | "PHYSICAL";

Expand All @@ -32,6 +32,7 @@ interface ApiErrorBody {
}

const API_BASE = (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, "");
const STORE_CHECK_TIMEOUT_MS = 8_000;

const categoryOptions = [
"Fashion",
Expand All @@ -58,6 +59,31 @@ function normalizeHandle(value: string): string {
.slice(0, 30);
}

function getSafePostSetupRedirect(value: string | null): string {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
return "/store/dashboard";
}

const redirectUrl = new URL(value, window.location.origin);
if (
redirectUrl.pathname === "/store/setup" ||
redirectUrl.pathname === "/store/setup/"
) {
return "/store/dashboard";
}

return value;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function getPostSetupRedirect(): string {
if (typeof window === "undefined") {
return "/store/dashboard";
}

const params = new URLSearchParams(window.location.search);
return getSafePostSetupRedirect(params.get("redirect"));
}

function getErrorMessage(body: ApiErrorBody | null): string {
if (!body) {
return "Something went wrong. Please try again.";
Expand Down Expand Up @@ -119,18 +145,25 @@ export function StoreSetupClient() {
return;
}

const controller = new AbortController();
const timeoutId = window.setTimeout(
() => controller.abort(),
STORE_CHECK_TIMEOUT_MS,
);

try {
const response = await fetch(`${API_BASE}/stores/me`, {
credentials: "include",
headers: { Accept: "application/json" },
signal: controller.signal,
});

if (cancelled) {
return;
}

if (response.ok) {
router.replace("/store/dashboard");
router.replace(getPostSetupRedirect());
return;
}

Expand All @@ -146,6 +179,7 @@ export function StoreSetupClient() {
);
}
} finally {
window.clearTimeout(timeoutId);
if (!cancelled) {
setCheckingStore(false);
}
Expand All @@ -161,7 +195,7 @@ export function StoreSetupClient() {

const handlePreview = useMemo(() => {
const normalized = normalizeHandle(storeHandle);
return normalized ? `@${normalized}` : "@your_store";
return normalized || "your_store_name";
}, [storeHandle]);

const canSubmit =
Expand Down Expand Up @@ -213,13 +247,13 @@ export function StoreSetupClient() {
});

if (response.ok) {
router.replace("/store/dashboard");
router.replace(getPostSetupRedirect());
return;
}

const body = await readErrorBody(response);
if (response.status === 409 && body?.code === "STORE_ALREADY_EXISTS") {
router.replace("/store/dashboard");
router.replace(getPostSetupRedirect());
return;
}

Expand All @@ -232,12 +266,7 @@ export function StoreSetupClient() {
}

if (checkingStore) {
return (
<div className="mx-auto w-full max-w-4xl px-4 py-8 sm:px-6">
<Skeleton className="h-24 w-full bg-[var(--color-bianca)]/10" />
<Skeleton className="mt-6 h-[520px] w-full bg-[var(--color-bianca)]/10" />
</div>
);
return <PageLoadingState label="Checking your store..." />;
}

return (
Expand Down Expand Up @@ -281,7 +310,7 @@ export function StoreSetupClient() {
label="Store name"
value={storeName}
onChange={(event) => setStoreName(event.target.value)}
placeholder="Raseed Wears"
placeholder="Your store name"
maxLength={80}
required
/>
Expand All @@ -291,7 +320,7 @@ export function StoreSetupClient() {
value={storeHandle}
onChange={(event) => setStoreHandle(event.target.value)}
onBlur={() => setStoreHandle(normalizeHandle(storeHandle))}
placeholder="raseed_wears"
placeholder="your_store_name"
helperText="Lowercase letters, numbers, and underscores only."
maxLength={30}
required
Expand Down Expand Up @@ -362,8 +391,8 @@ export function StoreSetupClient() {
onChange={(event) => setFormattedAddress(event.target.value)}
placeholder={
storeType === "PHYSICAL"
? "12 Allen Avenue, Ikeja, Lagos"
: "Yaba, Lagos"
? "Street address, city, state"
: "Your city and state"
}
required
/>
Expand All @@ -373,14 +402,14 @@ export function StoreSetupClient() {
label="City"
value={city}
onChange={(event) => setCity(event.target.value)}
placeholder="Ikeja"
placeholder="City"
required
/>
<Input
label="State"
value={stateName}
onChange={(event) => setStateName(event.target.value)}
placeholder="Lagos"
placeholder="State"
required
/>
</div>
Expand Down
Loading