Self-hosted e-commerce platform for atproto bazaar.whereditgo.diamonds

Add merchant gifts (ADR 0026) #24

Merged whereditgo.diamonds
targeting main
Merged opened by and assigned to whereditgo.diamonds
targeting main

Merchant gifts (ADR 0026) #

Merchants can give a listing to a specific account without going through the payment processor. The recipient accepts or declines the next time they sign in. Accepting writes an ordinary purchase.receipt to their repo, signed exactly like a paid one, so downloads and receipt verification need no changes.

How it works #

  • Prepare (merchant): pick a recipient with the handle typeahead (locked to the DID that was picked), choose an active listing, and add an optional message. The listing, license and granted items are captured at prepare time.
  • Accept / decline (recipient): a banner appears after sign-in and links to a gift page laid out like the receipt page. Accepting writes the receipt through the recipient's own session.
  • Gift receipts: payment: { processor: "gift", ref: "gift_<id>" } with pricePaid: 0. The existing seven-field storefrontSig is unchanged, and there's no lexicon change.
  • Statuses: pending → accepted | declined | cancelled. The merchant can cancel only while the gift is pending; accepted is final. Gifts never expire.
  • Updated access: if a product gains items, prepare another gift. Only one pending gift is allowed per recipient + listing, and accepted ones don't block a new one.
  • The message stays in Bazaar's database and is never written to a PDS.

Server #

  • Receipt building, signing and the "write once" logic moved out of fulfillCheckoutSession into lib/atproto/receipt.ts, now shared by checkout and gifts. Checkout behaves the same.
  • New gift table (migration 0002_gifts) with a unique index that allows only one open gift per recipient + listing.
  • GET/POST /api/merchant/gifts, POST /api/merchant/gifts/:id/cancel.
  • GET /api/gifts/pending, GET /api/gifts/:id, POST /api/gifts/:id/accept|decline. All are scoped to the signed-in DID, and someone else's gift returns 404.
  • Accept can be retried safely: an existing receipt in the recipient's repo is picked up instead of written twice, a failed write puts the gift back to pending, and an accept stuck mid-flight is picked up again after 120s.

Client #

  • A Gifts item in the merchant menu and a Gifts tile on the dashboard.
  • Recipient banner: the whole box is clickable and it stays under the header while scrolling.
  • Gift page: the same merchant badge as the product page ("A gift from"), the message credited to the storefront name, and numbered included items.
  • Gift receipts show "Gift" instead of a price on the receipt page and purchases list.

Also in this branch #

  • Atmosphere client selector: merchants choose which app their DID links to on product and gift pages (pdsls by default, plus bluesky, blacksky, witchsky, mu, red dwarf). The list is ported from plyr.fm's atclients.ts (MIT), and the choice is stored only in Bazaar's database. It's saved with "Save profile", which is now disabled when nothing has changed.
  • Dashboard: the Getting started checklist waits for its setup checks to finish (stat tiles show placeholders meanwhile), so a fully set-up merchant no longer sees it flash.
  • Sign-in: an "Is this you?" confirmation card before OAuth, or an inline error if the handle can't be found. The page is centered, both views share one card, and the card fades in.

This pull request has no versions submitted yet.