Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
03aa318
Initial plan
Copilot Mar 7, 2026
7dbdadf
feat: migrate Signin.tsx to Better Auth native auth + passkey endpoints
Copilot Mar 7, 2026
49e5b28
Initial plan
Copilot Mar 7, 2026
3c1b820
Initial plan
Copilot Mar 7, 2026
919d0ae
refactor: use Better Auth get-session as auth source of truth in Auth…
Copilot Mar 7, 2026
cdddc80
Migrate Signup.tsx to Better Auth signup + passkey registration flow
Copilot Mar 7, 2026
420c30c
Remove package-lock.json artifact from PR
Copilot Mar 7, 2026
aced66f
feat: session-first fetchUser, add Signin.test.tsx, fix input ids for…
Copilot Mar 7, 2026
bf3de29
Revert .gitignore changes
Copilot Mar 8, 2026
48affbb
Merge PR #36: Better Auth signin + passkey migration
benhalverson Mar 8, 2026
2dac67a
Merge PR #38: AuthContext session source-of-truth refactor
benhalverson Mar 8, 2026
79ad0be
Merge PR #37: Better Auth signup + passkey registration flow
benhalverson Mar 8, 2026
590e312
fix(auth): migrate profile passkey calls to /api/auth/passkey
benhalverson Mar 8, 2026
1883649
fix(passkey): include credential aliases for verify-registration payload
benhalverson Mar 8, 2026
88719d4
fix(passkey): send full credential object in verify-registration resp…
benhalverson Mar 8, 2026
7f6dc4e
refactor(passkey): send minimal verify-registration payload
benhalverson Mar 8, 2026
7c4a7c0
fix(auth): address PR review follow-ups
benhalverson Mar 8, 2026
0187f0d
style: resolve biome findings
benhalverson Mar 8, 2026
e325449
style: apply biome formatting to auth changes
benhalverson Mar 8, 2026
3394932
test: stabilize ColorPicker loading assertion
benhalverson Mar 8, 2026
aea9e40
test: fix product test OOM and serialize test runs
benhalverson Mar 8, 2026
7344362
fixed passkey issues
benhalverson Mar 8, 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
5 changes: 3 additions & 2 deletions src/components/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const Gallery: React.FC<GalleryProps> = ({
<div className="flex flex-row gap-4 justify-center mt-4">
{images.map((src, index) => {
return (
<div
<button
type="button"
key={`${src}-${index}`}
className={`relative cursor-pointer rounded-lg overflow-hidden border-2 hover:border-indigo-500 ${
selectedIndex === index
Expand All @@ -38,7 +39,7 @@ const Gallery: React.FC<GalleryProps> = ({
loading="lazy"
decoding="async"
/>
</div>
</button>
);
})}
</div>
Expand Down
38 changes: 29 additions & 9 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ interface User {
lastName?: string;
}

type FetchUserResult = User | null;

interface AuthContextType {
user: User | null;
loading: boolean;
setUser: (user: User | null) => void;
fetchUser: () => Promise<void>;
fetchUser: () => Promise<FetchUserResult>;
}

const AuthContext = createContext<AuthContextType | null>(null);
Expand All @@ -26,27 +28,45 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

const fetchUser = async () => {
const fetchUser = async (): Promise<FetchUserResult> => {
try {
const res = await fetch(`${BASE_URL}/profile`, {
const sessionRes = await fetch(`${BASE_URL}/api/auth/get-session`, {
credentials: "include",
});
if (res.ok) {
const data: any = await res.json();
setUser(data);
} else {

if (!sessionRes.ok) {
throw new Error("Failed to fetch session");
}

const sessionData = await sessionRes.json();

if (!sessionData?.session) {
setUser(null);
return null;
}
} catch {

const profileRes = await fetch(`${BASE_URL}/profile`, {
credentials: "include",
});

if (!profileRes.ok) {
throw new Error("Failed to fetch profile");
}

const data: User = await profileRes.json();
setUser(data);
return data;
} catch (error) {
setUser(null);
throw error instanceof Error ? error : new Error("Failed to fetch user");
} finally {
setLoading(false);
}
};

// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: useEventEffect in 19
useEffect(() => {
fetchUser();
fetchUser().catch(() => undefined);
}, []);

return (
Expand Down
42 changes: 33 additions & 9 deletions src/pages/Checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,40 @@ function isObject(val: unknown): val is Record<string, unknown> {
return typeof val === "object" && val !== null;
}

function parsePaymentIntentResponse(obj: unknown): PaymentIntentResponse | null {
function parsePaymentIntentResponse(
obj: unknown,
): PaymentIntentResponse | null {
if (!isObject(obj)) return null;
const record = obj as Record<string, unknown>;
const checkout_url = typeof record.checkout_url === "string" ? record.checkout_url : undefined;
const clientSecret = typeof record.clientSecret === "string" ? record.clientSecret : undefined;
const client_secret = typeof record.client_secret === "string" ? record.client_secret : undefined;
const checkout_url =
typeof record.checkout_url === "string" ? record.checkout_url : undefined;
const clientSecret =
typeof record.clientSecret === "string" ? record.clientSecret : undefined;
const client_secret =
typeof record.client_secret === "string" ? record.client_secret : undefined;
const amount = typeof record.amount === "number" ? record.amount : undefined;
const currency = typeof record.currency === "string" ? record.currency : undefined;
const currency =
typeof record.currency === "string" ? record.currency : undefined;
const orderId =
typeof record.orderId === "string" || typeof record.orderId === "number"
? (record.orderId as string | number)
: undefined;
if (checkout_url || clientSecret || client_secret || amount || currency || orderId)
return { checkout_url, clientSecret, client_secret, amount, currency, orderId };
if (
checkout_url ||
clientSecret ||
client_secret ||
amount ||
currency ||
orderId
)
return {
checkout_url,
clientSecret,
client_secret,
amount,
currency,
orderId,
};
return null;
}

Expand Down Expand Up @@ -216,7 +236,9 @@ export default function Checkout() {
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Payment intent request failed (${res.status}): ${text}`);
throw new Error(
`Payment intent request failed (${res.status}): ${text}`,
);
}
const dataJson: unknown = await res.json();
const data = parsePaymentIntentResponse(dataJson);
Expand All @@ -240,7 +262,9 @@ export default function Checkout() {
navigate(`/order/${data.orderId}`);
}
} catch (err: unknown) {
setCartError(err instanceof Error ? err.message : "Payment intent failed");
setCartError(
err instanceof Error ? err.message : "Payment intent failed",
);
} finally {
setCartLoading(false);
}
Expand Down
15 changes: 10 additions & 5 deletions src/pages/OrderComplete.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { Link } from "react-router-dom";
import { useEffect } from "react";
import { Link } from "react-router-dom";

export default function OrderComplete() {
useEffect(() => {
try {
localStorage.removeItem("cartId");
} catch (e) {
// ignore localStorage errors in some environments
console.log('Could not clear cartId from localStorage', e);
console.log("Could not clear cartId from localStorage", e);
}
}, []);

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12">
<div className="max-w-xl w-full bg-white shadow rounded-lg p-8 text-center">
<h1 className="text-2xl font-semibold mb-4">Thank you — your order is complete</h1>
<h1 className="text-2xl font-semibold mb-4">
Thank you — your order is complete
</h1>
<p className="text-gray-600 mb-6">
We received your payment. You will receive an email confirmation shortly.
We received your payment. You will receive an email confirmation
shortly.
</p>
<div className="flex justify-center gap-4">
<Link to="/" className="rounded-md bg-indigo-600 text-white px-4 py-2">
<Link
to="/"
className="rounded-md bg-indigo-600 text-white px-4 py-2">
Back to shop
</Link>
<Link to="/profile" className="rounded-md border px-4 py-2">
Expand Down
20 changes: 10 additions & 10 deletions src/pages/Payment.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { loadStripe, type PaymentIntentResult } from "@stripe/stripe-js";
import {
Elements,
PaymentElement,
useStripe,
useElements,
useStripe,
} from "@stripe/react-stripe-js";
import { loadStripe, type PaymentIntentResult } from "@stripe/stripe-js";
import { useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY as string;
const stripePromise = loadStripe(publishableKey);
Expand All @@ -28,7 +28,7 @@ function PaymentForm() {
elements,
confirmParams: {
// You can change return_url to an order confirmation route
return_url: window.location.origin + "/order/complete",
return_url: `${window.location.origin}/order/complete`,
},
redirect: "if_required",
});
Expand Down Expand Up @@ -69,6 +69,10 @@ export default function PaymentPage() {
// Prefer router state (in-memory) to avoid exposing secrets in the URL.
const state = (loc.state as { clientSecret?: string } | null) ?? null;
const clientSecret = state?.clientSecret ?? q.get("client_secret");
const options = useMemo(
() => (clientSecret ? { clientSecret } : undefined),
[clientSecret],
);

if (!publishableKey) {
return (
Expand All @@ -79,13 +83,9 @@ export default function PaymentPage() {
}

if (!clientSecret) {
return (
<div className="p-8 text-center">No payment session available.</div>
);
return <div className="p-8 text-center">No payment session available.</div>;
}

const options = useMemo(() => ({ clientSecret }), [clientSecret]);

return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="mx-auto max-w-2xl px-4">
Expand Down
Loading