diff --git a/public/blog/paycode/mqtt.png b/public/blog/paycode/mqtt.png new file mode 100644 index 00000000..330fa558 Binary files /dev/null and b/public/blog/paycode/mqtt.png differ diff --git a/public/blog/paycode/paycode.png b/public/blog/paycode/paycode.png new file mode 100644 index 00000000..b8d5968c Binary files /dev/null and b/public/blog/paycode/paycode.png differ diff --git a/public/blog/paycode/paymentflow.png b/public/blog/paycode/paymentflow.png new file mode 100644 index 00000000..0f475b1c Binary files /dev/null and b/public/blog/paycode/paymentflow.png differ diff --git a/public/img/pos-demo.mov b/public/img/pos-demo.mov new file mode 100644 index 00000000..ce38f289 Binary files /dev/null and b/public/img/pos-demo.mov differ diff --git a/public/img/user-logos/paycode.svg b/public/img/user-logos/paycode.svg new file mode 100644 index 00000000..086d948a --- /dev/null +++ b/public/img/user-logos/paycode.svg @@ -0,0 +1,54 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + diff --git a/src/app/blog/paycode/page.mdx b/src/app/blog/paycode/page.mdx new file mode 100644 index 00000000..c8801090 --- /dev/null +++ b/src/app/blog/paycode/page.mdx @@ -0,0 +1,93 @@ +import { BlogPostLayout } from '@/components/BlogPostLayout' +import { MQTTDiagram } from '@/components/MQTTDiagram' +import { IrohPaymentDiagram } from '@/components/IrohPaymentDiagram' +import { QRTicketFlow } from '@/components/QRTicketFlow' + +export const post = { + draft: false, + author: 'okdistribute', + date: '2026-03-26', + title: 'iroh for payments', + description: + 'How Paycode used iroh to connect payment terminals to point of sale systems at highway toll booths, with no additional servers and full compliance.', +} + +export const metadata = { + title: post.title, + description: post.description, + openGraph: { + title: post.title, + description: post.description, + images: [{ + url: `/api/og?title=Blog&subtitle=${post.title}`, + width: 1200, + height: 630, + alt: post.title, + type: 'image/png', + }], + type: 'article' + } +} + +export default (props) => + +When people think about next generation payment infrastructure, they usually think fancy iPads, flashy apps, Bluetooth, and paperless receipts. But the reality for most of the world is more humble. You're more likely to be presented with Windows 7, proprietary printers, and ethernet cables. + +In this post we look at [Paycode], a team deploying iroh in remote environments in Mexico. Their most recent project: connecting payment terminals and point of sale systems to bring tap-to-pay to highway toll booths. + + + + +## Why peer-to-peer payments? + +Paying for things can be frustrating when the tech breaks. Slow or inoperable point of sale systems cause massive delays, lines, and lost revenue. Connectivity isn't always guaranteed — especially for mobile devices that move out of range, or when the cloud service goes down. Every second saved at the point of sale is a second where people can get on with their lives instead of staring at a loading spinner. + +There are plenty of ways to solve connectivity issues: you can add a server to the local WiFi network, use a classic HTTP server, or run an [MQTT] broker. But these server-based approaches come with costs. More on-site hardware means more technicians to service that hardware. And the original problem never fully goes away, because the single point of failure just shifts from the cloud to the local server. + + + +With peer-to-peer connectivity, no server is needed. The payment terminal syncs encrypted payloads directly with the point of sale device. + + +One of the most critical requirements for PCI-compliant payment systems is that raw payment data can never flow through anything other than the official compliant software. Peer-to-peer connections act as a blind command and control channel between devices: raw payment data stays on the payment terminal, and any resulting transaction data is encrypted into a secure payload before leaving the device. This separation ensures that even as connectivity improves, compliance and security boundaries remain intact. + + +## Legacy hardware, modern protocols + +Paycode chose iroh to implement peer-to-peer connectivity between devices in the field — payment terminals, point of sale systems, and highway toll software. Existing constraints on hardware and budget made server-based options impossible, so a peer-to-peer approach was the only way forward. + +The environment is far from modern: +- Touch-based Windows 7 machines +- Dual-core Intel CPUs with up to 8GB of RAM +- A mix of Ethernet (on terminals) and Wi-Fi (across the tollway system) + +Despite these constraints, the team integrated iroh by bundling the Rust library inside a .NET 6 SDK. + +> "iroh was super easy to use… I started hacking and was able to integrate it into our Kotlin PoS app and have a published .NET NuGet package for our client to use in that month." - Carlos Diez, Head of mobile and front-end development at Paycode + +## How it works + +Each QR code encodes an [iroh ticket], which contains the endpoint information needed to connect to a remote node. A terminal scans the QR code, registers the remote node as a static provider, and establishes a connection through gossip-based discovery. + + + +From there, the terminal can receive commands and send information back through the gossip channel. One such command is the start transaction request: the terminal receives the data needed to charge the user, executes the transaction within the PCI-compliant flow, and sends the outcome back. + + + + +Because the communication is direct between devices, transactions avoid unnecessary intermediaries. This reduces latency and makes the process reliable. **And since all data is end-to-end encrypted, sensitive data stays protected throughout the entire transaction lifecycle.** + +## What makes this interesting + +This deployment highlights something we think is important: **innovation doesn't always happen in greenfield environments.** In many cases, the most impactful work happens when you can modernize legacy systems without replacing old hardware: reducing cloud costs and improving connectivity and reliability at the same time. + +The Paycode team needed something that could run on constrained devices, handle +unreliable networks, and meet strict compliance requirements. iroh fit because +it's designed to work on any device and adapt to the network conditions it +finds. If you're working on something similar, [let's talk!][book] + +[Paycode]: https://paycode.com.mx +[iroh ticket]: https://docs.iroh.computer/concepts/tickets +[MQTT]: https://mqtt.org/ +[book]: https://cal.com/team/number-0/iroh-services diff --git a/src/app/layout.jsx b/src/app/layout.jsx index a686a290..27f60a29 100644 --- a/src/app/layout.jsx +++ b/src/app/layout.jsx @@ -39,8 +39,11 @@ export default async function RootLayout({children}) { + + + - +
{children} diff --git a/src/app/page.jsx b/src/app/page.jsx index edd6a6b1..465f0949 100644 --- a/src/app/page.jsx +++ b/src/app/page.jsx @@ -11,7 +11,8 @@ import {AnywhereIllustration} from '@/components/AnywhereIllustration'; import { IrohEverywhere } from '@/components/IrohEverywhere'; import {ProtocolHeroList} from '@/components/ProtocolHeroList'; import {LogoCloud} from '@/components/home/LogoCloud'; -import {FeatureBentoGrid} from '@/components/FeatureBentoGrid'; + +import {HomeFeatureTabs} from '@/components/HomeFeatureTabs'; import logoRust from '@/images/language-logos/rust.svg'; import { CodeBlock } from '@/components/CodeBlock'; @@ -43,7 +44,7 @@ export default function Page() {

Modular networking stack for direct,{' '}
peer-to-peer connections between devices

@@ -69,7 +70,6 @@ export default function Page() { The core peer-to-peer technology is open source and built on open standards, so you're never locked in.

- @@ -77,7 +77,7 @@ export default function Page() {
-
+

“Doubling the network speed halves our compute budget.”

@@ -95,7 +95,7 @@ export default function Page() {
- + {/* Solutions */}
@@ -128,14 +128,25 @@ export default function Page() {
-

Data sync & P2P Web Apps

-

Powers in-chat apps for hundreds of thousands of devices around the world, even when internet access is precarious.

+

Real-time Sync for Mobile Applications

+

Powers apps for hundreds of thousands of devices around the world, even when internet access is precarious.

Delta Chat logo
+ +
+
+

Point of Sale Payments

+

Connect payment terminals directly to point of sale systems over Bluetooth, LAN, or Wi-Fi with full PCI compliance and no additional servers.

+
+
+ Paycode logo +
+
+
diff --git a/src/app/solutions/delta-chat/page.jsx b/src/app/solutions/delta-chat/page.jsx index 80732ca5..ae6003a4 100644 --- a/src/app/solutions/delta-chat/page.jsx +++ b/src/app/solutions/delta-chat/page.jsx @@ -23,13 +23,13 @@ export default function DeltaChatSolutionPage() {
-

Solution: Resilient Apps

+

Real-time Sync for Mobile

- Resilient Messaging & P2P Web Apps + Real-time Sync for Mobile & Web Apps

Delta Chat uses iroh for multi-device setup and realtime P2P communication, - powering in-chat apps for hundreds of thousands of devices around the world. + powering apps for hundreds of thousands of devices around the world.

@@ -80,7 +80,7 @@ export default function DeltaChatSolutionPage() {

Works Everywhere

- Proven holepunching keeps users connected even when internet access is precarious. + Proven NAT traversal keeps users connected even when internet access is precarious.

@@ -102,7 +102,7 @@ export default function DeltaChatSolutionPage() {

- Ready to Build Resilient Apps? + Ready to Build Real-time Sync?

Get started with iroh and build apps that work everywhere. diff --git a/src/app/solutions/page.jsx b/src/app/solutions/page.jsx index 266df81e..88283000 100644 --- a/src/app/solutions/page.jsx +++ b/src/app/solutions/page.jsx @@ -27,13 +27,22 @@ const solutions = [ logo: "rave", }, { - category: "Resilient Apps", + category: "Real-time Sync for Mobile", company: "Delta Chat", - headline: "Resilient Messaging & P2P Web Apps", - description: "Power in-chat apps for hundreds of thousands of devices around the world, even when internet access is precarious.", + headline: "Real-time Sync for Mobile Applications", + description: "Power apps for hundreds of thousands of devices around the world, even when internet access is precarious.", href: "/solutions/delta-chat", logo: "delta_chat", }, + { + category: "Payments / Point of Sale", + company: "Payments", + headline: "PCI-Compliant Peer-to-Peer Payments", + description: "Connect payment terminals directly to point of sale systems with no additional servers and full PCI compliance.", + href: "/solutions/pos", + logo: "paycode", + logoExt: "svg", + }, ] export default function SolutionsPage() { @@ -72,14 +81,18 @@ export default function SolutionsPage() {

- + {solution.logo ? ( + + ) : ( +

{solution.company}

+ )}
diff --git a/src/app/solutions/pos/page.jsx b/src/app/solutions/pos/page.jsx new file mode 100644 index 00000000..e3159a2e --- /dev/null +++ b/src/app/solutions/pos/page.jsx @@ -0,0 +1,189 @@ +import { Button } from "@/components/Button" +import { HeaderSparse } from '@/components/HeaderSparse' +import { FooterMarketing } from "@/components/FooterMarketing" +import Link from "next/link" +import { MQTTDiagram } from "@/components/MQTTDiagram" +import { IrohPaymentDiagram } from "@/components/IrohPaymentDiagram" +import { POSFeatureTabs } from "@/components/POSFeatureTabs" + + +export const metadata = { + title: 'Iroh for Payments', + description: 'Connect payment terminals directly to point of sale systems over Bluetooth, LAN, or Wi-Fi with iroh. No servers, no brokers, no complexity.', +} + +export default function PaycodeUseCasePage() { + return ( +
+ + +
+ {/* Hero Section */} +
+
+
+
+

Payments / Point of Sale

+

+ Payments & Point of Sale +

+

+ Connect payment terminals directly to point of sale systems over + Bluetooth, LAN, or Wi-Fi with iroh. + No additional servers, no cloud dependency. +

+
+ + + + + + +
+
+
+
+
+
+
+ + {/* The Problem: Local Servers */} +
+
+
+
+

The Problem

+

Local servers are a liability

+

+ Traditional payment architectures use an MQTT broker or local server to route + messages between devices. The operator machine publishes a command, the broker + forwards it to the POS terminal, and the result takes the same path back. +

+

+ This means extra hardware on-site, another service to maintain, and a single point + of failure that can take down every terminal at once. +

+
+
+ +
+
+
+
+ + {/* The Solution: No Servers */} +
+
+
+
+

The Solution

+

No servers required

+

+ With peer-to-peer, there is no local server required. Devices connect directly over + whatever network is available: Bluetooth, LAN, + or Wi-Fi. Devices on the same local network connect directly without internet. +

+ +

+ Additionally, servers and relays in the cloud need to hold no extra state + beyond authentication. This means that even if the cloud + service goes down, devices can continue to operate and sync + data. +

+ +
+
+ +
+
+
+
+ + {/* Case Study: Paycode */} +
+
+
+
+

Case Study

+

Highway Toll Booths

+

+ Paycode used iroh to bring tap-to-pay to highway toll booths running Windows 7 + hardware. They connected Kotlin Android POS devices to .NET 6 terminals with no + additional servers. +

+
+

+ “iroh was super easy to use… I started hacking and was able to integrate + it into our Kotlin PoS app and have a published .NET NuGet package for our client + to use in that month.” +

+
+ + Read the full case study → + +
+
+ Paycode logo +
+
+
+
+ + {/* How It Works — Feature Tabs */} + + + {/* CTA */} +
+
+

+ Build Payment Systems Without Servers +

+

+ Get started with iroh in minutes. Connect devices, + maintain compliance, and reduce complexity. +

+
+ + + + + + +
+
+
+ + +
+
+ ) +} diff --git a/src/components/BlogHeader.jsx b/src/components/BlogHeader.jsx index f4a37bec..906a5296 100644 --- a/src/components/BlogHeader.jsx +++ b/src/components/BlogHeader.jsx @@ -30,7 +30,7 @@ export default function BlogHeader() { {navItems.map((item, i ) => { return {item.content}; })} -
  • +
  • diff --git a/src/components/BlogPostLayout.jsx b/src/components/BlogPostLayout.jsx index 33d42adf..7825ee51 100644 --- a/src/components/BlogPostLayout.jsx +++ b/src/components/BlogPostLayout.jsx @@ -10,23 +10,25 @@ export function BlogPostLayout({ article, references = [], children }) { return (
    -
    +
    -
    - - - Blog Index - +
    +
    -

    +

    {article.title}

    - + @@ -38,7 +40,7 @@ export function BlogPostLayout({ article, references = [], children }) { {references.length > 0 && ( )} -
    +
    Iroh is a dial-any-device networking library that just works. Compose from an ecosystem of ready-made protocols to get the features you need, or go fully custom on a clean abstraction over dumb pipes. Iroh is open source, and already running in production on hundreds of thousands of devices.
    To get started, take a look at our docs, dive directly into the code, or chat with us in our discord channel.
    diff --git a/src/components/FeatureTabs.jsx b/src/components/FeatureTabs.jsx new file mode 100644 index 00000000..fd93f2b4 --- /dev/null +++ b/src/components/FeatureTabs.jsx @@ -0,0 +1,49 @@ +'use client' + +import { useState, useEffect } from 'react' + +const INTERVAL = 10000 + +export function FeatureTabs({ title, tabs }) { + const [active, setActive] = useState(0) + + useEffect(() => { + const timer = setInterval(() => { + setActive((prev) => (prev + 1) % tabs.length) + }, INTERVAL) + return () => clearInterval(timer) + }, [active, tabs.length]) + + return ( +
    +
    + {title &&

    {title}

    } +
    +
    + {tabs.map((tab, i) => ( + + ))} +
    +
    + {tabs[active].diagram} +
    +
    +
    +
    + ) +} diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 311b9b4e..c7fec18b 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -25,7 +25,8 @@ export const navItems = [ {content: 'Use Cases', href: '/#solutions', dropdown: [ {label: 'Distributed AI', href: '/solutions/nous'}, {label: 'Video Streaming', href: '/solutions/rave'}, - {label: 'Resilient Apps', href: '/solutions/delta-chat'}, + {label: 'Real-time Sync', href: '/solutions/delta-chat'}, + {label: 'Payments & POS', href: '/solutions/pos'}, {label: 'Enterprise', href: '/enterprise'}, ]}, {content: 'Docs', href: 'https://docs.iroh.computer/'}, diff --git a/src/components/HomeFeatureTabs.jsx b/src/components/HomeFeatureTabs.jsx new file mode 100644 index 00000000..e00a89c8 --- /dev/null +++ b/src/components/HomeFeatureTabs.jsx @@ -0,0 +1,44 @@ +'use client' + +import Link from 'next/link' +import { FeatureTabs } from '@/components/FeatureTabs' +import { IrohEverywhere } from '@/components/IrohEverywhere' +import { OpenSourceIllustration } from '@/components/OpenSourceIllustration' +import { PaycodePCIDiagram } from '@/components/PaycodePCIDiagram' + +const tabs = [ + { + id: 'everywhere', + label: 'Works Everywhere', + body: ( +

    + Windows, macOS, Linux, Android, iOS, and embedded devices. Connect over Bluetooth, LAN, Wi-Fi, or Tor. +

    + ), + diagram: , + }, + { + id: 'encrypted', + label: 'E2E Encrypted, Always', + body: ( +

    + Every connection is end-to-end encrypted over QUIC. No certificates to manage. +

    + ), + diagram: , + }, + { + id: 'costs', + label: 'Save Cloud Costs', + body: ( +

    + Peer-to-peer connections bypass NATs and firewalls. Relays keep data flowing when direct connections can't be made. +

    + ), + diagram: , + }, +] + +export function HomeFeatureTabs() { + return +} diff --git a/src/components/IrohPaymentDiagram.jsx b/src/components/IrohPaymentDiagram.jsx new file mode 100644 index 00000000..4e58e1e7 --- /dev/null +++ b/src/components/IrohPaymentDiagram.jsx @@ -0,0 +1,181 @@ +const palette = [ + "#7C7CFF", // irohPurple-500 (primary) + "#6257F7", // irohPurple-600 (secondary) + "#4E3FE0", // irohPurple-700 (tertiary) +] + +const GRAY = "#888" + +// Layout — same horizontal positions as MQTT diagram but no broker +const OP_X = 130 +const OP_Y = 130 +const POS_X = 400 +const POS_Y = 130 +const CLOUD_X = 400 +const CLOUD_Y = 245 + +// Operator machine — monitor with keyboard +const OperatorMachine = ({ x, y }) => ( + + + + + + + + Point of Sale + +) + +// Payment Terminal — card reader device +const POSTerminal = ({ x, y }) => ( + + + + + + {/* Keypad */} + + + + + + + + Payment Terminal + +) + +// Cloud server +const CloudServer = ({ x, y }) => ( + + + + + + Payment Backend + +) + +// Animated bidirectional dot +const MessageDot = ({ x1, y1, x2, y2, delay, dur, color }) => ( + + + + + +) + +// Label badge +const Badge = ({ x, y, text, color = palette[0] }) => { + const w = text.length * 4.5 + 16 + return ( + + + + + {text} + + + ) +} + +// iroh channel pill +const IrohBadge = ({ x, y }) => ( + + + + + iroh + + +) + +export function IrohPaymentDiagram({ className }) { + const opRight = OP_X + 22 + const posLeft = POS_X - 14 + const midX = (opRight + posLeft) / 2 + + return ( +
    + + + {/* --- Operator → POS (start_txn) top lane --- */} + + + + + {/* --- POS → Operator (txn_result) bottom lane --- */} + + + + + {/* --- Labels --- */} + + DIRECT P2P + + + + + {/* iroh badge at center of channel */} + + + {/* --- POS → Payment Backend (cloud) --- */} + + + PCI flow + + + {/* --- No server needed label --- */} + + no local server needed + + + {/* --- Icons --- */} + + + + + +
    + ) +} diff --git a/src/components/MQTTDiagram.jsx b/src/components/MQTTDiagram.jsx new file mode 100644 index 00000000..cf8fe751 --- /dev/null +++ b/src/components/MQTTDiagram.jsx @@ -0,0 +1,247 @@ +const palette = [ + "#7C7CFF", // irohPurple-500 (primary) + "#6257F7", // irohPurple-600 (secondary) + "#4E3FE0", // irohPurple-700 (tertiary) +] + +const GRAY = "#888" +const WARN = "#f59e0b" + +// Layout positions — left to right +const OP_X = 85 +const OP_Y = 130 +const BROKER_X = 260 +const BROKER_Y = 130 +const POS_X = 435 +const POS_Y = 130 +const CLOUD_X = 435 +const CLOUD_Y = 245 + +// Operator machine — monitor with keyboard +const OperatorMachine = ({ x, y }) => ( + + + + + + + + Point of Sale + +) + +// MQTT Broker — server with warning indicator +const BrokerServer = ({ x, y }) => ( + + + {/* Top panel */} + + + + + + + {/* Middle panel */} + + + + + + + {/* Bottom panel */} + + + + + + MQTT Broker + (local server) + +) + +// Payment Terminal — card reader device +const POSTerminal = ({ x, y }) => ( + + + + + + {/* Keypad */} + + + + + + + + Payment Terminal + +) + +// Cloud server +const CloudServer = ({ x, y }) => ( + + {/* Cloud shape */} + + {/* Server lines inside cloud */} + + + + Payment Backend + +) + +// Animated message dot +const MessageDot = ({ x1, y1, x2, y2, delay, dur, color }) => ( + + + + + +) + +// Label badge +const Badge = ({ x, y, text, color = palette[0] }) => { + const w = text.length * 4.5 + 16 + return ( + + + + + {text} + + + ) +} + +export function MQTTDiagram({ className }) { + const opRight = OP_X + 22 + const brokerLeft = BROKER_X - 20 + const brokerRight = BROKER_X + 20 + const posLeft = POS_X - 14 + + // Midpoints for labels + const midLeftX = (opRight + brokerLeft) / 2 + const midRightX = (brokerRight + posLeft) / 2 + + return ( +
    + + + {/* --- Operator → Broker (PUBLISH start_txn) --- */} + + + + + PUBLISH + + + + {/* --- Broker → POS (forward start_txn) --- */} + + + + + SUBSCRIBE + + + + {/* --- POS → Broker (PUBLISH txn_result) --- */} + + + + + {/* --- Broker → Operator (forward txn_result) --- */} + + + + + {/* Return label */} + + + + {/* --- POS → Payment Backend (cloud) --- */} + + + PCI flow + + + {/* --- Warning: single point of failure --- */} + + + ⚠ single point of failure + + + + {/* --- Icons --- */} + + + + + + +
    + ) +} diff --git a/src/components/POSFeatureTabs.jsx b/src/components/POSFeatureTabs.jsx new file mode 100644 index 00000000..ed6dbb99 --- /dev/null +++ b/src/components/POSFeatureTabs.jsx @@ -0,0 +1,50 @@ +'use client' + +import Link from 'next/link' +import { FeatureTabs } from '@/components/FeatureTabs' +import { PaycodePCIDiagram } from '@/components/PaycodePCIDiagram' +import { IrohEverywhere } from '@/components/IrohEverywhere' +import { OpenSourceIllustration } from '@/components/OpenSourceIllustration' + +const tabs = [ + { + id: 'secure', + label: 'Secure & Offline', + body: ( +

    + All iroh connections are encrypted by default using open standards. + Raw card data never leaves the payment terminal. Compliance is maintained by architecture. +

    + ), + diagram: , + }, + { + id: 'cross-platform', + label: 'Cross-Platform', + body: ( +

    + iroh runs on Android, iOS, Windows, Linux, and embedded devices. + Connect any POS device to any terminal regardless of platform—from + modern tablets to legacy Windows 7 machines. +

    + ), + diagram: , + }, + { + id: 'resilient', + label: 'Resilient by Default', + body: ( +

    + Host relays across AWS, GCP, Azure, or your own infrastructure. If one + connection path goes down, iroh automatically finds another—direct, + local network, or fallback through relays. + No single cloud provider is a single point of failure. +

    + ), + diagram: , + }, +] + +export function POSFeatureTabs() { + return +} diff --git a/src/components/PaycodePCIDiagram.jsx b/src/components/PaycodePCIDiagram.jsx new file mode 100644 index 00000000..8c1c1992 --- /dev/null +++ b/src/components/PaycodePCIDiagram.jsx @@ -0,0 +1,434 @@ +const palette = [ + "#7C7CFF", // irohPurple-500 (primary) + "#6257F7", // irohPurple-600 (secondary) + "#4E3FE0", // irohPurple-700 (tertiary) +] + +// Monitor icon for the toll booth terminal +const TerminalIcon = ({ x, y }) => ( + + {/* Monitor */} + + {/* Screen content lines */} + + + + {/* Stand */} + + + +) + +// POS device icon (card reader terminal) +const POSIcon = ({ x, y }) => ( + + {/* Device body */} + + {/* Screen */} + + {/* Keypad dots */} + + + + + + + {/* Card slot indicator */} + + +) + +// Credit card icon +const CardIcon = ({ x, y }) => ( + + + {/* Magnetic stripe */} + + {/* Chip */} + + +) + +// Lock icon +const LockIcon = ({ x, y, size = 12 }) => ( + + + + +) + +// Animated data packet traveling along a path +const DataPacket = ({ x1, y1, x2, y2, dur, delay, label, color = palette[0] }) => { + return ( + + {/* Packet dot */} + + + + + + + ) +} + +export function PaycodePCIDiagram({ className }) { + const terminalX = 110 + const posX = 480 + const centerY = 140 + + // iroh channel endpoints + const channelLeft = 185 + const channelRight = 400 + + // PCI boundary box + const pciLeft = 370 + const pciTop = 45 + const pciWidth = 200 + const pciHeight = 230 + + return ( +
    + + {/* ---- PCI Compliance Boundary ---- */} + + + + + {/* PCI boundary label */} + + + + PCI COMPLIANCE BOUNDARY + + + + {/* ---- iroh P2P Channel ---- */} + {/* Channel background */} + + + {/* Channel line - top */} + + + {/* Channel line - bottom */} + + + {/* Channel label */} + + iroh P2P channel + + + {/* Encrypted indicator on channel */} + + + {/* Animated packets - Commands going right (top lane) */} + + + + {/* Animated packets - Encrypted results going left (bottom lane) */} + + + + {/* Flow labels */} + + Commands & Encrypted Payloads Only + + + {/* Transport options */} + + via Bluetooth / LAN / Wi-Fi + + + {/* ---- Point of Sale Terminal (Left) ---- */} + + + + Point of Sale + + {/* Terminal features */} + + Charge creation + + + Transaction status + + + {/* Connection dot - terminal to channel */} + + + + + {/* Line from terminal to channel */} + + + {/* ---- Payment Terminal (Right, inside PCI boundary) ---- */} + + + + Payment Terminal + + + {/* Connection dot - channel to POS */} + + + + + {/* Line from channel to POS */} + + + {/* ---- Raw Card Data (stays inside PCI boundary) ---- */} + + + + + Raw Card Data + + + Never leaves this boundary + + + + {/* ---- "No Exit" indicator ---- */} + {/* X mark on the PCI boundary where data could escape */} + + + + + + + {/* Status indicators */} + + + + + + + +
    + ) +} diff --git a/src/components/Prose.jsx b/src/components/Prose.jsx index 5f5bfbe0..1fbbbd30 100644 --- a/src/components/Prose.jsx +++ b/src/components/Prose.jsx @@ -3,7 +3,8 @@ import clsx from 'clsx'; export function Prose({as: Component = 'div', className, ...props}) { return ( ); diff --git a/src/components/QRTicketFlow.jsx b/src/components/QRTicketFlow.jsx new file mode 100644 index 00000000..175e1cf4 --- /dev/null +++ b/src/components/QRTicketFlow.jsx @@ -0,0 +1,233 @@ +const palette = [ + "#7C7CFF", // irohPurple-500 (primary) + "#6257F7", // irohPurple-600 (secondary) + "#4E3FE0", // irohPurple-700 (tertiary) +] + +const GRAY = "#888" + +// Participant X positions +const POS_X = 80 +const QR_X = 210 +const TERMINAL_X = 340 +const DISCOVERY_X = 470 + +// Vertical layout +const HEAD_Y = 40 +const LIFELINE_START = 62 +const LIFELINE_END = 340 + +// Message Y positions +const MSG1_Y = 100 +const MSG2_Y = 150 +const MSG3_Y = 195 +const MSG4_Y = 240 +const MSG5_Y = 285 +const SELF_H = 20 + +// Participant header box +const Participant = ({ x, label, sub }) => { + const w = sub ? Math.max(label.length, sub.length) * 6.5 + 20 : label.length * 6.5 + 20 + return ( + + + + + {label} + + {sub && ( + + {sub} + + )} + + ) +} + +// Dashed vertical lifeline +const Lifeline = ({ x }) => ( + +) + +// Horizontal arrow with label +const Arrow = ({ x1, x2, y, label, color = palette[0], dashed }) => { + const dir = x2 > x1 ? 1 : -1 + const tipX = x2 + return ( + + + + + {label} + + + + + + + + ) +} + +// Self-call arrow +const SelfArrow = ({ x, y, label, color = palette[0] }) => { + const w = 32 + return ( + + + + + {label} + + + ) +} + +// Bidirectional connection +const BiArrow = ({ x1, x2, y, label, color = palette[0] }) => { + const gap = 4 + return ( + + + + + + + {label} + + + + + + + + + + + + + ) +} + +// Note box +const Note = ({ x, y, text, sub, color = palette[0] }) => { + const w = sub + ? Math.max(text.length, sub.length) * 5 + 16 + : text.length * 5 + 16 + const h = sub ? 26 : 18 + return ( + + + + + {text} + + {sub && ( + + {sub} + + )} + + ) +} + +// Small QR icon for the participant header +const QRIcon = ({ x, y, size }) => { + const s = size / 7 + const ox = x - size / 2 + const oy = y - size / 2 + const Finder = ({ gx, gy }) => ( + + + + + ) + return ( + + + + + + {[[3, 0], [4, 1], [3, 3], [5, 4], [3, 5], [4, 6], [6, 3], [6, 6]].map(([c, r], i) => ( + + ))} + + ) +} + +export function QRTicketFlow({ className }) { + return ( +
    + + + {/* Lifelines */} + + + + + + {/* Participant headers */} + + + + + + {/* 1: Point of Sale encodes ticket into QR */} + + + + {/* 2: Payment Terminal scans QR code */} + + + {/* 3: Payment Terminal registers as static provider */} + + + {/* 4: Payment Terminal queries gossip discovery */} + + + + {/* 5: Established connection */} + + + +
    + ) +} diff --git a/src/components/UsersShowcase.jsx b/src/components/UsersShowcase.jsx index 53411def..65142ad1 100644 --- a/src/components/UsersShowcase.jsx +++ b/src/components/UsersShowcase.jsx @@ -7,7 +7,7 @@ import Link from 'next/link' const projects = [ { title: "Delta Chat", - description: "Iroh powers multi-device backup & live connections for in-chat WebXDC apps", + description: "Iroh powers multi-device backup & live connections for apps", thumbnail: '/img/users/delta_chat.png', href: 'https://delta.chat', }, diff --git a/src/components/home/LogoCloud.jsx b/src/components/home/LogoCloud.jsx index 7a10d72e..51f6b737 100644 --- a/src/components/home/LogoCloud.jsx +++ b/src/components/home/LogoCloud.jsx @@ -8,6 +8,7 @@ const companies = [ { name: "spacedrive", ext: "png" }, { name: "nous", ext: "png" }, { name: "shaga", ext: "png" }, + { name: "paycode", ext: "svg" }, { name: "rave", ext: "png" }, { name: "delta_chat", ext: "png" }, { name: "holochain", ext: "svg" }, diff --git a/src/components/home/UseCases.jsx b/src/components/home/UseCases.jsx index 7722f0be..6b0f34f6 100644 --- a/src/components/home/UseCases.jsx +++ b/src/components/home/UseCases.jsx @@ -16,8 +16,8 @@ const useCases = { "Data Transfer": [ "Recall uses iroh to replicate massive amounts content-addressed data for validating AI Agents." ], - "Resilient Apps": [ - "Delta Chat uses iroh to power in-chat apps for hundreds of thousands of devices around the world, even when internet access is precarious." + "Real-time Sync for Apps": [ + "Delta Chat uses iroh to power apps for hundreds of thousands of devices around the world, even when internet access is precarious." ] } diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index ced56b38..78842fca 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -1,5 +1,3 @@ -@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&family=Space+Mono&display=swap"); - @import "tailwindcss"; @import "tw-animate-css"; @@ -146,5 +144,6 @@ } body { @apply bg-background text-foreground; + font-family: 'Space Grotesk', sans-serif; } } diff --git a/typography.js b/typography.js index b173f438..0502afe2 100644 --- a/typography.js +++ b/typography.js @@ -1,64 +1,58 @@ module.exports = ({ theme }) => ({ DEFAULT: { css: { - '--tw-prose-body': theme('colors.zinc.700'), - '--tw-prose-headings': theme('colors.zinc.900'), + '--tw-prose-body': theme('colors.irohGray.700'), + '--tw-prose-headings': theme('colors.irohGray.900'), '--tw-prose-links': theme('colors.irohPurple.500'), '--tw-prose-links-hover': theme('colors.irohPurple.600'), '--tw-prose-links-underline': theme('colors.irohPurple.500 / 0.3'), - '--tw-prose-bold': theme('colors.zinc.900'), - '--tw-prose-counters': theme('colors.zinc.500'), - '--tw-prose-bullets': theme('colors.zinc.300'), - '--tw-prose-hr': theme('colors.zinc.900 / 0.05'), - '--tw-prose-quotes': theme('colors.zinc.900'), - '--tw-prose-quote-borders': theme('colors.zinc.200'), - '--tw-prose-captions': theme('colors.zinc.500'), - '--tw-prose-code': theme('colors.zinc.900'), - '--tw-prose-code-bg': theme('colors.zinc.100'), - '--tw-prose-code-ring': theme('colors.zinc.300'), - '--tw-prose-th-borders': theme('colors.zinc.300'), - '--tw-prose-td-borders': theme('colors.zinc.200'), + '--tw-prose-bold': theme('colors.irohGray.900'), + '--tw-prose-counters': theme('colors.irohGray.500'), + '--tw-prose-bullets': theme('colors.irohGray.300'), + '--tw-prose-hr': theme('colors.irohGray.900 / 0.05'), + '--tw-prose-quotes': theme('colors.irohGray.900'), + '--tw-prose-quote-borders': theme('colors.irohGray.200'), + '--tw-prose-captions': theme('colors.irohGray.500'), + '--tw-prose-code': theme('colors.irohGray.900'), + '--tw-prose-code-bg': theme('colors.irohGray.100'), + '--tw-prose-code-ring': theme('colors.irohGray.300'), + '--tw-prose-th-borders': theme('colors.irohGray.300'), + '--tw-prose-td-borders': theme('colors.irohGray.200'), - '--tw-prose-invert-body': theme('colors.zinc.400'), + '--tw-prose-invert-body': theme('colors.irohGray.400'), '--tw-prose-invert-headings': theme('colors.white'), '--tw-prose-invert-links': theme('colors.irohPurple.400'), '--tw-prose-invert-links-hover': theme('colors.irohPurple.500'), '--tw-prose-invert-links-underline': theme('colors.irohPurple.500 / 0.3'), '--tw-prose-invert-bold': theme('colors.white'), - '--tw-prose-invert-counters': theme('colors.zinc.400'), - '--tw-prose-invert-bullets': theme('colors.zinc.600'), + '--tw-prose-invert-counters': theme('colors.irohGray.400'), + '--tw-prose-invert-bullets': theme('colors.irohGray.600'), '--tw-prose-invert-hr': theme('colors.white / 0.05'), - '--tw-prose-invert-quotes': theme('colors.zinc.100'), - '--tw-prose-invert-quote-borders': theme('colors.zinc.700'), - '--tw-prose-invert-captions': theme('colors.zinc.400'), + '--tw-prose-invert-quotes': theme('colors.irohGray.100'), + '--tw-prose-invert-quote-borders': theme('colors.irohGray.700'), + '--tw-prose-invert-captions': theme('colors.irohGray.400'), '--tw-prose-invert-code': theme('colors.white'), - '--tw-prose-invert-code-bg': theme('colors.zinc.700 / 0.15'), + '--tw-prose-invert-code-bg': theme('colors.irohGray.700 / 0.15'), '--tw-prose-invert-code-ring': theme('colors.white / 0.1'), - '--tw-prose-invert-th-borders': theme('colors.zinc.600'), - '--tw-prose-invert-td-borders': theme('colors.zinc.700'), + '--tw-prose-invert-th-borders': theme('colors.irohGray.600'), + '--tw-prose-invert-td-borders': theme('colors.irohGray.700'), // Base color: 'var(--tw-prose-body)', - fontSize: theme('fontSize.sm')[0], - ...theme('fontSize.sm')[1], + fontFamily: 'Space Grotesk, sans-serif', + fontSize: theme('fontSize.base')[0], + ...theme('fontSize.base')[1], lineHeight: theme('lineHeight.7'), // Layout '> *': { - maxWidth: theme('maxWidth.3xl'), - // marginLeft: 'auto', - marginRight: 'auto', - '@screen lg': { - maxWidth: theme('maxWidth.3xl'), - marginLeft: `calc(50% - min(50%, ${theme('maxWidth.lg')}))`, - marginRight: `calc(50% - min(50%, ${theme('maxWidth.lg')}))`, - }, + maxWidth: 'none', }, // Text p: { - fontSize: theme('fontSize.base')[0], - ...theme('fontSize.base')[1], + fontSize: theme('fontSize.lg')[0], + ...theme('fontSize.lg')[1], marginTop: theme('spacing.6'), marginBottom: theme('spacing.6'), }, @@ -69,7 +63,7 @@ module.exports = ({ theme }) => ({ // Lists ol: { - fontSize: theme('fontSize.base')[0], + fontSize: theme('fontSize.lg')[0], listStyleType: 'decimal', marginTop: theme('spacing.5'), marginBottom: theme('spacing.5'), @@ -104,7 +98,7 @@ module.exports = ({ theme }) => ({ listStyleType: 'decimal', }, ul: { - fontSize: theme('fontSize.base')[0], + fontSize: theme('fontSize.lg')[0], listStyleType: 'disc', marginTop: theme('spacing.5'), marginBottom: theme('spacing.5'), @@ -170,47 +164,56 @@ module.exports = ({ theme }) => ({ fontWeight: '500', fontStyle: 'italic', color: 'var(--tw-prose-quotes)', - borderLeftWidth: '0.25rem', - borderLeftColor: 'var(--tw-prose-quote-borders)', + backgroundColor: theme('colors.irohGray.100'), + borderRadius: theme('borderRadius.lg'), + borderLeftWidth: '3px', + borderLeftColor: theme('colors.irohPurple.500'), quotes: '"\\201C""\\201D""\\2018""\\2019"', marginTop: theme('spacing.8'), marginBottom: theme('spacing.8'), - paddingLeft: theme('spacing.5'), + paddingTop: theme('spacing.4'), + paddingBottom: theme('spacing.4'), + paddingLeft: theme('spacing.6'), + paddingRight: theme('spacing.6'), + fontSize: theme('fontSize.lg')[0], + ...theme('fontSize.lg')[1], + }, + ':is(.dark) blockquote': { + backgroundColor: theme('colors.irohGray.800'), }, 'blockquote p:first-of-type::before': { - content: 'open-quote', + content: 'none', }, 'blockquote p:last-of-type::after': { - content: 'close-quote', + content: 'none', }, // Headings h1: { color: 'var(--tw-prose-headings)', - // TODO(b5): Use theme() fontFamily: 'Space Grotesk', fontWeight: '700', - fontSize: theme('fontSize.2xl')[0], - ...theme('fontSize.2xl')[1], - marginBottom: theme('spacing.2'), + fontSize: theme('fontSize.3xl')[0], + ...theme('fontSize.3xl')[1], + marginBottom: theme('spacing.4'), }, h2: { color: 'var(--tw-prose-headings)', fontFamily: 'Space Grotesk', - fontWeight: '600', - fontSize: theme('fontSize.lg')[0], - ...theme('fontSize.lg')[1], + fontWeight: '700', + fontSize: theme('fontSize.2xl')[0], + ...theme('fontSize.2xl')[1], marginTop: theme('spacing.16'), - marginBottom: theme('spacing.2'), + marginBottom: theme('spacing.4'), }, h3: { color: 'var(--tw-prose-headings)', - fontSize: theme('fontSize.base')[0], - ...theme('fontSize.base')[1], + fontFamily: 'Space Grotesk', fontWeight: '600', + fontSize: theme('fontSize.xl')[0], + ...theme('fontSize.xl')[1], marginTop: theme('spacing.10'), marginBottom: theme('spacing.2'), - fontFamily: 'Space Grotesk', }, // Media