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
12 changes: 8 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Event key for the event you want to sell, for example ev_9f3a.
NEXT_PUBLIC_SEATLAYER_EVENT_KEY=ev_replace_me
# Event key: the top of the event's page in the dashboard. It is not the pk_ key.
NEXT_PUBLIC_SEATLAYER_EVENT_KEY=<YOUR_EVENT_KEY>

# Publishable key for the same account. It is safe in browser code.
# Publishable key: Dashboard → Developer. It is safe in browser code.
# Never put a secret sk_ key in a front end bundle.
NEXT_PUBLIC_SEATLAYER_PUBLIC_KEY=pk_test_replace_me

Expand All @@ -14,11 +14,15 @@ NEXT_PUBLIC_SEATLAYER_EVENT_KEY_3=
NEXT_PUBLIC_SEATLAYER_SEASON_KEY=

# Optional. The currency your event is priced in, for showing prices before a
# hold (ISO 4217, for example EUR). Defaults to USD.
# hold (ISO 4217, for example EUR). Defaults to EUR.
NEXT_PUBLIC_SEATLAYER_CURRENCY=

# Server only. Never prefixed with NEXT_PUBLIC_, so it stays out of the browser
# bundle. Enables the hold inspection and booking path in app/api/hold/route.ts
# and the real event names in app/api/events/route.ts, which lists only the
# event keys above.
SEATLAYER_SECRET_KEY=

# Optional. Serve the app under a sub-path, for example /demos. Set it for both
# `npm run build` and `npm start`. Leave it empty to serve at /.
SEATLAYER_BASE_PATH=
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,3 @@ dist
*.log
*.tsbuildinfo

# Cloudflare Workers (OpenNext)
.open-next
.wrangler
.dev.vars
cloudflare-env.d.ts
403 changes: 153 additions & 250 deletions README.md

Large diffs are not rendered by default.

38 changes: 29 additions & 9 deletions app/best-available/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { RouteIntro } from "@/components/RouteIntro";
import type { Metadata } from "next";
import { DemoShell } from "@/components/DemoShell";
import { BestAvailableFlow } from "@/components/BestAvailableFlow";
import { bestAvailable } from "@/lib/snippets";

export const metadata: Metadata = { title: "Best available" };

export default function Page() {
return (
<>
<RouteIntro
title="Best available seats for a group"
question="How do I offer best available seats for a group, so a party of six is seated together without clicking six seats?"
docsHref="https://docs.seatlayer.io/buyer-sdk/best-available/"
docsLabel="Best available seats"
/>
<DemoShell
slug="best-available"
lede="Buyers say how many seats they need. SeatLayer finds the best block of seats together and holds it in one call."
steps={[
["Choose how many seats", "From 1 to 8"],
["Press Find best seats", "The seats are held for you"],
["See them on the map", "Release them to try again"],
]}
code={[{ id: "react", label: "React", file: "components/BestAvailableFlow.tsx", code: bestAvailable }]}
sourceFile="components/BestAvailableFlow.tsx"
sideHeading="One call finds and holds"
sideText="bestAvailable() returns the held seats, or null when no block that size is free. Null is not the same as sold out, so tell the buyer to try a smaller group."
swapNote={
<>
<b>One price band only</b>
<p>
Pass a category key from your published chart as the second argument, for example{" "}
<code>bestAvailable(2, &quot;stalls&quot;)</code>.
</p>
</>
}
docsHref="https://docs.seatlayer.io/buyer-sdk/best-available/"
>
<BestAvailableFlow />
</>
</DemoShell>
);
}
98 changes: 34 additions & 64 deletions app/checkout-handoff/page.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,39 @@
import { RouteIntro } from "@/components/RouteIntro";
import type { Metadata } from "next";
import { DemoShell } from "@/components/DemoShell";
import { SeatSelection } from "@/components/SeatSelection";
import { ownCart, serverHold } from "@/lib/snippets";

export const metadata: Metadata = { title: "Checkout handoff" };

/**
* A page rather than a widget: this route is about the half of the integration
* that has no browser in it.
*/
export default function Page() {
return (
<>
<RouteIntro
title="Handing a hold to your own checkout"
question="How do I hand a seat hold to my own checkout and payment gateway, and what must run on my server rather than in the browser?"
docsHref="https://docs.seatlayer.io/buyer-sdk/holds-and-checkout/"
docsLabel="Holds and checkout handoff"
/>

<section className="prose">
<h2>What the browser does</h2>
<p>
The buyer SDK selects seats and creates a hold. A hold is a short,
revocable claim on inventory: it is not a ticket and it is not a payment. The
browser ends up with an opaque hold id and a display-only set of line items. It
never has booking authority.
</p>

<h2>What your server does</h2>
<p>
Your server holds the secret key. It reads the hold back from SeatLayer to get the
authoritative seats and prices, creates your own checkout session for that amount
with your own payment gateway, and books the hold once the payment has succeeded.
Your own order id is passed as the booking reference, which is what makes a repeat
of the same request safe: an unknown outcome is reconciled by repeating it with the
same reference, never by generating a new one.
</p>

<h2>Where it is in this repository</h2>
<ul>
<li>
<code>app/api/hold/route.ts</code> inspects the hold and books it. The server SDK
path runs only when <code>SEATLAYER_SECRET_KEY</code> is set.
</li>
<li>
<code>app/api/events/route.ts</code> serves the events you configure, and reads
their real names with the same key when it is present.
</li>
<li>
<code>components/SeatPickerFlow.tsx</code> is the browser side that produces the
hold id in the first place.
</li>
</ul>

<h2>Rules that do not bend</h2>
<ul>
<li>A secret key never reaches browser code, a response body or a log.</li>
<li>A price that arrived from a browser is never charged.</li>
<li>A real conflict is surfaced, not retried in a loop.</li>
<li>One business order keeps one immutable booking reference for its whole life.</li>
</ul>

<p className="muted small">
More: <a href="https://docs.seatlayer.io/server-api/idempotency-and-conflicts/">
Idempotency and conflicts
</a>{" "}
and <a href="https://docs.seatlayer.io/server-sdk/node/">the Node server SDK</a>.
</p>
</section>
</>
<DemoShell
slug="checkout-handoff"
lede="Your own cart around the seat map, then the part with no browser in it: your server reads the hold back from SeatLayer and books it."
steps={[
["Pick seats", "They land in your own cart"],
["Press Continue to checkout", "The browser holds them"],
["Watch your server's steps", "Read the hold, then book"],
]}
code={[
{ id: "react", label: "React", file: "components/SeatSelection.tsx", code: ownCart },
{ id: "server", label: "Server", file: "app/api/hold/route.ts", code: serverHold },
]}
sourceFile="app/api/hold/route.ts"
sideHeading="Rules that do not bend"
sideText="A secret key never reaches the browser. A price that came from a browser is never charged. One order keeps one booking reference, so a retry can never sell twice."
swapNote={
<>
<b>Turn on the server steps</b>
<p>
Set <code>SEATLAYER_SECRET_KEY</code> in <code>.env.local</code>. It is read only inside the route
handler and never sent to the browser.
</p>
</>
}
docsHref="https://docs.seatlayer.io/buyer-sdk/holds-and-checkout/"
>
<SeatSelection />
</DemoShell>
);
}
4 changes: 2 additions & 2 deletions app/control-room/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { ControlRoom } from "@/components/ControlRoom";

export default function Page() {
return (
<>
<div className="wrap">
<RouteIntro
title="The organizer board"
question="How do staff watch live inventory for one event without an account in my product and without a secret key in the browser?"
docsHref="https://docs.seatlayer.io/platform/embedded-control-room/"
docsLabel="Embedded Control Room"
/>
<ControlRoom />
</>
</div>
);
}
16 changes: 0 additions & 16 deletions app/events/page.tsx

This file was deleted.

Loading
Loading