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
15 changes: 12 additions & 3 deletions example-apps/dashmint-lab/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ const SORT_LABELS: Record<SortKey, string> = {
};
const SORT_ORDER: SortKey[] = ["rarity", "name", "owner", "price"];

function cardPriceValue(card: Card): bigint | null {
const price = card.$price;
if (price === undefined || price === 0 || price === 0n) return null;
return typeof price === "bigint" ? price : BigInt(Math.trunc(price));
}

function App() {
const session = useSession();
const {
Expand Down Expand Up @@ -154,9 +160,12 @@ function App() {
);
} else if (sortKey === "price") {
return [...cards].sort((a, b) => {
const pa = a.$price ? Number(a.$price) : -1;
const pb = b.$price ? Number(b.$price) : -1;
return pb - pa;
const pa = cardPriceValue(a);
const pb = cardPriceValue(b);
if (pa === pb) return 0;
if (pa === null) return 1;
if (pb === null) return -1;
return pa > pb ? -1 : 1;
});
}
return cards;
Expand Down
4 changes: 2 additions & 2 deletions example-apps/dashmint-lab/src/components/CardTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ export function CardTile({
style={{ background: RARITY_RAIL_COLORS[rarity] }}
/>

{/* Header: rarity tag + price */}
<div className="flex items-center justify-between">
{/* Header: reserves chip height so listed and unlisted cards align */}
<div className="flex min-h-[22px] items-center justify-between">
<RarityTag rarity={rarity} />
{hasPrice && (
<span
Expand Down
36 changes: 33 additions & 3 deletions example-apps/dashmint-lab/src/components/PurchaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export function PurchaseModal({
const [submitting, setSubmitting] = useState(false);
const [result, setResult] = useState<OperationResult | null>(null);

const price = card?.$price ?? null;
const insufficientCredits =
session.balance !== null && price !== null && session.balance < price;

useEffect(() => {
if (card) {
setResult(null);
Expand All @@ -42,7 +46,8 @@ export function PurchaseModal({
!session.keyManager ||
!session.contractId ||
card.$price === undefined ||
card.$price === null
card.$price === null ||
insufficientCredits
)
return;
setSubmitting(true);
Expand Down Expand Up @@ -82,16 +87,41 @@ export function PurchaseModal({
</span>
</div>

<div className="flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Your balance
</span>
<span className="text-[13px] font-bold text-ink-2">
{session.balance === null
? "—"
: `${formatCredits(session.balance)} credits`}
</span>
</div>

{insufficientCredits && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
Not enough credits to buy this card.
</p>
)}

{result && <OperationResultNotice result={result} />}

<div className="mt-3 flex gap-2">
<button
type="button"
onClick={handleBuy}
disabled={submitting || result?.kind === "success"}
disabled={
submitting ||
insufficientCredits ||
result?.kind === "success"
}
className="flex-1 rounded-md bg-accent px-4 py-2 text-[13px] font-semibold text-bg transition hover:bg-accent-dim disabled:cursor-not-allowed disabled:bg-surface-2 disabled:text-ink-4"
>
{submitting ? "Purchasing…" : "Buy"}
{submitting
? "Purchasing…"
: insufficientCredits
? "Insufficient credits"
: "Buy"}
</button>
<button
type="button"
Expand Down
25 changes: 21 additions & 4 deletions example-apps/dashmint-lab/src/components/SetPriceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export interface SetPriceModalProps {
}

const SUCCESS_CLOSE_DELAY_MS = 700;
// TODO(dashpay/platform#3786): Remove this app-level cap after the SDK can
// safely serialize document prices above Number.MAX_SAFE_INTEGER.
export const MAX_PRICE_CREDITS = 1_000_000_000_000_000;
const MAX_PRICE_CREDITS_BIGINT = BigInt(MAX_PRICE_CREDITS);
const MAX_PRICE_ERROR = `Price must be between 1 and ${formatCredits(MAX_PRICE_CREDITS)} credits.`;

export function SetPriceModal({ card, onClose, onPriced }: SetPriceModalProps) {
const session = useSession();
Expand Down Expand Up @@ -66,9 +71,16 @@ export function SetPriceModal({ card, onClose, onPriced }: SetPriceModalProps) {

async function handleSubmit(e: FormEvent) {
e.preventDefault();
const n = parseInt(amount, 10);
if (!Number.isFinite(n) || n < 1) return;
await submitPrice(n);
const value = amount.trim();
if (!/^\d+$/.test(value)) return;

const price = BigInt(value);
if (price < 1n) return;
if (price > MAX_PRICE_CREDITS_BIGINT) {
setResult({ kind: "error", message: MAX_PRICE_ERROR });
return;
}
await submitPrice(price);
}

const hasCurrentPrice = !!card && !!card.$price;
Expand All @@ -80,7 +92,11 @@ export function SetPriceModal({ card, onClose, onPriced }: SetPriceModalProps) {
onClose={onClose}
>
{card && (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<form
onSubmit={handleSubmit}
noValidate
className="flex flex-col gap-4"
>
<CardSummary card={card}>
{hasCurrentPrice && (
<div className="mb-3 text-[12px] text-accent">
Expand All @@ -95,6 +111,7 @@ export function SetPriceModal({ card, onClose, onPriced }: SetPriceModalProps) {
<input
type="number"
min={1}
max={MAX_PRICE_CREDITS}
value={amount}
onChange={(e) => {
setAmount(e.target.value);
Expand Down
6 changes: 5 additions & 1 deletion example-apps/dashmint-lab/src/dash/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export interface Card {
$price?: number | bigint;
}

function hasSalePrice(card: Card): boolean {
return card.$price != null && card.$price !== 0 && card.$price !== 0n;
}

function toCard(id: string | null, raw: DashCardQueryDocument): Card {
const j: Record<string, unknown> =
typeof raw?.toJSON === "function" ? raw.toJSON() : raw;
Expand Down Expand Up @@ -112,7 +116,7 @@ export async function listMarketplaceCards({
documentTypeName: "card",
limit,
});
const cards = normalizeCards(results).filter((c) => c.$price);
const cards = normalizeCards(results).filter(hasSalePrice);
log?.(`Found ${cards.length} card(s) for sale.`);
return cards;
}
36 changes: 36 additions & 0 deletions example-apps/dashmint-lab/test/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,42 @@ describe("App", () => {
);
});

it("sorts prices by exact bigint value above Number.MAX_SAFE_INTEGER", async () => {
const session = makeSession();
const largePriceCards: Card[] = [
{
id: "lower",
ownerId: "owner-1",
data: { name: "Lower Price", attack: 1, defense: 1 },
$price: 9_007_199_254_740_992n,
},
{
id: "higher",
ownerId: "owner-2",
data: { name: "Higher Price", attack: 1, defense: 1 },
$price: 9_007_199_254_740_993n,
},
];
mockUseSession.mockReturnValue(session);
mockListAllCards.mockResolvedValue(largePriceCards);

render(<App />);

await waitFor(() => {
expect(screen.getByTestId("cards").textContent).toBe(
"Lower Price|Higher Price",
);
});

fireEvent.click(screen.getByRole("button", { name: "Sort: Rarity" }));
fireEvent.click(screen.getByRole("button", { name: "Sort: Name" }));
fireEvent.click(screen.getByRole("button", { name: "Sort: Owner" }));

expect(screen.getByTestId("cards").textContent).toBe(
"Higher Price|Lower Price",
);
});

it("wires modal state into child props for login and card actions", async () => {
const session = makeSession();
mockUseSession.mockReturnValue(session);
Expand Down
Loading