import { ArrowUpRightIcon, CheckIcon, ClockIcon, CreditCardIcon, FingerprintIcon, InfoIcon, LinkIcon, PlusIcon, ShieldCheckIcon, XIcon, ZapIcon, } from "lucide-react"; import React from "react"; import { Link, useNavigate } from "react-router"; import twoFiatLogo from "src/assets/cards/2fiat.png"; import freedomiaLogo from "src/assets/cards/freedomia.png"; import redotpayLogo from "src/assets/cards/redotpay.png"; import solvocardLogo from "src/assets/cards/solvocard.png"; import bringinLogo from "src/assets/suggested-apps/bringin.png"; import wavespaceLogo from "src/assets/suggested-apps/wave-space.png"; import AppHeader from "src/components/AppHeader"; import { AlbyIcon } from "src/components/icons/Alby"; import { AppleIcon } from "src/components/icons/Apple"; import { GooglePayIcon } from "src/components/icons/GooglePay"; import { VisaLogo } from "src/components/icons/VisaLogo"; import { Avatar, AvatarFallback, AvatarImage } from "src/components/ui/avatar"; import { Badge } from "src/components/ui/badge"; import { Button } from "src/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "src/components/ui/dialog"; import { FieldError } from "src/components/ui/field"; import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "src/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "src/components/ui/table"; import { ToggleGroup, ToggleGroupItem } from "src/components/ui/toggle-group"; import { Tooltip, TooltipContent, TooltipTrigger, } from "src/components/ui/tooltip"; import { localStorageKeys } from "src/constants"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useInfo } from "src/hooks/useInfo"; import { cn } from "src/lib/utils"; import { sendEvent } from "src/utils/sendEvent"; type Region = "Global" | "US" | "EU" | "UK" | "LATAM" | "Africa" | "Asia"; type FeatureFilter = | "ApplePay" | "GooglePay" | "Self-custody" | "Lightning-native" | "No KYC"; type Provider = { id: string; name: string; url: string; logo: string; initials: string; networks: ("Visa" | "Mastercard")[]; cardType: "Physical" | "Virtual" | "Both"; regions: Region[]; applePay: boolean; googlePay: boolean; selfCustody: boolean; lightningNative: boolean; kyc: boolean; timeToGet: string; cardCost: string; fees: string; /** * If set, the provider connects via its own NWC flow in the in-hub app store * (the row's action links to /appstore/) rather than through the * stablecoin top-up Connect-card dialog. */ appStoreId?: string; }; const providers: Provider[] = [ { id: "redotpay", name: "RedotPay", url: "https://wap.redotpay.com/en/invite/?referralId=qr1a5", logo: redotpayLogo, initials: "RP", networks: ["Visa"], cardType: "Virtual", regions: ["Global"], applePay: true, googlePay: true, selfCustody: false, lightningNative: false, kyc: true, timeToGet: "<10 minutes", cardCost: "$10", fees: "~2.2% + FX", appStoreId: "bitcoin-card-topup", }, { id: "2fiat", name: "2fiat", url: "https://2fiat.com/getalby", logo: twoFiatLogo, initials: "2F", networks: ["Mastercard"], cardType: "Virtual", regions: ["Global"], applePay: true, googlePay: true, selfCustody: false, lightningNative: true, kyc: false, timeToGet: "Instant", cardCost: "$50", fees: "~6.8% top-up + $0.50", appStoreId: "2fiat", }, { id: "freedomia", name: "Freedomia", url: "https://www.freedomia.io/a/getalby", logo: freedomiaLogo, initials: "FR", networks: ["Visa"], cardType: "Virtual", regions: ["Global"], applePay: false, googlePay: true, selfCustody: false, lightningNative: true, kyc: false, timeToGet: "Instant", cardCost: "$5–30 / mo", fees: "1.3–4.3%", appStoreId: "bitcoin-card-topup", }, { id: "solvocard", name: "Solvocard", url: "https://www.solvocard.com/auth/sign-up?ref=77CEM87Q", logo: solvocardLogo, initials: "SC", networks: ["Visa", "Mastercard"], cardType: "Virtual", regions: ["Global"], applePay: true, googlePay: true, selfCustody: false, lightningNative: false, kyc: false, timeToGet: "Instant", cardCost: "$99", fees: "4% deposit + $1 card topup", appStoreId: "bitcoin-card-topup", }, { id: "bringin", name: "Bringin", url: "https://bringin.app", logo: bringinLogo, initials: "BR", networks: ["Visa"], cardType: "Both", regions: ["EU"], // Direct Apple Pay isn't live — usable today via Curve only, so don't // claim it. Google/Samsung/Fitbit/Garmin Pay are direct. applePay: false, googlePay: true, selfCustody: false, lightningNative: true, kyc: true, timeToGet: "Minutes", // Their cards page advertises a €3.49/mo subscription that bundles // both cards. Worth re-confirming during checkout — sources disagree. cardCost: "€3.49 / mo", fees: "1% + 0.5%", appStoreId: "bringin", }, { id: "wavespace", name: "wavecard by wave.space", url: "https://app.wave.space/?utm_source=albyhub&affiliate=AlbyHub", logo: wavespaceLogo, initials: "WS", networks: ["Visa"], cardType: "Both", // Card is accepted worldwide, but issuance requires EEA residency. regions: ["EU"], applePay: false, googlePay: true, selfCustody: false, lightningNative: true, kyc: true, timeToGet: "Minutes", cardCost: "€2.99 / €29.99", fees: "1% + 0.5%", appStoreId: "wavespace", }, ]; const howItWorksSteps = [ { icon: CreditCardIcon, title: "Get a card", description: "Pick a debit card that fits your region. Physical or virtual, Apple Pay or Google Pay ready.", }, { icon: LinkIcon, title: "Get a top-up link", description: "Connect your card once and save the top-up link to your phone. Open it anytime to fund your card.", }, { icon: ZapIcon, title: "Top up in seconds", description: "Pay from your Lightning balance. Your card is funded in seconds, ready to spend.", }, ]; const regionFilters: { label: string; value: Region | "All" }[] = [ { label: "All", value: "All" }, { label: "Global", value: "Global" }, { label: "US", value: "US" }, { label: "EU", value: "EU" }, { label: "UK", value: "UK" }, { label: "LATAM", value: "LATAM" }, { label: "Africa", value: "Africa" }, { label: "Asia", value: "Asia" }, ]; export function Cards() { const { data: albyMe } = useAlbyMe(); const { data: info } = useInfo(); const cardholderName = ( albyMe?.name || info?.nodeAlias || "Alby Hub User" ).toUpperCase(); const [region, setRegion] = React.useState("All"); const [features, setFeatures] = React.useState([]); const [connectOpen, setConnectOpen] = React.useState(false); const [heroDismissed, setHeroDismissed] = React.useState( () => localStorage.getItem(localStorageKeys.cardsHeroDismissed) === "true" ); const dismissHero = React.useCallback(() => { setHeroDismissed(true); localStorage.setItem(localStorageKeys.cardsHeroDismissed, "true"); }, []); const filtered = providers.filter((p) => { if ( region !== "All" && !p.regions.includes(region) && !(region !== "Global" && p.regions.includes("Global")) ) { return false; } for (const f of features) { if (f === "ApplePay" && !p.applePay) { return false; } if (f === "GooglePay" && !p.googlePay) { return false; } if (f === "Self-custody" && !p.selfCustody) { return false; } if (f === "Lightning-native" && !p.lightningNative) { return false; } if (f === "No KYC" && p.kyc) { return false; } } return true; }); return ( <> setConnectOpen(true)}> Connect card } /> {/* Hero — AI-style with 3-step strip */} {!heroDismissed && (
{/* Left */}

Cards + Bitcoin

Got your card ready?
Connect it.

Spend Bitcoin anywhere. Top up your debit card from your Lightning balance in under a minute.

{/* Right — card visual */}
ALBY HUB
•••• •••• •••• 4421

Cardholder

{cardholderName}

{/* Three steps */}
{howItWorksSteps.map((item) => { const Icon = item.icon; return (

{item.title}

{item.description}

); })}
)} {/* Filter bar */}

Region

Filter

setFeatures(v as FeatureFilter[])} variant="outline" size="sm" spacing={1} className="flex-wrap flex-1 min-w-0 *:data-[state=on]:bg-primary *:data-[state=on]:text-primary-foreground *:data-[state=on]:border-primary" > {providers.some((p) => p.applePay) && ( Apple Pay )} {providers.some((p) => p.googlePay) && ( Google Pay )} {providers.some((p) => p.selfCustody) && ( Self-custody )} {providers.some((p) => p.lightningNative) && ( Lightning )} {providers.some((p) => !p.kyc) && ( No KYC )}
{(region !== "All" || features.length > 0) && (

Showing{" "} {filtered.length} {" "} of {providers.length} providers

)}
{/* Provider table (desktop) / card list (mobile) */}
Provider Type Regions Mobile KYC Time to get Card cost Fees {filtered.length === 0 && ( No providers match these filters. )} {filtered.map((p) => ( ))}
{/* Mobile: stacked cards instead of a sideways-scrolling table */}
{filtered.length === 0 && (
No providers match these filters.
)} {filtered.map((p) => ( ))}

Alby Hub does not issue or operate these cards. Availability, fees, and KYC are set by each provider — values here are approximate and may change.

); } function openProvider(provider: Provider) { sendEvent("debit_card_url_clicked", { name: provider.name, url: provider.url, }); window.open(provider.url, "_blank", "noopener,noreferrer"); } function ProviderRow({ provider }: { provider: Provider }) { return ( openProvider(provider)} >
{provider.initials}
{provider.name} {provider.selfCustody && ( Self-custodial )} {provider.lightningNative && ( Lightning-native )}

{provider.networks.join(" / ")}

{provider.cardType}
{provider.regions.map((r) => ( {r} ))}
{provider.applePay ? "Apple Pay supported" : "No Apple Pay"} {provider.googlePay ? "Google Pay supported" : "No Google Pay"}
{provider.timeToGet}
{provider.cardCost}
{provider.fees}
); } function ProviderCard({ provider }: { provider: Provider }) { return (
{provider.initials}
{provider.name} {provider.selfCustody && ( )} {provider.lightningNative && ( )}

{provider.networks.join(" / ")} · {provider.cardType}

{provider.regions.map((r) => ( {r} ))} {(provider.applePay || provider.googlePay) && ( {provider.applePay && } {provider.googlePay && } )}
{provider.timeToGet} {provider.cardCost} {provider.fees}
); } function CardFact({ label, children, }: { label: string; children: React.ReactNode; }) { return (

{label}

{children}
); } function ConnectCardDialog({ open, onOpenChange, providers, }: { open: boolean; onOpenChange: (open: boolean) => void; providers: Provider[]; }) { const navigate = useNavigate(); const [showOtherCardForm, setShowOtherCardForm] = React.useState(false); const [otherCardName, setOtherCardName] = React.useState(""); const [otherCardNameError, setOtherCardNameError] = React.useState(""); // The dialog is controlled and opened programmatically (no DialogTrigger), // so onOpenChange never fires with true — reset the form here instead. React.useEffect(() => { if (open) { setShowOtherCardForm(false); setOtherCardName(""); setOtherCardNameError(""); } }, [open]); const handleOpenChange = (o: boolean) => { if (o) { setShowOtherCardForm(false); setOtherCardName(""); setOtherCardNameError(""); } onOpenChange(o); }; const handleOtherCardSubmit = (e: React.FormEvent) => { e.preventDefault(); const cardName = otherCardName.trim(); if (!cardName) { setOtherCardNameError("Enter a card name"); return; } sendEvent("debit_card_connect", { name: cardName }); onOpenChange(false); navigate( `/apps/new?app=bitcoin-card-topup&name=${encodeURIComponent(`${cardName} - Bitcoin Card Topup`)}` ); }; if (showOtherCardForm) { return ( Name your card We'll use it to label your top-up connection.
{ setOtherCardName(e.target.value); setOtherCardNameError(""); }} placeholder="e.g. Moon" required autoComplete="off" aria-invalid={!!otherCardNameError || undefined} aria-describedby={ otherCardNameError ? "other-card-name-error" : undefined } /> {otherCardNameError}
); } return ( Pick your card provider We'll take you to the setup guide for the one you choose.
{providers.map((p) => { if (!p.appStoreId) { return null; } // The generic card topup app pre-configures itself from a `provider` // query param; pass the selected provider so its preset is applied. const to = p.appStoreId === "bitcoin-card-topup" ? `/apps/new?app=${p.appStoreId}&provider=${encodeURIComponent(p.id)}&name=${encodeURIComponent(`${p.name} - Bitcoin Card Topup`)}` : `/apps/new?app=${p.appStoreId}`; return ( { sendEvent("debit_card_connect", { name: p.name }); onOpenChange(false); }} className="flex items-center gap-3 rounded-lg border border-border p-3 hover:bg-accent/40 transition-colors" > {p.initials}

{p.name}

{p.networks.join(" / ")} · {p.cardType}

); })}
); } function KycBadge({ kyc }: { kyc: Provider["kyc"] }) { if (!kyc) { return ( No No identity verification required. These cards typically operate via a single merchant-of-record account — privacy-friendly, but operationally fragile, and the program can be paused or shut down without notice. ); } return ( Yes Identity verification required. What you'll need depends on your passport and country — it can be as little as a passport and selfie, or also include a tax number, proof of address, and other details. ); }