-
Offers
-
- {offerCards.map((account) => (
-
- ))}
-
-
diff --git a/app/features/gift-cards/offers-section.tsx b/app/features/gift-cards/offers-section.tsx
new file mode 100644
index 000000000..8ebfd64f0
--- /dev/null
+++ b/app/features/gift-cards/offers-section.tsx
@@ -0,0 +1,156 @@
+import { useNavigate, useViewTransitionState } from 'react-router';
+import {
+ WalletCard,
+ WalletCardBackgroundImage,
+ WalletCardBlank,
+ WalletCardOverlay,
+} from '~/components/wallet-card';
+import type { CashuAccount } from '~/features/accounts/account';
+import useUserAgent from '~/hooks/use-user-agent';
+import { cn } from '~/lib/utils';
+import { CARD_WIDTH } from './card-stack-constants';
+import { getGiftCardImageByUrl } from './gift-card-images';
+import { useRestoreScrollPosition } from './use-restore-scroll-position';
+
+type OfferCardButtonProps = {
+ account: CashuAccount;
+ size?: 'default' | 'sm';
+ isTransitioning: boolean;
+ onClick: () => void;
+ className?: string;
+ cardClassName?: string;
+};
+
+function OfferCardButton({
+ account,
+ size = 'default',
+ isTransitioning,
+ onClick,
+ className,
+ cardClassName,
+}: OfferCardButtonProps) {
+ const image = getGiftCardImageByUrl(account.mintUrl);
+ return (
+
+ );
+}
+
+type OffersSectionProps = {
+ offers: CashuAccount[];
+};
+
+export function OffersSection({ offers }: OffersSectionProps) {
+ const navigate = useNavigate();
+ const isTransitioning = useViewTransitionState(
+ '/gift-cards/offers/:accountId',
+ );
+ const { isMobile } = useUserAgent();
+ const { scrollRef, handleScroll, getScrollState } = useRestoreScrollPosition(
+ 'offersScrollPosition',
+ );
+
+ const navigateToOffer = (account: CashuAccount) => {
+ navigate(`/gift-cards/offers/${account.id}`, {
+ viewTransition: true,
+ state: getScrollState(),
+ });
+ };
+
+ if (offers.length === 0) return null;
+
+ if (offers.length === 1) {
+ const offer = offers[0];
+ return (
+
+
Offers
+
+ navigateToOffer(offer)}
+ className="w-full"
+ />
+
+
+ );
+ }
+
+ if (offers.length === 2) {
+ return (
+
+
Offers
+
+ {offers.map((offer) => (
+ navigateToOffer(offer)}
+ className="min-w-0 flex-1"
+ />
+ ))}
+
+
+ );
+ }
+
+ return (
+
+
Offers
+
+
+
+ {offers.map((offer, index) => (
+ navigateToOffer(offer)}
+ cardClassName={cn(
+ index === 0 && 'ml-4 sm:ml-0',
+ index === offers.length - 1 && 'mr-4 sm:mr-0',
+ )}
+ />
+ ))}
+
+
+
+
+ );
+}
diff --git a/app/features/gift-cards/use-restore-scroll-position.ts b/app/features/gift-cards/use-restore-scroll-position.ts
new file mode 100644
index 000000000..e64e07417
--- /dev/null
+++ b/app/features/gift-cards/use-restore-scroll-position.ts
@@ -0,0 +1,45 @@
+import { useEffect, useRef } from 'react';
+import { useLocation } from 'react-router';
+import { z } from 'zod';
+
+/**
+ * Persists and restores the horizontal scroll position of a container across
+ * navigations. Positions are stored in `location.state` under `state.scrollPositions[stateKey]`.
+ * Pass `getScrollState()` as the `state` argument to `navigate(...)` when
+ * leaving the page so the position gets restored on back navigation.
+ */
+export function useRestoreScrollPosition
(stateKey: K) {
+ const location = useLocation();
+ const scrollRef = useRef(null);
+ const scrollPositionRef = useRef(0);
+
+ useEffect(() => {
+ const result = z
+ .object({ scrollPositions: z.object({ [stateKey]: z.number() }) })
+ .safeParse(location.state);
+
+ if (result.success && scrollRef.current) {
+ scrollRef.current.scrollLeft = result.data.scrollPositions[stateKey];
+ }
+ }, [location.state, stateKey]);
+
+ const handleScroll = () => {
+ if (scrollRef.current) {
+ scrollPositionRef.current = scrollRef.current.scrollLeft;
+ }
+ };
+
+ const getScrollState = () => {
+ const existing = z
+ .object({ scrollPositions: z.record(z.string(), z.number()) })
+ .safeParse(location.state);
+ return {
+ scrollPositions: {
+ ...(existing.success ? existing.data.scrollPositions : {}),
+ [stateKey]: scrollPositionRef.current,
+ },
+ };
+ };
+
+ return { scrollRef, handleScroll, getScrollState };
+}
diff --git a/app/features/receive/receive-scanner.tsx b/app/features/receive/receive-scanner.tsx
index b6f20ac15..ed067536d 100644
--- a/app/features/receive/receive-scanner.tsx
+++ b/app/features/receive/receive-scanner.tsx
@@ -1,5 +1,5 @@
import {
- PageBackButton,
+ ClosePageButton,
PageContent,
PageHeader,
PageHeaderTitle,
@@ -20,7 +20,7 @@ export default function ReceiveScanner() {
return (
<>
-
-
-
-
+
Scan
diff --git a/bunfig.toml b/bunfig.toml
index d38314e06..86ed0c75d 100644
--- a/bunfig.toml
+++ b/bunfig.toml
@@ -1,2 +1,6 @@
[test]
-root = "./app"
\ No newline at end of file
+root = "./app"
+
+[install]
+# 3 days, in seconds — see https://bun.com/docs/runtime/bunfig#install-minimumreleaseage
+minimumReleaseAge = 259200
\ No newline at end of file