Skip to content
Open
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
14 changes: 13 additions & 1 deletion sentry.edge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,21 @@ if (process.env.NODE_ENV === "production") {
dsn: "https://d4d2dfa789477bdaeea1a7e468f54d08@o4509168932880384.ingest.de.sentry.io/4509168934125648",

// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
tracesSampleRate: 0.2,

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

beforeSend(event, hint) {
const error = hint.originalException;
if (
error instanceof Error &&
"code" in error &&
(error as Error & { code: string }).code === "FORBIDDEN"
) {
return null;
}
return event;
},
});
}
14 changes: 13 additions & 1 deletion sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ if (process.env.NODE_ENV === "production") {
dsn: "https://d4d2dfa789477bdaeea1a7e468f54d08@o4509168932880384.ingest.de.sentry.io/4509168934125648",

// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
tracesSampleRate: 0.2,

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

beforeSend(event, hint) {
const error = hint.originalException;
if (
error instanceof Error &&
"code" in error &&
(error as Error & { code: string }).code === "FORBIDDEN"
) {
return null;
}
return event;
},
});
}
64 changes: 64 additions & 0 deletions src/app/(main)/pools/[address]/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client"; // Error components must be Client Components

import { RotateCw, TriangleAlert } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { ContentContainer } from "~/components/layout/content-container";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Button } from "~/components/ui/button";

interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void;
}

export default function PoolError({ error, reset }: ErrorPageProps) {
useEffect(() => {
// Log the error to an error reporting service
console.error("Pool error:", error);
}, [error]);

return (
<ContentContainer>
<div className="flex flex-col items-center justify-center text-center h-[calc(100vh-200px)] px-4 max-w-md mx-auto">
<div className="rounded-full bg-destructive/10 p-6 mb-8">
<TriangleAlert className="h-12 w-12 text-destructive" />
</div>

<h2 className="text-3xl font-bold mb-4 text-foreground">
Unable to Load Pool
</h2>

<p className="mb-6 text-muted-foreground">
{error.message ||
"An unexpected error occurred while loading this pool."}
</p>

{error.digest && (
<Alert variant="destructive" className="mb-8 text-left">
<AlertTitle>Error Details</AlertTitle>
<AlertDescription className="font-mono text-xs break-all">
{error.digest}
</AlertDescription>
</Alert>
)}

<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<Button
onClick={() => reset()}
variant="default"
className="w-full flex items-center justify-center gap-2"
>
<RotateCw className="h-4 w-4" />
Try Again
</Button>
<Link href="/pools" className="w-full sm:w-auto">
<Button variant="outline" className="w-full">
Go Home
</Button>
</Link>
</div>
</div>
</ContentContainer>
);
}
5 changes: 4 additions & 1 deletion src/app/(main)/pools/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { getAddress } from "viem";
import { getAddress, isAddress } from "viem";
import {
getContractIndex,
getSwapPool,
Expand All @@ -25,6 +25,9 @@ type Props = {

export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params;
if (!isAddress(params.address)) {
return { title: "Invalid Pool Address" };
}
const pool_address = getAddress(params.address);

const poolDetails = await getSwapPool(publicClient, pool_address);
Expand Down
18 changes: 15 additions & 3 deletions src/app/(main)/pools/[address]/pool-client-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TagIcon } from "lucide-react";
import Image from "next/image";
import { useParams } from "next/navigation";
import { toast } from "sonner";
import { getAddress } from "viem";
import { getAddress, isAddress } from "viem";
import Address from "~/components/address";
import { EditableImageOverlay } from "~/components/editable-image-overlay";
import { ContentContainer } from "~/components/layout/content-container";
Expand All @@ -20,9 +20,12 @@ import { PoolTabs } from "./pool-tabs";

export function PoolClientPage() {
const { address } = useParams<{ address: string }>();
const pool_address = getAddress(address);
const isValid = isAddress(address);
const pool_address = isValid ? getAddress(address) : ("0x0000000000000000000000000000000000000000" as `0x${string}`);
const { data: pool } = useSwapPool(pool_address);
const { data: metadata } = trpc.pool.get.useQuery(pool_address);
const { data: metadata } = trpc.pool.get.useQuery(pool_address, {
enabled: isValid,
});

const isOwner = useIsContractOwner(pool_address);
const auth = useAuth();
Expand All @@ -47,6 +50,15 @@ export function PoolClientPage() {
}
};

if (!isValid) {
return (
<ContentContainer>
<div className="flex flex-col items-center justify-center text-center h-[calc(100vh-200px)]">
<p className="text-muted-foreground">Invalid pool address</p>
</div>
</ContentContainer>
);
}
return (
<ContentContainer title={metadata?.pool_name ?? pool?.name ?? ""} className="bg-transparent">
{/* Modern Hero Section */}
Expand Down
64 changes: 64 additions & 0 deletions src/app/(main)/vouchers/[address]/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client"; // Error components must be Client Components

import { RotateCw, TriangleAlert } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { ContentContainer } from "~/components/layout/content-container";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Button } from "~/components/ui/button";

interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void;
}

export default function VoucherError({ error, reset }: ErrorPageProps) {
useEffect(() => {
// Log the error to an error reporting service
console.error("Voucher error:", error);
}, [error]);

return (
<ContentContainer>
<div className="flex flex-col items-center justify-center text-center h-[calc(100vh-200px)] px-4 max-w-md mx-auto">
<div className="rounded-full bg-destructive/10 p-6 mb-8">
<TriangleAlert className="h-12 w-12 text-destructive" />
</div>

<h2 className="text-3xl font-bold mb-4 text-foreground">
Unable to Load Voucher
</h2>

<p className="mb-6 text-muted-foreground">
{error.message ||
"An unexpected error occurred while loading this voucher."}
</p>

{error.digest && (
<Alert variant="destructive" className="mb-8 text-left">
<AlertTitle>Error Details</AlertTitle>
<AlertDescription className="font-mono text-xs break-all">
{error.digest}
</AlertDescription>
</Alert>
)}

<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<Button
onClick={() => reset()}
variant="default"
className="w-full flex items-center justify-center gap-2"
>
<RotateCw className="h-4 w-4" />
Try Again
</Button>
<Link href="/vouchers" className="w-full sm:w-auto">
<Button variant="outline" className="w-full">
Go Home
</Button>
</Link>
</div>
</div>
</ContentContainer>
);
}
23 changes: 17 additions & 6 deletions src/components/file-uploader/use-file-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const useFileUpload = () => {

const xhr = new XMLHttpRequest();
xhr.open("POST", "https://storage.sarafu.network/v1/upload");
xhr.timeout = 30_000;

xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
Expand All @@ -20,9 +21,17 @@ const useFileUpload = () => {
};

return new Promise<string>((resolve, reject) => {
xhr.onerror = (e) => {
console.error(e);
reject(new Error("File upload failed"));
xhr.ontimeout = () => {
toast.error("File upload timed out. Please try again.");
reject(new Error("File upload timed out"));
};
xhr.onerror = () => {
console.error("File upload network error", {
status: xhr.status,
statusText: xhr.statusText,
});
toast.error("File upload failed. Please check your connection.");
reject(new Error(`File upload failed: network error`));
};
xhr.onload = () => {
if (xhr.status === 200) {
Expand All @@ -37,11 +46,13 @@ const useFileUpload = () => {
resolve(response.payload.s3);
} else {
toast.error("File upload failed");
reject(new Error("File upload failed"));
reject(new Error("File upload failed: server returned ok=false"));
}
} else {
toast.error("File upload failed");
reject(new Error("File upload failed"));
const errorMsg = `File upload failed (HTTP ${xhr.status})`;
console.error(errorMsg, xhr.responseText?.slice(0, 200));
toast.error(errorMsg);
reject(new Error(errorMsg));
}
};

Expand Down
10 changes: 10 additions & 0 deletions src/components/file-uploader/webcam-capture.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CameraIcon, RotateCcwIcon, XIcon } from "lucide-react";
import { forwardRef, useState } from "react";
import Webcam from "react-webcam";
import { toast } from "sonner";
import { Button } from "../ui/button";

const WebcamCapture = forwardRef<
Expand All @@ -18,6 +19,14 @@ const WebcamCapture = forwardRef<
}));
};

const handleCameraError = (error: string | DOMException) => {
console.error("Camera error:", error);
toast.error(
"Camera access failed. Please check permissions or use file upload instead."
);
props.onCancel();
};

return (
<div className="fixed inset-0 w-full h-full z-50 flex items-center justify-center bg-muted">
<Webcam
Expand All @@ -26,6 +35,7 @@ const WebcamCapture = forwardRef<
screenshotFormat="image/jpeg"
className="border-2 border-gray-300 mx-auto w-full"
videoConstraints={videoConstraints}
onUserMediaError={handleCameraError}
/>
<div className="absolute bottom-2 gap-4 left-[50%] translate-x-[-50%] flex items-center justify-center">
<Button
Expand Down
18 changes: 9 additions & 9 deletions src/components/voucher/voucher-statistics-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,19 @@ export function VoucherStatisticsGrid({
<div className="grid w-fill gap-2 md:gap-4 grid-cols-2 md:grid-cols-4 items-center">
<StatisticsCard
isLoading={isLoading}
value={stats?.transactions.total.toString() || "0"}
value={stats?.transactions?.total.toString() || "0"}
title="Transactions"
Icon={Icons.hash}
delta={stats?.transactions.delta || 0}
isIncrease={(stats?.transactions.delta || 0) > 0}
delta={stats?.transactions?.delta || 0}
isIncrease={(stats?.transactions?.delta || 0) > 0}
/>
<StatisticsCard
isLoading={isLoading}
value={stats?.accounts.total.toString() || "0"}
value={stats?.accounts?.total.toString() || "0"}
title="Active Users"
Icon={Icons.person}
delta={stats?.accounts.delta || 0}
isIncrease={(stats?.accounts.delta || 0) > 0}
delta={stats?.accounts?.delta || 0}
isIncrease={(stats?.accounts?.delta || 0) > 0}
/>
<StatisticsCard
isLoading={isLoading}
Expand All @@ -77,15 +77,15 @@ export function VoucherStatisticsGrid({
<StatisticsCard
isLoading={isLoading}
value={toUserUnitsString(
stats?.volume.total || BigInt(0),
stats?.volume?.total || BigInt(0),
details?.decimals
)}
title="Volume"
Icon={Icons.hash}
delta={parseFloat(
toUserUnitsString(stats?.volume.delta || BigInt(0), details?.decimals)
toUserUnitsString(stats?.volume?.delta || BigInt(0), details?.decimals)
)}
isIncrease={(stats?.volume.delta || BigInt(0)) > 0}
isIncrease={(stats?.volume?.delta || BigInt(0)) > 0}
/>
</div>
);
Expand Down
9 changes: 9 additions & 0 deletions src/contracts/writer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/nextjs";
import { createWalletClient } from "viem";
import { nonceManager, privateKeyToAccount } from "viem/accounts";
import { celo } from "viem/chains";
Expand Down Expand Up @@ -81,6 +82,14 @@ export async function withWriterLock<T>(fn: () => Promise<T>): Promise<T> {
const lockId = await acquireWriterLock();
try {
return await fn();
} catch (error) {
Sentry.addBreadcrumb({
category: "writer",
message: "Writer lock transaction failed",
data: { lockId },
level: "error",
});
throw error;
} finally {
await releaseWriterLock(lockId);
}
Expand Down
14 changes: 13 additions & 1 deletion src/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@ if (process.env.NODE_ENV === "production") {
dsn: "https://d4d2dfa789477bdaeea1a7e468f54d08@o4509168932880384.ingest.de.sentry.io/4509168934125648",

// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
tracesSampleRate: 0.2,

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

ignoreErrors: [
/router state header.*could not be parsed/i,
/Failed to execute 'removeChild' on 'Node'/,
/Failed to execute 'insertBefore' on 'Node'/,
/Failed to execute 'appendChild' on 'Node'/,
/setPhotoOptions failed/,
/The associated Track is in an invalid state/,
/Proposal expired/,
/Connection interrupted/,
/Error invoking postEvent/,
],
});
}

Expand Down
Loading
Loading