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
9,512 changes: 0 additions & 9,512 deletions public/legacy-recovery-safes.json

This file was deleted.

26,294 changes: 26,294 additions & 0 deletions public/safe-recovery-data.csv

Large diffs are not rendered by default.

122 changes: 88 additions & 34 deletions src/components/ui/incident-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,110 @@
import { AlertTriangle } from "lucide-react";
import { AlertTriangle, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { cn } from "@/utils/cn";
import { GNOSIS_PAY_STATUS_URL } from "@/constants";
import { Link } from "react-router-dom";
import { isAddress, type Address } from "viem";
import { useSafeMigration } from "@/hooks/useSafeMigration";
import { useSafeRecoveryData } from "@/hooks/useSafeRecoveryData";
import {
createDismissalData,
getBannerDismissalData,
setBannerDismissalData,
shouldShowBanner,
} from "@/utils/bannerUtils";

interface IncidentBannerProps {
className?: string;
}

function MoreInformationLink({ testId }: { testId: string }) {
return (
<a
href={GNOSIS_PAY_STATUS_URL}
target="_blank"
rel="noopener noreferrer"
data-testid={testId}
className="text-destructive underline hover:text-destructive/80"
>
More information
</a>
);
}

export function IncidentBanner({ className }: IncidentBannerProps) {
const { hasOldSafe, oldSafe, isLoading: isMigrationLoading } = useSafeMigration();
const oldSafeAddress = oldSafe?.address && isAddress(oldSafe.address) ? (oldSafe.address as Address) : undefined;
const { affected, hasPreHackBalance, isLoading: isDataLoading } = useSafeRecoveryData(oldSafeAddress);
const [isDismissed, setIsDismissed] = useState(true);

useEffect(() => {
const dismissalData = getBannerDismissalData("incident");
setIsDismissed(!shouldShowBanner(dismissalData));
}, []);

const handleDismiss = useCallback(() => {
const currentData = getBannerDismissalData("incident");
const newData = createDismissalData(currentData);
setBannerDismissalData(newData, "incident");
setIsDismissed(true);
}, []);

if (isDismissed || !hasOldSafe || isMigrationLoading || isDataLoading || affected === undefined || hasPreHackBalance === undefined) {
return null;
}

return (
<div
data-testid="incident-notice-banner"
className={cn(
"block w-full rounded-lg bg-destructive/15 mb-6",
"relative block w-full rounded-lg mb-6",
affected ? "bg-destructive/15" : "bg-warning/15",
className
)}
role="alert"
>
<button
type="button"
onClick={handleDismiss}
className="absolute top-2 right-2 p-1 rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer z-10"
aria-label="Dismiss banner"
data-testid="incident-notice-banner-dismiss"
>
<X size={14} className="text-foreground" />
</button>

<div className="flex items-start gap-4 p-5 sm:p-6">
<div
className="shrink-0 flex items-center justify-center size-12 sm:size-14 rounded-full bg-destructive"
aria-hidden
>
<AlertTriangle size={28} className="text-destructive-foreground" />
</div>

<div className="flex-1 min-w-0">
<h2 className="text-foreground text-base sm:text-lg leading-tight">
Normal operations have been suspended in response to a security incident. The issue has now been contained and we will be resuming operations over the coming days. User funds are not at risk.<br /><br />
In the meantime:<br />
- do not send funds to your Card account<br />
- do not use IBAN<br />
</h2>
<p className="mt-1 text-sm sm:text-base text-muted-foreground leading-snug">
<MoreInformationLink
testId="incident-notice-more-info"
/>
className={cn(
"shrink-0 flex items-center justify-center size-12 sm:size-14 rounded-full",
affected ? "bg-destructive" : "bg-warning"
)}
aria-hidden
>
<AlertTriangle size={28} className={affected ? "text-destructive-foreground" : "text-background"} />
</div>

<div className="flex-1 min-w-0 pr-6">
<p className="font-bold text-foreground text-base sm:text-lg leading-tight">
Your Gnosis Pay card is back up and running.
</p>

{!affected && hasPreHackBalance && (
<>
<p className="mt-2 text-sm sm:text-base text-foreground leading-snug">
To keep your account secure, we issued you a new Gnosis Pay Safe. Your funds are safe, but you'll need to move them from your old Safe before you can spend with your card.
</p>
<Link
to="/withdraw-legacy"
className="mt-3 inline-flex items-center rounded-md bg-warning px-4 py-2 text-sm font-medium text-background hover:bg-warning/90 transition-colors"
>
Move my funds
</Link>
</>
)}

{!affected && !hasPreHackBalance && (
<p className="mt-2 text-sm sm:text-base text-foreground leading-snug">
To keep your account secure, we issued you a new Gnosis Pay Safe. Please only use the new Safe address going forward.
</p>
)}

{affected && hasPreHackBalance && (
<p className="mt-2 text-sm sm:text-base text-foreground leading-snug">
We've restored your balance and issued you a new Gnosis Pay Safe. Your previous Safe is no longer secure. Do not use your old Safe address again: anything you send there will be lost.
</p>
)}

{affected && !hasPreHackBalance && (
<p className="mt-2 text-sm sm:text-base text-foreground leading-snug">
We've issued you a new Gnosis Pay Safe because your previous Safe is no longer secure. Do not use your old Safe address again: anything you send there will be lost.
</p>
)}
</div>
</div>
</div>
Expand Down
93 changes: 0 additions & 93 deletions src/components/ui/legacy-safe-recovery-banner.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1641,5 +1641,3 @@ export const PARTNERS_URL = "https://gnosispay.com/apps";
export const PARTNER_ID = "cmfo3b7c806fj8bbnp3zad7rb";

export const REFUND_ADDRESS = "0x4822521E6135CD2599199c83Ea35179229A172EE";

export const GNOSIS_PAY_STATUS_URL = "https://x.com/gnosispay";
53 changes: 0 additions & 53 deletions src/hooks/useIsLegacyRecoverySafe.ts

This file was deleted.

56 changes: 56 additions & 0 deletions src/hooks/useSafeRecoveryData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useState } from "react";
import type { Address } from "viem";
import { loadSafeRecoveryData } from "@/utils/safeRecoveryData";

interface UseSafeRecoveryDataResult {
affected: boolean | undefined;
hasPreHackBalance: boolean | undefined;
isLoading: boolean;
}

export const useSafeRecoveryData = (address: Address | undefined): UseSafeRecoveryDataResult => {
const [affected, setAffected] = useState<boolean | undefined>(undefined);
const [hasPreHackBalance, setHasPreHackBalance] = useState<boolean | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
if (!address) {
setAffected(undefined);
setHasPreHackBalance(undefined);
setIsLoading(false);
return;
}

let active = true;
setIsLoading(true);

loadSafeRecoveryData()
.then((data) => {
if (!active) return;
const entry = data[address.toLowerCase()];
if (entry) {
setAffected(entry.affected);
setHasPreHackBalance(entry.preHackBalanceUsd > 0);
} else {
setAffected(undefined);
setHasPreHackBalance(undefined);
}
})
.catch((error) => {
console.error("Error loading safe recovery data:", error);
if (active) {
setAffected(undefined);
setHasPreHackBalance(undefined);
}
})
.finally(() => {
if (active) setIsLoading(false);
});

return () => {
active = false;
};
}, [address]);

return { affected, hasPreHackBalance, isLoading };
};
2 changes: 0 additions & 2 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { Link } from "react-router-dom";
import { StatusHelpIcon } from "@/components/ui/status-help-icon";
import { PartnerBanner } from "@/components/ui/partner-banner";
import { IncidentBanner } from "@/components/ui/incident-banner";
import { LegacySafeRecoveryBanner } from "@/components/ui/legacy-safe-recovery-banner";
import { UnspendableAmountAlert } from "@/components/unspendable-amount-alert";

export const Home = () => {
Expand All @@ -23,7 +22,6 @@ export const Home = () => {
<div className="grid grid-cols-6 gap-4 h-full mt-4">
<div className="col-span-6 px-4 lg:px-0">
<IncidentBanner />
<LegacySafeRecoveryBanner />
</div>
<div className="col-span-6 lg:col-start-2 lg:col-span-4">
<div className="mx-4 lg:mx-0">
Expand Down
5 changes: 4 additions & 1 deletion src/utils/bannerUtils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
const PARTNER_BANNER_STORAGE_KEY = "gp-ui.partner-banner-dismissed.v2";
const LEGACY_SAFE_RECOVERY_BANNER_STORAGE_KEY = "gp-ui.legacy-safe-recovery-banner-dismissed.v1";
const INCIDENT_BANNER_STORAGE_KEY = "gp-ui.incident-banner-dismissed.v1";

export interface BannerDismissalData {
nextShowTimestamp: number;
count: number;
}

export type BannerType = "partner" | "legacy-safe-recovery";
export type BannerType = "partner" | "legacy-safe-recovery" | "incident";

function getBannerStorageKey(bannerType: BannerType): string {
switch (bannerType) {
case "legacy-safe-recovery":
return LEGACY_SAFE_RECOVERY_BANNER_STORAGE_KEY;
case "incident":
return INCIDENT_BANNER_STORAGE_KEY;
default:
return PARTNER_BANNER_STORAGE_KEY;
}
Expand Down
Loading
Loading