sanity-plugin-stripe 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { a as stockLeft, c as SHOP_SETTINGS_ID, d as STOCK_TYPE, f as VARIANT_TYPE, h as stockId, i as readStockDocuments, l as SHOP_SETTINGS_TYPE, o as COMMERCE_TYPE, p as publishedId, r as STOCK_QUERY, s as SHIPPING_REGION_TYPE, t as EU_COUNTRIES } from "./countries-BuqQF1AW.js";
2
- import { i as isObject, o as formatPrice, s as priceDecimals, t as asArray } from "./guards-CAcETsf7.js";
1
+ import { a as SHIPPING_REGION_TYPE, d as VARIANT_TYPE, f as publishedId, i as COMMERCE_TYPE, l as STOCK_TYPE, m as stockId, n as readStockDocuments, o as SHOP_SETTINGS_ID, r as stockLeft, s as SHOP_SETTINGS_TYPE, t as STOCK_QUERY, u as SUBSCRIPTION_TYPE } from "./stock-DAsSGayt.js";
2
+ import { c as formatPrice, l as priceDecimals, o as isObject, r as asArray, t as EU_COUNTRIES } from "./countries-Csjmv4lY.js";
3
3
  import { defineArrayMember, defineField, definePlugin, defineType, set, useClient, useFormValue } from "sanity";
4
4
  import { CreditCardIcon } from "@sanity/icons/CreditCard";
5
+ import { SyncIcon } from "@sanity/icons/Sync";
5
6
  import { TagIcon } from "@sanity/icons/Tag";
6
7
  import { Button, Card, Flex, Stack, Text, TextInput } from "@sanity/ui";
7
8
  import { useEffect, useState } from "react";
@@ -228,7 +229,67 @@ const AVAILABILITY = [{
228
229
  }, {
229
230
  title: "Sold out",
230
231
  value: "soldOut"
231
- }], isOnSale = (commerce) => isObject(commerce) && commerce.availability === "onSale";
232
+ }], UNITS = [{
233
+ title: "Weeks",
234
+ value: "week"
235
+ }, {
236
+ title: "Months",
237
+ value: "month"
238
+ }], LONGEST = {
239
+ week: 156,
240
+ month: 36
241
+ }, isOnSale = (commerce) => isObject(commerce) && commerce.availability === "onSale", isOffered = (subscription) => isObject(subscription) && subscription.enabled === !0;
242
+ function createSubscriptionType(config) {
243
+ return defineType({
244
+ name: SUBSCRIPTION_TYPE,
245
+ title: "Subscription",
246
+ type: "object",
247
+ icon: SyncIcon,
248
+ fields: [
249
+ defineField({
250
+ name: "enabled",
251
+ title: "Offer a subscription",
252
+ type: "boolean",
253
+ description: "Buyers can choose to get this again and again, charged with each delivery, until they cancel.",
254
+ initialValue: !1
255
+ }),
256
+ defineField({
257
+ name: "price",
258
+ title: moneyTitle("Price per delivery", config),
259
+ type: "number",
260
+ hidden: ({ parent }) => !isOffered(parent),
261
+ validation: (rule) => rule.min(0).precision(config.priceDecimals).custom((price, context) => isOffered(context.parent) && typeof price != "number" ? "A subscription needs a price." : !0)
262
+ }),
263
+ defineField({
264
+ name: "every",
265
+ title: "Delivered every",
266
+ type: "number",
267
+ description: "A whole number: 6 with \"Weeks\" below is every 6 weeks.",
268
+ hidden: ({ parent }) => !isOffered(parent),
269
+ validation: (rule) => rule.integer().min(1).custom((every, context) => {
270
+ let parent = context.parent;
271
+ if (!isOffered(parent)) return !0;
272
+ if (typeof every != "number") return "Say how often it is delivered.";
273
+ let unit = isObject(parent) && parent.unit === "month" ? "month" : "week";
274
+ return every > LONGEST[unit] ? "Stripe charges a subscription at least once every three years." : !0;
275
+ })
276
+ }),
277
+ defineField({
278
+ name: "unit",
279
+ title: "Weeks or months",
280
+ type: "string",
281
+ options: {
282
+ list: UNITS,
283
+ layout: "radio",
284
+ direction: "horizontal"
285
+ },
286
+ initialValue: "week",
287
+ hidden: ({ parent }) => !isOffered(parent),
288
+ validation: (rule) => rule.custom((unit, context) => isOffered(context.parent) && unit !== "week" && unit !== "month" ? "Choose weeks or months." : !0)
289
+ })
290
+ ]
291
+ });
292
+ }
232
293
  function createVariantType() {
233
294
  return defineType({
234
295
  name: VARIANT_TYPE,
@@ -302,6 +363,11 @@ function createCommerceType(config) {
302
363
  initialValue: taxClasses[0]?.value,
303
364
  validation: (rule) => rule.required()
304
365
  })] : [],
366
+ defineField({
367
+ name: "subscription",
368
+ title: "Subscribe and save",
369
+ type: SUBSCRIPTION_TYPE
370
+ }),
305
371
  defineField({
306
372
  name: "variants",
307
373
  type: "array",
@@ -346,7 +412,13 @@ function createRegionsInput(starting) {
346
412
  });
347
413
  };
348
414
  }
349
- const coversEverywhereElse = (region) => isObject(region) && region.everywhereElse === !0;
415
+ const SUBSCRIPTION_DELIVERY = [{
416
+ title: "Free: delivery is in the subscription price",
417
+ value: "free"
418
+ }, {
419
+ title: "The region’s rate with every delivery",
420
+ value: "rate"
421
+ }], coversEverywhereElse = (region) => isObject(region) && region.everywhereElse === !0;
350
422
  /** The regions from the plugin's options, as the shop settings start with them. */
351
423
  function initialRegions(config) {
352
424
  return config.shop.shippingRegions.map((region, index) => ({
@@ -354,7 +426,8 @@ function initialRegions(config) {
354
426
  _type: SHIPPING_REGION_TYPE,
355
427
  title: region.title,
356
428
  ...region.everywhereElse ? { everywhereElse: !0 } : { countries: [...region.countries ?? []] },
357
- rate: region.rate
429
+ rate: region.rate,
430
+ ...typeof region.freeFrom == "number" ? { freeFrom: region.freeFrom } : {}
358
431
  }));
359
432
  }
360
433
  function createShippingRegionType(config) {
@@ -395,18 +468,30 @@ function createShippingRegionType(config) {
395
468
  type: "number",
396
469
  description: "One flat rate per order.",
397
470
  validation: (rule) => rule.required().min(0).precision(config.priceDecimals)
471
+ }),
472
+ defineField({
473
+ name: "freeFrom",
474
+ title: `Free from (${config.currencyLabel})`,
475
+ type: "number",
476
+ description: "Optional. An order whose goods add up to this amount or more is delivered free. It goes by the goods before any promotion code.",
477
+ validation: (rule) => rule.positive().precision(config.priceDecimals)
398
478
  })
399
479
  ],
400
480
  preview: {
401
481
  select: {
402
482
  title: "title",
403
483
  rate: "rate",
484
+ freeFrom: "freeFrom",
404
485
  countries: "countries",
405
486
  everywhereElse: "everywhereElse"
406
487
  },
407
- prepare: ({ title, rate, countries, everywhereElse }) => ({
488
+ prepare: ({ title, rate, freeFrom, countries, everywhereElse }) => ({
408
489
  title,
409
- subtitle: [typeof rate == "number" ? formatPrice(rate, config.currency) : "No rate", everywhereElse ? "every other country" : countCountries(asArray(countries).length)].join(" · ")
490
+ subtitle: [
491
+ typeof rate == "number" ? formatPrice(rate, config.currency) : "No rate",
492
+ ...typeof freeFrom == "number" ? [`free from ${formatPrice(freeFrom, config.currency)}`] : [],
493
+ everywhereElse ? "every other country" : countCountries(asArray(countries).length)
494
+ ].join(" · ")
410
495
  })
411
496
  }
412
497
  });
@@ -422,32 +507,46 @@ function createShopSettingsType(config) {
422
507
  title: "Shop settings",
423
508
  type: "document",
424
509
  icon: CogIcon,
425
- fields: [defineField({
426
- name: "shippingRegions",
427
- title: "Shipping regions",
428
- type: "array",
429
- description: "Buyers choose one in the cart. Its rate is added at checkout, and Stripe only accepts addresses in its countries.",
430
- of: [defineArrayMember({ type: SHIPPING_REGION_TYPE })],
431
- components: { input: createRegionsInput(regions) },
432
- ...regions.length ? { initialValue: regions } : {},
433
- validation: (rule) => rule.required().min(1).custom((value) => {
434
- if (!value) return !0;
435
- if (value.filter((region) => region.everywhereElse).length > 1) return "Only one region can cover everywhere else.";
436
- let seen = /* @__PURE__ */ new Map();
437
- for (let region of value) if (!region.everywhereElse) for (let country of region.countries ?? []) {
438
- let other = seen.get(country);
439
- if (other) return `${country} is in both ${other} and ${region.title}.`;
440
- seen.set(country, region.title);
441
- }
442
- return !0;
510
+ fields: [
511
+ defineField({
512
+ name: "shippingRegions",
513
+ title: "Shipping regions",
514
+ type: "array",
515
+ description: "Buyers choose one in the cart. Its rate is added at checkout, and Stripe only accepts addresses in its countries.",
516
+ of: [defineArrayMember({ type: SHIPPING_REGION_TYPE })],
517
+ components: { input: createRegionsInput(regions) },
518
+ ...regions.length ? { initialValue: regions } : {},
519
+ validation: (rule) => rule.required().min(1).custom((value) => {
520
+ if (!value) return !0;
521
+ if (value.filter((region) => region.everywhereElse).length > 1) return "Only one region can cover everywhere else.";
522
+ let seen = /* @__PURE__ */ new Map();
523
+ for (let region of value) if (!region.everywhereElse) for (let country of region.countries ?? []) {
524
+ let other = seen.get(country);
525
+ if (other) return `${country} is in both ${other} and ${region.title}.`;
526
+ seen.set(country, region.title);
527
+ }
528
+ return !0;
529
+ })
530
+ }),
531
+ defineField({
532
+ name: "autoSoldOut",
533
+ title: "Sell out automatically",
534
+ type: "boolean",
535
+ description: "When the stock count of a product or variant reaches 0, mark it sold out.",
536
+ initialValue: !0
537
+ }),
538
+ defineField({
539
+ name: "subscriptionDelivery",
540
+ title: "Delivery for subscriptions",
541
+ type: "string",
542
+ description: "Only matters when a product offers a subscription. With the region’s rate, the rate is charged with the first delivery and every one after it, unless the subscription reaches the region’s \"free from\" amount.",
543
+ options: {
544
+ list: SUBSCRIPTION_DELIVERY,
545
+ layout: "radio"
546
+ },
547
+ initialValue: "free"
443
548
  })
444
- }), defineField({
445
- name: "autoSoldOut",
446
- title: "Sell out automatically",
447
- type: "boolean",
448
- description: "When the stock count of a product or variant reaches 0, mark it sold out.",
449
- initialValue: !0
450
- })],
549
+ ],
451
550
  preview: { prepare: () => ({ title: "Shop settings" }) }
452
551
  });
453
552
  }
@@ -467,8 +566,9 @@ function settingsActions(actions, schemaType) {
467
566
  }
468
567
  /**
469
568
  * Selling with Stripe Checkout from the Studio: the `stripeCommerce` field (price, tax class,
470
- * on sale or sold out, variants, and stock counts kept out of the public dataset) and the shop
471
- * settings document (shipping regions, selling out at 0).
569
+ * on sale or sold out, an optional subscription, variants, and stock counts kept out of the
570
+ * public dataset) and the shop settings document (shipping regions, selling out at 0, delivery
571
+ * for subscriptions).
472
572
  *
473
573
  * ```ts
474
574
  * plugins: [structureTool({structure}), stripeCommerce({currency: 'eur'})]
@@ -484,6 +584,7 @@ const stripeCommerce = definePlugin((options) => {
484
584
  schema: { types: [
485
585
  createCommerceType(config),
486
586
  createVariantType(),
587
+ createSubscriptionType(config),
487
588
  createShippingRegionType(config),
488
589
  createShopSettingsType(config)
489
590
  ] },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/studio/config.ts","../src/studio/stock.ts","../src/studio/StockInput.tsx","../src/studio/schema/commerce.ts","../src/studio/RegionsInput.tsx","../src/studio/schema/settings.ts","../src/studio/plugin.ts","../src/studio/structure.ts"],"sourcesContent":["import {priceDecimals} from '../core/money'\n\n/** The Studio's API version for the plugin's own reads and writes. */\nexport const API_VERSION = '2025-02-19'\n\n/** A tax class an editor can give a product, such as a reduced and a standard VAT rate. */\nexport interface TaxClass {\n /** What the editor reads, such as \"Printed matter (9%)\". */\n title: string\n /** What is saved, and what the site maps to a Stripe tax rate, such as `reduced`. */\n value: string\n}\n\n/** A shipping region to start the shop settings with. */\nexport interface ShippingRegionInput {\n title: string\n /** ISO 3166-1 alpha-2 codes, such as `['NL']` or `[...EU_COUNTRIES]`. */\n countries?: readonly string[]\n /** Covers every country Stripe ships to that no other region lists. */\n everywhereElse?: boolean\n /** One flat rate per order, in currency units. */\n rate: number\n}\n\nexport interface StripeCommerceConfig {\n /** ISO 4217 code of the currency every price is in, such as `eur` or `usd`. */\n currency: string\n tax?: {\n /** What tax is called where the shop is: `VAT`, `GST`, `sales tax`. Default `tax`. */\n label?: string\n /** Whether the prices editors enter include tax. It only changes the labels. Default true. */\n included?: boolean\n /** Tax classes editors choose from. Without any, products have no tax class field. */\n classes?: readonly TaxClass[]\n }\n /** Starting values for the shop settings document, offered while it has no regions. */\n shop?: {\n shippingRegions?: readonly ShippingRegionInput[]\n }\n}\n\nexport interface ResolvedConfig {\n /** Lower case. */\n currency: string\n /** Upper case, for labels. */\n currencyLabel: string\n priceDecimals: 0 | 2\n tax: {label: string; included: boolean; classes: readonly TaxClass[]}\n shop: {shippingRegions: readonly ShippingRegionInput[]}\n}\n\nexport function resolveConfig(config: StripeCommerceConfig): ResolvedConfig {\n const currency = typeof config?.currency === 'string' ? config.currency.trim().toLowerCase() : ''\n if (!/^[a-z]{3}$/.test(currency))\n throw new TypeError(\n `sanity-plugin-stripe: currency must be a three-letter code, such as stripeCommerce({currency: 'eur'})`,\n )\n return {\n currency,\n currencyLabel: currency.toUpperCase(),\n priceDecimals: priceDecimals(currency),\n tax: {\n label: config.tax?.label?.trim() || 'tax',\n included: config.tax?.included ?? true,\n classes: config.tax?.classes ?? [],\n },\n shop: {shippingRegions: config.shop?.shippingRegions ?? []},\n }\n}\n\n/** \"Price (EUR, including VAT)\", or \"Price (EUR)\" when prices are entered without tax. */\nexport function moneyTitle(name: string, config: ResolvedConfig): string {\n return config.tax.included\n ? `${name} (${config.currencyLabel}, including ${config.tax.label})`\n : `${name} (${config.currencyLabel})`\n}\n","import {STOCK_TYPE, stockId} from '../core/ids'\nimport {readStockDocuments, STOCK_QUERY, stockLeft, type ProductStock} from '../core/stock'\n\n/**\n * Reading and writing a product's stock counts from the Studio. They live in their own document,\n * with a dot in its id, so a public dataset never serves them: the editor's own session reads and\n * writes it, and nobody without a token can.\n */\n\ninterface StockPatch {\n set?: Record<string, unknown>\n unset?: string[]\n setIfMissing?: Record<string, unknown>\n insert?: {after: string; items: unknown[]}\n}\n\ninterface StockTransaction {\n createIfNotExists(document: {_id: string; _type: string}): StockTransaction\n patch(documentId: string, operations: StockPatch): StockTransaction\n commit(): Promise<unknown>\n}\n\n/** As much of the Studio's client as stock needs. */\nexport interface StockClient {\n fetch(query: string, params?: Record<string, unknown>): Promise<unknown>\n transaction(): StockTransaction\n}\n\nconst SAFE_KEY = /^[\\w-]+$/\nconst EMPTY: ProductStock = {stockCount: null, variants: []}\n\n/** A product's stock, or empty counts when it has no stock document yet. */\nexport async function loadStock(client: StockClient, productId: string): Promise<ProductStock> {\n const found = await client.fetch(STOCK_QUERY, {ids: [stockId(productId)]})\n return readStockDocuments(found).get(productId) ?? EMPTY\n}\n\n/**\n * A product's stock from its stock document, as a change event carries it, or empty counts when\n * the document is gone. A query asked right after an event can still answer with the counts from\n * before it, so the boxes take the document the event brings.\n */\nexport function stockFromDocument(productId: string, document: unknown): ProductStock {\n return readStockDocuments(document ? [document] : []).get(productId) ?? EMPTY\n}\n\n/** What an editor typed in a stock box: a whole number from 0 up, or nothing for \"not counted\". */\nexport function parseCount(text: string): number | null | 'invalid' {\n const trimmed = text.trim()\n if (trimmed === '') return null\n return /^\\d{1,9}$/.test(trimmed) ? Number(trimmed) : 'invalid'\n}\n\n/**\n * Sets one count, of the product or of one variant, leaving every other count as it is: an order\n * coming in at the same moment keeps its own change. `count: null` stops counting.\n */\nexport async function saveStock(\n client: StockClient,\n productId: string,\n current: ProductStock,\n change: {variantKey?: string; count: number | null},\n): Promise<void> {\n const {variantKey, count} = change\n if (variantKey !== undefined && !SAFE_KEY.test(variantKey))\n throw new TypeError(`\"${variantKey}\" is not a variant key`)\n const id = stockId(productId)\n const transaction = client.transaction().createIfNotExists({_id: id, _type: STOCK_TYPE})\n\n if (variantKey === undefined) {\n transaction.patch(id, count === null ? {unset: ['stockCount']} : {set: {stockCount: count}})\n } else {\n const entry = `variants[_key==\"${variantKey}\"]`\n const exists = current.variants.some((variant) => variant._key === variantKey)\n if (exists) {\n transaction.patch(\n id,\n count === null ? {unset: [entry]} : {set: {[`${entry}.stockCount`]: count}},\n )\n } else if (count !== null) {\n transaction.patch(id, {setIfMissing: {variants: []}}).patch(id, {\n insert: {after: 'variants[-1]', items: [{_key: variantKey, stockCount: count}]},\n })\n } else {\n return\n }\n }\n await transaction.commit()\n}\n\nexport {stockLeft}\n","import {Card, Flex, Stack, Text, TextInput} from '@sanity/ui'\nimport {useEffect, useState} from 'react'\nimport {useClient, useFormValue, type ObjectInputProps} from 'sanity'\n\nimport {asArray, isObject} from '../core/guards'\nimport {publishedId, stockId} from '../core/ids'\nimport type {ProductStock} from '../core/stock'\nimport {API_VERSION} from './config'\nimport {loadStock, parseCount, saveStock, stockFromDocument, stockLeft} from './stock'\n\n/**\n * The price-and-availability field, with the stock boxes under it. The boxes don't belong to the\n * product document: they read and write the product's private stock document, so a count is\n * saved at once and is never part of what a public dataset serves.\n */\nexport function CommerceInput(props: ObjectInputProps) {\n return (\n <Stack gap={5}>\n {props.renderDefault(props)}\n <StockBoxes commerce={props.value} readOnly={Boolean(props.readOnly)} />\n </Stack>\n )\n}\n\ninterface VariantRow {\n key: string\n label: string\n soldOut: boolean\n}\n\nfunction readVariants(commerce: unknown): VariantRow[] {\n if (!isObject(commerce)) return []\n return asArray(commerce.variants).flatMap((variant) =>\n isObject(variant) && typeof variant._key === 'string'\n ? [\n {\n key: variant._key,\n label: typeof variant.label === 'string' && variant.label ? variant.label : 'Unnamed',\n soldOut: variant.availability === 'soldOut',\n },\n ]\n : [],\n )\n}\n\nfunction StockBoxes({commerce, readOnly}: {commerce: unknown; readOnly: boolean}) {\n const client = useClient({apiVersion: API_VERSION})\n const documentId = useFormValue(['_id'])\n const productId = typeof documentId === 'string' ? publishedId(documentId) : null\n const [stock, setStock] = useState<ProductStock | null>(null)\n const [failure, setFailure] = useState<string | null>(null)\n\n // Read the counts, and follow them while the form is open: an order lowers them.\n useEffect(() => {\n if (!productId) return undefined\n let live = true\n let changed = false\n const read = async () => {\n const found = await loadStock(client, productId).catch(() => null)\n // A change that arrived while this was on its way is newer than what it found.\n if (!live || changed) return\n if (found) setStock(found)\n setFailure(found ? null : 'The stock counts could not be read.')\n }\n void read()\n const changes = client.listen('*[_id == $id]', {id: stockId(productId)}).subscribe({\n // The event brings the document as it is now. Asking for it again can still get the old one.\n next: (event) => {\n if (!live) return\n changed = true\n setStock(stockFromDocument(productId, event.result))\n setFailure(null)\n },\n error: () => {},\n })\n return () => {\n live = false\n changes.unsubscribe()\n }\n }, [client, productId])\n\n const save = async (variantKey: string | undefined, count: number | null) => {\n if (!productId || !stock) return\n try {\n await saveStock(client, productId, stock, {\n ...(variantKey === undefined ? {} : {variantKey}),\n count,\n })\n setStock(await loadStock(client, productId))\n setFailure(null)\n } catch {\n setFailure('The count could not be saved. Your role may not allow it.')\n }\n }\n\n const variants = readVariants(commerce)\n const productSoldOut = isObject(commerce) && commerce.availability === 'soldOut'\n\n return (\n <Card border padding={3} radius={2}>\n <Stack gap={4}>\n <Stack gap={3}>\n <Text size={1} weight=\"semibold\">\n In stock\n </Text>\n <Text muted size={1}>\n Optional. Leave a box empty to sell without counting. Counts are saved as you leave the\n box and are never public: visitors see on sale or sold out, not a number. They are\n checked at checkout when the site has a Sanity token.\n </Text>\n </Stack>\n {!stock && !failure && (\n <Text muted size={1}>\n Reading the counts…\n </Text>\n )}\n {stock && variants.length === 0 && (\n <StockBox\n label=\"This product\"\n count={stockLeft(stock)}\n disabled={readOnly}\n warn={productSoldOut}\n onSave={(count) => save(undefined, count)}\n />\n )}\n {stock &&\n variants.map((variant) => (\n <StockBox\n key={variant.key}\n label={variant.label}\n count={stockLeft(stock, variant.key)}\n disabled={readOnly}\n warn={productSoldOut || variant.soldOut}\n onSave={(count) => save(variant.key, count)}\n />\n ))}\n {failure && (\n <Card padding={3} radius={2} tone=\"critical\">\n <Text size={1}>{failure}</Text>\n </Card>\n )}\n </Stack>\n </Card>\n )\n}\n\nfunction StockBox(props: {\n label: string\n count: number | null\n disabled: boolean\n /** The product or variant is switched to sold out. */\n warn: boolean\n onSave: (count: number | null) => Promise<void>\n}) {\n const {label, count, disabled, warn, onSave} = props\n const saved = count === null ? '' : String(count)\n const [text, setText] = useState(saved)\n const [seen, setSeen] = useState(saved)\n // The saved count changed under the box (an order, another editor): show it.\n if (saved !== seen) {\n setSeen(saved)\n setText(saved)\n }\n\n const parsed = parseCount(text)\n const commit = () => {\n if (parsed === 'invalid' || text.trim() === saved) return\n void onSave(parsed)\n }\n\n return (\n <Stack gap={2}>\n <Flex align=\"center\" gap={3}>\n <Text size={1} style={{flex: 1}}>\n {label}\n </Text>\n <div style={{width: '7rem'}}>\n <TextInput\n aria-label={`In stock: ${label}`}\n inputMode=\"numeric\"\n placeholder=\"Not counted\"\n disabled={disabled}\n value={text}\n customValidity={parsed === 'invalid' ? 'A whole number, 0 or more' : ''}\n onChange={(event) => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commit()\n }}\n />\n </div>\n </Flex>\n {parsed === 'invalid' && (\n <Text size={1} muted>\n Use a whole number, 0 or more.\n </Text>\n )}\n {warn && typeof count === 'number' && count > 0 && (\n <Text size={1} muted>\n {label} has stock again but is switched to sold out. Put it back on sale above.\n </Text>\n )}\n </Stack>\n )\n}\n","import {CreditCardIcon} from '@sanity/icons/CreditCard'\nimport {TagIcon} from '@sanity/icons/Tag'\nimport {defineArrayMember, defineField, defineType} from 'sanity'\n\nimport type {Availability} from '../../core/cart'\nimport {isObject} from '../../core/guards'\nimport {COMMERCE_TYPE, VARIANT_TYPE} from '../../core/ids'\nimport {moneyTitle, type ResolvedConfig} from '../config'\nimport {CommerceInput} from '../StockInput'\n\nconst AVAILABILITY: Array<{title: string; value: Availability}> = [\n {title: 'On sale', value: 'onSale'},\n {title: 'Sold out', value: 'soldOut'},\n]\n\n/** A variant as the Studio saves it. */\nexport interface StripeVariantValue {\n _key: string\n label: string\n availability?: Availability\n}\n\n/**\n * The price-and-availability field as the Studio saves it, on a document anyone can read. Stock\n * counts are not in it: they are kept in a private document of their own.\n */\nexport interface StripeCommerceValue {\n availability: Availability\n /** In currency units (25 means €25). */\n price?: number\n taxClass?: string\n variants?: StripeVariantValue[]\n}\n\n// The field's own value, as validation gets it: unknown, and maybe half filled in.\nconst isOnSale = (commerce: unknown) => isObject(commerce) && commerce.availability === 'onSale'\n\nexport function createVariantType() {\n return defineType({\n name: VARIANT_TYPE,\n title: 'Variant',\n type: 'object',\n icon: TagIcon,\n fields: [\n defineField({\n name: 'label',\n type: 'string',\n description: 'What the buyer chooses, for example \"Black, L\".',\n validation: (rule) => rule.required(),\n }),\n defineField({\n name: 'availability',\n type: 'string',\n options: {list: AVAILABILITY, layout: 'radio', direction: 'horizontal'},\n initialValue: 'onSale',\n }),\n ],\n preview: {\n select: {title: 'label', availability: 'availability'},\n prepare: ({title, availability}) => ({\n title,\n subtitle: availability === 'soldOut' ? 'Sold out' : 'On sale',\n }),\n },\n })\n}\n\nexport function createCommerceType(config: ResolvedConfig) {\n const taxClasses = config.tax.classes\n return defineType({\n name: COMMERCE_TYPE,\n title: 'Price and stock',\n type: 'object',\n icon: CreditCardIcon,\n // Adds the stock boxes, which are saved outside this document.\n components: {input: CommerceInput},\n fields: [\n defineField({\n name: 'availability',\n type: 'string',\n options: {list: AVAILABILITY, layout: 'radio', direction: 'horizontal'},\n initialValue: 'onSale',\n validation: (rule) => rule.required(),\n }),\n defineField({\n name: 'price',\n title: moneyTitle('Price', config),\n type: 'number',\n validation: (rule) =>\n rule\n .min(0)\n .precision(config.priceDecimals)\n .custom((price, context) =>\n isOnSale(context.parent) && typeof price !== 'number'\n ? 'A product on sale needs a price.'\n : true,\n ),\n }),\n ...(taxClasses.length\n ? [\n defineField({\n name: 'taxClass',\n title: config.tax.label.charAt(0).toUpperCase() + config.tax.label.slice(1),\n type: 'string',\n options: {\n list: taxClasses.map(({title, value}) => ({title, value})),\n layout: 'radio',\n },\n initialValue: taxClasses[0]?.value,\n validation: (rule) => rule.required(),\n }),\n ]\n : []),\n defineField({\n name: 'variants',\n type: 'array',\n description: 'Sizes or colours. Each variant can sell out on its own.',\n of: [defineArrayMember({type: VARIANT_TYPE})],\n }),\n ],\n })\n}\n","import {Button, Card, Flex, Stack, Text} from '@sanity/ui'\nimport {set, type ArrayOfObjectsInputProps} from 'sanity'\n\nimport type {ShippingRegionValue} from './schema/settings'\n\n/**\n * The shipping regions list. While it is empty it offers the regions from the plugin's options.\n * A document's starting values come from its template, and a Studio that keeps the settings to\n * one document usually removes that template, so the offer is made here instead.\n */\nexport function createRegionsInput(starting: ShippingRegionValue[]) {\n return function RegionsInput(props: ArrayOfObjectsInputProps) {\n const empty = !props.value?.length\n return (\n <Stack gap={3}>\n {empty && starting.length > 0 && !props.readOnly && (\n <Card border padding={3} radius={2}>\n <Flex align=\"center\" gap={3}>\n <Text size={1} style={{flex: 1}}>\n No shipping regions yet. Start from the {starting.length} this Studio was set up\n with, then change the rates.\n </Text>\n <Button\n mode=\"ghost\"\n text=\"Use the starting regions\"\n onClick={() => props.onChange(set(starting))}\n />\n </Flex>\n </Card>\n )}\n {props.renderDefault(props)}\n </Stack>\n )\n }\n}\n","import {CogIcon} from '@sanity/icons/Cog'\nimport {PinIcon} from '@sanity/icons/Pin'\nimport {defineArrayMember, defineField, defineType} from 'sanity'\n\nimport {asArray, isObject} from '../../core/guards'\nimport {SHIPPING_REGION_TYPE, SHOP_SETTINGS_TYPE} from '../../core/ids'\nimport {formatPrice} from '../../core/money'\nimport {moneyTitle, type ResolvedConfig} from '../config'\nimport {createRegionsInput} from '../RegionsInput'\n\n/** A shipping region as the Studio saves it. */\nexport interface ShippingRegionValue {\n _key: string\n _type?: typeof SHIPPING_REGION_TYPE\n title: string\n /** Every country Stripe ships to that no other region lists. */\n everywhereElse?: boolean\n countries?: string[]\n /** Flat rate per order, in currency units. */\n rate: number\n}\n\n/**\n * The shop settings document as the Studio saves it. Anyone can read it from a public dataset,\n * so it holds only what the cart shows: no email address, no key.\n */\nexport interface ShopSettingsValue {\n shippingRegions?: ShippingRegionValue[]\n autoSoldOut?: boolean\n}\n\n// A region as validation and `hidden` get it: unknown, and maybe half filled in.\nconst coversEverywhereElse = (region: unknown) => isObject(region) && region.everywhereElse === true\n\n/** The regions from the plugin's options, as the shop settings start with them. */\nexport function initialRegions(config: ResolvedConfig): ShippingRegionValue[] {\n return config.shop.shippingRegions.map((region, index) => ({\n _key: `region-${index + 1}`,\n _type: SHIPPING_REGION_TYPE,\n title: region.title,\n ...(region.everywhereElse\n ? {everywhereElse: true}\n : {countries: [...(region.countries ?? [])]}),\n rate: region.rate,\n }))\n}\n\nexport function createShippingRegionType(config: ResolvedConfig) {\n return defineType({\n name: SHIPPING_REGION_TYPE,\n title: 'Shipping region',\n type: 'object',\n icon: PinIcon,\n fields: [\n defineField({\n name: 'title',\n type: 'string',\n description: 'Shown in the cart, for example \"European Union\".',\n validation: (rule) => rule.required(),\n }),\n defineField({\n name: 'everywhereElse',\n title: 'Everywhere else',\n type: 'boolean',\n description: 'Covers every country Stripe ships to that no other region lists.',\n initialValue: false,\n }),\n defineField({\n name: 'countries',\n type: 'array',\n description:\n 'Two-letter country codes, for example NL or DE. Stripe only accepts addresses in these countries.',\n of: [\n defineArrayMember({\n type: 'string',\n validation: (rule) =>\n rule\n .regex(/^[A-Z]{2}$/, {name: 'country code'})\n .error('Use a two-letter code in capitals.'),\n }),\n ],\n options: {layout: 'tags'},\n hidden: ({parent}) => coversEverywhereElse(parent),\n validation: (rule) =>\n rule.custom((countries, context) =>\n coversEverywhereElse(context.parent) || countries?.length\n ? true\n : 'Add at least one country.',\n ),\n }),\n defineField({\n name: 'rate',\n title: moneyTitle('Shipping', config),\n type: 'number',\n description: 'One flat rate per order.',\n validation: (rule) => rule.required().min(0).precision(config.priceDecimals),\n }),\n ],\n preview: {\n select: {\n title: 'title',\n rate: 'rate',\n countries: 'countries',\n everywhereElse: 'everywhereElse',\n },\n prepare: ({title, rate, countries, everywhereElse}) => ({\n title,\n subtitle: [\n typeof rate === 'number' ? formatPrice(rate, config.currency) : 'No rate',\n everywhereElse ? 'every other country' : countCountries(asArray(countries).length),\n ].join(' · '),\n }),\n },\n })\n}\n\n/** How many countries a region lists, for its line in the Studio. */\nexport function countCountries(count: number): string {\n return count === 1 ? '1 country' : `${count} countries`\n}\n\nexport function createShopSettingsType(config: ResolvedConfig) {\n const regions = initialRegions(config)\n return defineType({\n name: SHOP_SETTINGS_TYPE,\n title: 'Shop settings',\n type: 'document',\n icon: CogIcon,\n fields: [\n defineField({\n name: 'shippingRegions',\n title: 'Shipping regions',\n type: 'array',\n description:\n 'Buyers choose one in the cart. Its rate is added at checkout, and Stripe only accepts addresses in its countries.',\n of: [defineArrayMember({type: SHIPPING_REGION_TYPE})],\n components: {input: createRegionsInput(regions)},\n ...(regions.length ? {initialValue: regions} : {}),\n validation: (rule) =>\n rule\n .required()\n .min(1)\n .custom((value: ShippingRegionValue[] | undefined) => {\n if (!value) return true\n if (value.filter((region) => region.everywhereElse).length > 1) {\n return 'Only one region can cover everywhere else.'\n }\n const seen = new Map<string, string>()\n for (const region of value) {\n if (region.everywhereElse) continue\n for (const country of region.countries ?? []) {\n const other = seen.get(country)\n if (other) return `${country} is in both ${other} and ${region.title}.`\n seen.set(country, region.title)\n }\n }\n return true\n }),\n }),\n defineField({\n name: 'autoSoldOut',\n title: 'Sell out automatically',\n type: 'boolean',\n description: 'When the stock count of a product or variant reaches 0, mark it sold out.',\n initialValue: true,\n }),\n ],\n preview: {prepare: () => ({title: 'Shop settings'})},\n })\n}\n","import {definePlugin} from 'sanity'\n\nimport {SHOP_SETTINGS_TYPE} from '../core/ids'\nimport {resolveConfig, type StripeCommerceConfig} from './config'\nimport {createCommerceType, createVariantType} from './schema/commerce'\nimport {createShippingRegionType, createShopSettingsType} from './schema/settings'\n\n/** What an editor can do with the one shop settings document: not duplicate or delete it. */\nconst SETTINGS_ACTIONS = new Set(['publish', 'discardChanges', 'restore'])\n\n/** There is one shop settings document, made by opening it in the structure: no \"new\" for it. */\nexport function withoutSettingsTemplate<T extends {templateId: string}>(options: T[]): T[] {\n return options.filter((option) => option.templateId !== SHOP_SETTINGS_TYPE)\n}\n\n/** The shop settings keep publish, discard and restore; every other document keeps everything. */\nexport function settingsActions<T extends {action?: string}>(\n actions: T[],\n schemaType: string,\n): T[] {\n return schemaType === SHOP_SETTINGS_TYPE\n ? actions.filter(({action}) => action !== undefined && SETTINGS_ACTIONS.has(action))\n : actions\n}\n\n/**\n * Selling with Stripe Checkout from the Studio: the `stripeCommerce` field (price, tax class,\n * on sale or sold out, variants, and stock counts kept out of the public dataset) and the shop\n * settings document (shipping regions, selling out at 0).\n *\n * ```ts\n * plugins: [structureTool({structure}), stripeCommerce({currency: 'eur'})]\n * ```\n *\n * Give a product the field with `defineField({name: 'commerce', type: 'stripeCommerce'})`, and\n * put the settings in the structure with `shopSettingsListItem(S)`.\n */\nexport const stripeCommerce = definePlugin<StripeCommerceConfig>((options) => {\n const config = resolveConfig(options)\n return {\n name: 'sanity-plugin-stripe',\n schema: {\n types: [\n createCommerceType(config),\n createVariantType(),\n createShippingRegionType(config),\n createShopSettingsType(config),\n ],\n },\n document: {\n newDocumentOptions: (previous) => withoutSettingsTemplate(previous),\n actions: (previous, context) => settingsActions(previous, context.schemaType),\n },\n }\n})\n","import {CogIcon} from '@sanity/icons/Cog'\nimport type {ListItemBuilder, StructureBuilder} from 'sanity/structure'\n\nimport {SHOP_SETTINGS_ID, SHOP_SETTINGS_TYPE} from '../core/ids'\n\n/**\n * The shop settings as one item in the Studio's structure, opening the one document:\n *\n * ```ts\n * S.list().items([S.documentTypeListItem('product'), shopSettingsListItem(S)])\n * ```\n */\nexport function shopSettingsListItem(\n S: StructureBuilder,\n options: {title?: string} = {},\n): ListItemBuilder {\n const title = options.title ?? 'Shop settings'\n return S.listItem()\n .title(title)\n .id(SHOP_SETTINGS_ID)\n .icon(CogIcon)\n .child(S.document().schemaType(SHOP_SETTINGS_TYPE).documentId(SHOP_SETTINGS_ID).title(title))\n}\n\n/** True for the shop settings type's own list item, to leave it out of `S.documentTypeListItems()`. */\nexport function isShopSettingsListItem(item: ListItemBuilder): boolean {\n return item.getId() === SHOP_SETTINGS_TYPE\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/studio/config.ts","../src/studio/stock.ts","../src/studio/StockInput.tsx","../src/studio/schema/commerce.ts","../src/studio/RegionsInput.tsx","../src/studio/schema/settings.ts","../src/studio/plugin.ts","../src/studio/structure.ts"],"sourcesContent":["import {priceDecimals} from '../core/money'\n\n/** The Studio's API version for the plugin's own reads and writes. */\nexport const API_VERSION = '2025-02-19'\n\n/** A tax class an editor can give a product, such as a reduced and a standard VAT rate. */\nexport interface TaxClass {\n /** What the editor reads, such as \"Printed matter (9%)\". */\n title: string\n /** What is saved, and what the site maps to a Stripe tax rate, such as `reduced`. */\n value: string\n}\n\n/** A shipping region to start the shop settings with. */\nexport interface ShippingRegionInput {\n title: string\n /** ISO 3166-1 alpha-2 codes, such as `['NL']` or `[...EU_COUNTRIES]`. */\n countries?: readonly string[]\n /** Covers every country Stripe ships to that no other region lists. */\n everywhereElse?: boolean\n /** One flat rate per order, in currency units. */\n rate: number\n /** Goods adding up to this amount or more are delivered free. In currency units. */\n freeFrom?: number\n}\n\nexport interface StripeCommerceConfig {\n /** ISO 4217 code of the currency every price is in, such as `eur` or `usd`. */\n currency: string\n tax?: {\n /** What tax is called where the shop is: `VAT`, `GST`, `sales tax`. Default `tax`. */\n label?: string\n /** Whether the prices editors enter include tax. It only changes the labels. Default true. */\n included?: boolean\n /** Tax classes editors choose from. Without any, products have no tax class field. */\n classes?: readonly TaxClass[]\n }\n /** Starting values for the shop settings document, offered while it has no regions. */\n shop?: {\n shippingRegions?: readonly ShippingRegionInput[]\n }\n}\n\nexport interface ResolvedConfig {\n /** Lower case. */\n currency: string\n /** Upper case, for labels. */\n currencyLabel: string\n priceDecimals: 0 | 2\n tax: {label: string; included: boolean; classes: readonly TaxClass[]}\n shop: {shippingRegions: readonly ShippingRegionInput[]}\n}\n\nexport function resolveConfig(config: StripeCommerceConfig): ResolvedConfig {\n const currency = typeof config?.currency === 'string' ? config.currency.trim().toLowerCase() : ''\n if (!/^[a-z]{3}$/.test(currency))\n throw new TypeError(\n `sanity-plugin-stripe: currency must be a three-letter code, such as stripeCommerce({currency: 'eur'})`,\n )\n return {\n currency,\n currencyLabel: currency.toUpperCase(),\n priceDecimals: priceDecimals(currency),\n tax: {\n label: config.tax?.label?.trim() || 'tax',\n included: config.tax?.included ?? true,\n classes: config.tax?.classes ?? [],\n },\n shop: {shippingRegions: config.shop?.shippingRegions ?? []},\n }\n}\n\n/** \"Price (EUR, including VAT)\", or \"Price (EUR)\" when prices are entered without tax. */\nexport function moneyTitle(name: string, config: ResolvedConfig): string {\n return config.tax.included\n ? `${name} (${config.currencyLabel}, including ${config.tax.label})`\n : `${name} (${config.currencyLabel})`\n}\n","import {STOCK_TYPE, stockId} from '../core/ids'\nimport {readStockDocuments, STOCK_QUERY, stockLeft, type ProductStock} from '../core/stock'\n\n/**\n * Reading and writing a product's stock counts from the Studio. They live in their own document,\n * with a dot in its id, so a public dataset never serves them: the editor's own session reads and\n * writes it, and nobody without a token can.\n */\n\ninterface StockPatch {\n set?: Record<string, unknown>\n unset?: string[]\n setIfMissing?: Record<string, unknown>\n insert?: {after: string; items: unknown[]}\n}\n\ninterface StockTransaction {\n createIfNotExists(document: {_id: string; _type: string}): StockTransaction\n patch(documentId: string, operations: StockPatch): StockTransaction\n commit(): Promise<unknown>\n}\n\n/** As much of the Studio's client as stock needs. */\nexport interface StockClient {\n fetch(query: string, params?: Record<string, unknown>): Promise<unknown>\n transaction(): StockTransaction\n}\n\nconst SAFE_KEY = /^[\\w-]+$/\nconst EMPTY: ProductStock = {stockCount: null, variants: []}\n\n/** A product's stock, or empty counts when it has no stock document yet. */\nexport async function loadStock(client: StockClient, productId: string): Promise<ProductStock> {\n const found = await client.fetch(STOCK_QUERY, {ids: [stockId(productId)]})\n return readStockDocuments(found).get(productId) ?? EMPTY\n}\n\n/**\n * A product's stock from its stock document, as a change event carries it, or empty counts when\n * the document is gone. A query asked right after an event can still answer with the counts from\n * before it, so the boxes take the document the event brings.\n */\nexport function stockFromDocument(productId: string, document: unknown): ProductStock {\n return readStockDocuments(document ? [document] : []).get(productId) ?? EMPTY\n}\n\n/** What an editor typed in a stock box: a whole number from 0 up, or nothing for \"not counted\". */\nexport function parseCount(text: string): number | null | 'invalid' {\n const trimmed = text.trim()\n if (trimmed === '') return null\n return /^\\d{1,9}$/.test(trimmed) ? Number(trimmed) : 'invalid'\n}\n\n/**\n * Sets one count, of the product or of one variant, leaving every other count as it is: an order\n * coming in at the same moment keeps its own change. `count: null` stops counting.\n */\nexport async function saveStock(\n client: StockClient,\n productId: string,\n current: ProductStock,\n change: {variantKey?: string; count: number | null},\n): Promise<void> {\n const {variantKey, count} = change\n if (variantKey !== undefined && !SAFE_KEY.test(variantKey))\n throw new TypeError(`\"${variantKey}\" is not a variant key`)\n const id = stockId(productId)\n const transaction = client.transaction().createIfNotExists({_id: id, _type: STOCK_TYPE})\n\n if (variantKey === undefined) {\n transaction.patch(id, count === null ? {unset: ['stockCount']} : {set: {stockCount: count}})\n } else {\n const entry = `variants[_key==\"${variantKey}\"]`\n const exists = current.variants.some((variant) => variant._key === variantKey)\n if (exists) {\n transaction.patch(\n id,\n count === null ? {unset: [entry]} : {set: {[`${entry}.stockCount`]: count}},\n )\n } else if (count !== null) {\n transaction.patch(id, {setIfMissing: {variants: []}}).patch(id, {\n insert: {after: 'variants[-1]', items: [{_key: variantKey, stockCount: count}]},\n })\n } else {\n return\n }\n }\n await transaction.commit()\n}\n\nexport {stockLeft}\n","import {Card, Flex, Stack, Text, TextInput} from '@sanity/ui'\nimport {useEffect, useState} from 'react'\nimport {useClient, useFormValue, type ObjectInputProps} from 'sanity'\n\nimport {asArray, isObject} from '../core/guards'\nimport {publishedId, stockId} from '../core/ids'\nimport type {ProductStock} from '../core/stock'\nimport {API_VERSION} from './config'\nimport {loadStock, parseCount, saveStock, stockFromDocument, stockLeft} from './stock'\n\n/**\n * The price-and-availability field, with the stock boxes under it. The boxes don't belong to the\n * product document: they read and write the product's private stock document, so a count is\n * saved at once and is never part of what a public dataset serves.\n */\nexport function CommerceInput(props: ObjectInputProps) {\n return (\n <Stack gap={5}>\n {props.renderDefault(props)}\n <StockBoxes commerce={props.value} readOnly={Boolean(props.readOnly)} />\n </Stack>\n )\n}\n\ninterface VariantRow {\n key: string\n label: string\n soldOut: boolean\n}\n\nfunction readVariants(commerce: unknown): VariantRow[] {\n if (!isObject(commerce)) return []\n return asArray(commerce.variants).flatMap((variant) =>\n isObject(variant) && typeof variant._key === 'string'\n ? [\n {\n key: variant._key,\n label: typeof variant.label === 'string' && variant.label ? variant.label : 'Unnamed',\n soldOut: variant.availability === 'soldOut',\n },\n ]\n : [],\n )\n}\n\nfunction StockBoxes({commerce, readOnly}: {commerce: unknown; readOnly: boolean}) {\n const client = useClient({apiVersion: API_VERSION})\n const documentId = useFormValue(['_id'])\n const productId = typeof documentId === 'string' ? publishedId(documentId) : null\n const [stock, setStock] = useState<ProductStock | null>(null)\n const [failure, setFailure] = useState<string | null>(null)\n\n // Read the counts, and follow them while the form is open: an order lowers them.\n useEffect(() => {\n if (!productId) return undefined\n let live = true\n let changed = false\n const read = async () => {\n const found = await loadStock(client, productId).catch(() => null)\n // A change that arrived while this was on its way is newer than what it found.\n if (!live || changed) return\n if (found) setStock(found)\n setFailure(found ? null : 'The stock counts could not be read.')\n }\n void read()\n const changes = client.listen('*[_id == $id]', {id: stockId(productId)}).subscribe({\n // The event brings the document as it is now. Asking for it again can still get the old one.\n next: (event) => {\n if (!live) return\n changed = true\n setStock(stockFromDocument(productId, event.result))\n setFailure(null)\n },\n error: () => {},\n })\n return () => {\n live = false\n changes.unsubscribe()\n }\n }, [client, productId])\n\n const save = async (variantKey: string | undefined, count: number | null) => {\n if (!productId || !stock) return\n try {\n await saveStock(client, productId, stock, {\n ...(variantKey === undefined ? {} : {variantKey}),\n count,\n })\n setStock(await loadStock(client, productId))\n setFailure(null)\n } catch {\n setFailure('The count could not be saved. Your role may not allow it.')\n }\n }\n\n const variants = readVariants(commerce)\n const productSoldOut = isObject(commerce) && commerce.availability === 'soldOut'\n\n return (\n <Card border padding={3} radius={2}>\n <Stack gap={4}>\n <Stack gap={3}>\n <Text size={1} weight=\"semibold\">\n In stock\n </Text>\n <Text muted size={1}>\n Optional. Leave a box empty to sell without counting. Counts are saved as you leave the\n box and are never public: visitors see on sale or sold out, not a number. They are\n checked at checkout when the site has a Sanity token.\n </Text>\n </Stack>\n {!stock && !failure && (\n <Text muted size={1}>\n Reading the counts…\n </Text>\n )}\n {stock && variants.length === 0 && (\n <StockBox\n label=\"This product\"\n count={stockLeft(stock)}\n disabled={readOnly}\n warn={productSoldOut}\n onSave={(count) => save(undefined, count)}\n />\n )}\n {stock &&\n variants.map((variant) => (\n <StockBox\n key={variant.key}\n label={variant.label}\n count={stockLeft(stock, variant.key)}\n disabled={readOnly}\n warn={productSoldOut || variant.soldOut}\n onSave={(count) => save(variant.key, count)}\n />\n ))}\n {failure && (\n <Card padding={3} radius={2} tone=\"critical\">\n <Text size={1}>{failure}</Text>\n </Card>\n )}\n </Stack>\n </Card>\n )\n}\n\nfunction StockBox(props: {\n label: string\n count: number | null\n disabled: boolean\n /** The product or variant is switched to sold out. */\n warn: boolean\n onSave: (count: number | null) => Promise<void>\n}) {\n const {label, count, disabled, warn, onSave} = props\n const saved = count === null ? '' : String(count)\n const [text, setText] = useState(saved)\n const [seen, setSeen] = useState(saved)\n // The saved count changed under the box (an order, another editor): show it.\n if (saved !== seen) {\n setSeen(saved)\n setText(saved)\n }\n\n const parsed = parseCount(text)\n const commit = () => {\n if (parsed === 'invalid' || text.trim() === saved) return\n void onSave(parsed)\n }\n\n return (\n <Stack gap={2}>\n <Flex align=\"center\" gap={3}>\n <Text size={1} style={{flex: 1}}>\n {label}\n </Text>\n <div style={{width: '7rem'}}>\n <TextInput\n aria-label={`In stock: ${label}`}\n inputMode=\"numeric\"\n placeholder=\"Not counted\"\n disabled={disabled}\n value={text}\n customValidity={parsed === 'invalid' ? 'A whole number, 0 or more' : ''}\n onChange={(event) => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commit()\n }}\n />\n </div>\n </Flex>\n {parsed === 'invalid' && (\n <Text size={1} muted>\n Use a whole number, 0 or more.\n </Text>\n )}\n {warn && typeof count === 'number' && count > 0 && (\n <Text size={1} muted>\n {label} has stock again but is switched to sold out. Put it back on sale above.\n </Text>\n )}\n </Stack>\n )\n}\n","import {CreditCardIcon} from '@sanity/icons/CreditCard'\nimport {SyncIcon} from '@sanity/icons/Sync'\nimport {TagIcon} from '@sanity/icons/Tag'\nimport {defineArrayMember, defineField, defineType} from 'sanity'\n\nimport type {Availability, Interval} from '../../core/cart'\nimport {isObject} from '../../core/guards'\nimport {COMMERCE_TYPE, SUBSCRIPTION_TYPE, VARIANT_TYPE} from '../../core/ids'\nimport {moneyTitle, type ResolvedConfig} from '../config'\nimport {CommerceInput} from '../StockInput'\n\nconst AVAILABILITY: Array<{title: string; value: Availability}> = [\n {title: 'On sale', value: 'onSale'},\n {title: 'Sold out', value: 'soldOut'},\n]\n\nconst UNITS: Array<{title: string; value: Interval['unit']}> = [\n {title: 'Weeks', value: 'week'},\n {title: 'Months', value: 'month'},\n]\n\n/** Stripe charges a subscription at least once every three years. */\nconst LONGEST: Record<Interval['unit'], number> = {week: 156, month: 36}\n\n/** The subscription a product offers, as the Studio saves it. */\nexport interface StripeSubscriptionValue {\n enabled?: boolean\n /** Per delivery, in currency units. */\n price?: number\n /** With `unit`: 6 and `week` is every 6 weeks. */\n every?: number\n unit?: Interval['unit']\n}\n\n/** A variant as the Studio saves it. */\nexport interface StripeVariantValue {\n _key: string\n label: string\n availability?: Availability\n}\n\n/**\n * The price-and-availability field as the Studio saves it, on a document anyone can read. Stock\n * counts are not in it: they are kept in a private document of their own.\n */\nexport interface StripeCommerceValue {\n availability: Availability\n /** In currency units (25 means €25). */\n price?: number\n taxClass?: string\n subscription?: StripeSubscriptionValue\n variants?: StripeVariantValue[]\n}\n\n// The field's own value, as validation gets it: unknown, and maybe half filled in.\nconst isOnSale = (commerce: unknown) => isObject(commerce) && commerce.availability === 'onSale'\nconst isOffered = (subscription: unknown) => isObject(subscription) && subscription.enabled === true\n\nexport function createSubscriptionType(config: ResolvedConfig) {\n return defineType({\n name: SUBSCRIPTION_TYPE,\n title: 'Subscription',\n type: 'object',\n icon: SyncIcon,\n fields: [\n defineField({\n name: 'enabled',\n title: 'Offer a subscription',\n type: 'boolean',\n description:\n 'Buyers can choose to get this again and again, charged with each delivery, until they cancel.',\n initialValue: false,\n }),\n defineField({\n name: 'price',\n title: moneyTitle('Price per delivery', config),\n type: 'number',\n hidden: ({parent}) => !isOffered(parent),\n validation: (rule) =>\n rule\n .min(0)\n .precision(config.priceDecimals)\n .custom((price, context) =>\n isOffered(context.parent) && typeof price !== 'number'\n ? 'A subscription needs a price.'\n : true,\n ),\n }),\n defineField({\n name: 'every',\n title: 'Delivered every',\n type: 'number',\n description: 'A whole number: 6 with \"Weeks\" below is every 6 weeks.',\n hidden: ({parent}) => !isOffered(parent),\n validation: (rule) =>\n rule\n .integer()\n .min(1)\n .custom((every, context) => {\n const parent: unknown = context.parent\n if (!isOffered(parent)) return true\n if (typeof every !== 'number') return 'Say how often it is delivered.'\n const unit = isObject(parent) && parent.unit === 'month' ? 'month' : 'week'\n return every > LONGEST[unit]\n ? 'Stripe charges a subscription at least once every three years.'\n : true\n }),\n }),\n defineField({\n name: 'unit',\n title: 'Weeks or months',\n type: 'string',\n options: {list: UNITS, layout: 'radio', direction: 'horizontal'},\n initialValue: 'week',\n hidden: ({parent}) => !isOffered(parent),\n validation: (rule) =>\n rule.custom((unit, context) =>\n isOffered(context.parent) && unit !== 'week' && unit !== 'month'\n ? 'Choose weeks or months.'\n : true,\n ),\n }),\n ],\n })\n}\n\nexport function createVariantType() {\n return defineType({\n name: VARIANT_TYPE,\n title: 'Variant',\n type: 'object',\n icon: TagIcon,\n fields: [\n defineField({\n name: 'label',\n type: 'string',\n description: 'What the buyer chooses, for example \"Black, L\".',\n validation: (rule) => rule.required(),\n }),\n defineField({\n name: 'availability',\n type: 'string',\n options: {list: AVAILABILITY, layout: 'radio', direction: 'horizontal'},\n initialValue: 'onSale',\n }),\n ],\n preview: {\n select: {title: 'label', availability: 'availability'},\n prepare: ({title, availability}) => ({\n title,\n subtitle: availability === 'soldOut' ? 'Sold out' : 'On sale',\n }),\n },\n })\n}\n\nexport function createCommerceType(config: ResolvedConfig) {\n const taxClasses = config.tax.classes\n return defineType({\n name: COMMERCE_TYPE,\n title: 'Price and stock',\n type: 'object',\n icon: CreditCardIcon,\n // Adds the stock boxes, which are saved outside this document.\n components: {input: CommerceInput},\n fields: [\n defineField({\n name: 'availability',\n type: 'string',\n options: {list: AVAILABILITY, layout: 'radio', direction: 'horizontal'},\n initialValue: 'onSale',\n validation: (rule) => rule.required(),\n }),\n defineField({\n name: 'price',\n title: moneyTitle('Price', config),\n type: 'number',\n validation: (rule) =>\n rule\n .min(0)\n .precision(config.priceDecimals)\n .custom((price, context) =>\n isOnSale(context.parent) && typeof price !== 'number'\n ? 'A product on sale needs a price.'\n : true,\n ),\n }),\n ...(taxClasses.length\n ? [\n defineField({\n name: 'taxClass',\n title: config.tax.label.charAt(0).toUpperCase() + config.tax.label.slice(1),\n type: 'string',\n options: {\n list: taxClasses.map(({title, value}) => ({title, value})),\n layout: 'radio',\n },\n initialValue: taxClasses[0]?.value,\n validation: (rule) => rule.required(),\n }),\n ]\n : []),\n defineField({\n name: 'subscription',\n title: 'Subscribe and save',\n type: SUBSCRIPTION_TYPE,\n }),\n defineField({\n name: 'variants',\n type: 'array',\n description: 'Sizes or colours. Each variant can sell out on its own.',\n of: [defineArrayMember({type: VARIANT_TYPE})],\n }),\n ],\n })\n}\n","import {Button, Card, Flex, Stack, Text} from '@sanity/ui'\nimport {set, type ArrayOfObjectsInputProps} from 'sanity'\n\nimport type {ShippingRegionValue} from './schema/settings'\n\n/**\n * The shipping regions list. While it is empty it offers the regions from the plugin's options.\n * A document's starting values come from its template, and a Studio that keeps the settings to\n * one document usually removes that template, so the offer is made here instead.\n */\nexport function createRegionsInput(starting: ShippingRegionValue[]) {\n return function RegionsInput(props: ArrayOfObjectsInputProps) {\n const empty = !props.value?.length\n return (\n <Stack gap={3}>\n {empty && starting.length > 0 && !props.readOnly && (\n <Card border padding={3} radius={2}>\n <Flex align=\"center\" gap={3}>\n <Text size={1} style={{flex: 1}}>\n No shipping regions yet. Start from the {starting.length} this Studio was set up\n with, then change the rates.\n </Text>\n <Button\n mode=\"ghost\"\n text=\"Use the starting regions\"\n onClick={() => props.onChange(set(starting))}\n />\n </Flex>\n </Card>\n )}\n {props.renderDefault(props)}\n </Stack>\n )\n }\n}\n","import {CogIcon} from '@sanity/icons/Cog'\nimport {PinIcon} from '@sanity/icons/Pin'\nimport {defineArrayMember, defineField, defineType} from 'sanity'\n\nimport {asArray, isObject} from '../../core/guards'\nimport {SHIPPING_REGION_TYPE, SHOP_SETTINGS_TYPE} from '../../core/ids'\nimport {formatPrice} from '../../core/money'\nimport type {SubscriptionDelivery} from '../../core/regions'\nimport {moneyTitle, type ResolvedConfig} from '../config'\nimport {createRegionsInput} from '../RegionsInput'\n\n/** A shipping region as the Studio saves it. */\nexport interface ShippingRegionValue {\n _key: string\n _type?: typeof SHIPPING_REGION_TYPE\n title: string\n /** Every country Stripe ships to that no other region lists. */\n everywhereElse?: boolean\n countries?: string[]\n /** Flat rate per order, in currency units. */\n rate: number\n /** Goods adding up to this amount or more are delivered free. In currency units. */\n freeFrom?: number\n}\n\n/**\n * The shop settings document as the Studio saves it. Anyone can read it from a public dataset,\n * so it holds only what the cart shows: no email address, no key.\n */\nexport interface ShopSettingsValue {\n shippingRegions?: ShippingRegionValue[]\n autoSoldOut?: boolean\n subscriptionDelivery?: SubscriptionDelivery\n}\n\nconst SUBSCRIPTION_DELIVERY: Array<{title: string; value: SubscriptionDelivery}> = [\n {title: 'Free: delivery is in the subscription price', value: 'free'},\n {title: 'The region’s rate with every delivery', value: 'rate'},\n]\n\n// A region as validation and `hidden` get it: unknown, and maybe half filled in.\nconst coversEverywhereElse = (region: unknown) => isObject(region) && region.everywhereElse === true\n\n/** The regions from the plugin's options, as the shop settings start with them. */\nexport function initialRegions(config: ResolvedConfig): ShippingRegionValue[] {\n return config.shop.shippingRegions.map((region, index) => ({\n _key: `region-${index + 1}`,\n _type: SHIPPING_REGION_TYPE,\n title: region.title,\n ...(region.everywhereElse\n ? {everywhereElse: true}\n : {countries: [...(region.countries ?? [])]}),\n rate: region.rate,\n ...(typeof region.freeFrom === 'number' ? {freeFrom: region.freeFrom} : {}),\n }))\n}\n\nexport function createShippingRegionType(config: ResolvedConfig) {\n return defineType({\n name: SHIPPING_REGION_TYPE,\n title: 'Shipping region',\n type: 'object',\n icon: PinIcon,\n fields: [\n defineField({\n name: 'title',\n type: 'string',\n description: 'Shown in the cart, for example \"European Union\".',\n validation: (rule) => rule.required(),\n }),\n defineField({\n name: 'everywhereElse',\n title: 'Everywhere else',\n type: 'boolean',\n description: 'Covers every country Stripe ships to that no other region lists.',\n initialValue: false,\n }),\n defineField({\n name: 'countries',\n type: 'array',\n description:\n 'Two-letter country codes, for example NL or DE. Stripe only accepts addresses in these countries.',\n of: [\n defineArrayMember({\n type: 'string',\n validation: (rule) =>\n rule\n .regex(/^[A-Z]{2}$/, {name: 'country code'})\n .error('Use a two-letter code in capitals.'),\n }),\n ],\n options: {layout: 'tags'},\n hidden: ({parent}) => coversEverywhereElse(parent),\n validation: (rule) =>\n rule.custom((countries, context) =>\n coversEverywhereElse(context.parent) || countries?.length\n ? true\n : 'Add at least one country.',\n ),\n }),\n defineField({\n name: 'rate',\n title: moneyTitle('Shipping', config),\n type: 'number',\n description: 'One flat rate per order.',\n validation: (rule) => rule.required().min(0).precision(config.priceDecimals),\n }),\n defineField({\n name: 'freeFrom',\n title: `Free from (${config.currencyLabel})`,\n type: 'number',\n description:\n 'Optional. An order whose goods add up to this amount or more is delivered free. It goes by the goods before any promotion code.',\n validation: (rule) => rule.positive().precision(config.priceDecimals),\n }),\n ],\n preview: {\n select: {\n title: 'title',\n rate: 'rate',\n freeFrom: 'freeFrom',\n countries: 'countries',\n everywhereElse: 'everywhereElse',\n },\n prepare: ({title, rate, freeFrom, countries, everywhereElse}) => ({\n title,\n subtitle: [\n typeof rate === 'number' ? formatPrice(rate, config.currency) : 'No rate',\n ...(typeof freeFrom === 'number'\n ? [`free from ${formatPrice(freeFrom, config.currency)}`]\n : []),\n everywhereElse ? 'every other country' : countCountries(asArray(countries).length),\n ].join(' · '),\n }),\n },\n })\n}\n\n/** How many countries a region lists, for its line in the Studio. */\nexport function countCountries(count: number): string {\n return count === 1 ? '1 country' : `${count} countries`\n}\n\nexport function createShopSettingsType(config: ResolvedConfig) {\n const regions = initialRegions(config)\n return defineType({\n name: SHOP_SETTINGS_TYPE,\n title: 'Shop settings',\n type: 'document',\n icon: CogIcon,\n fields: [\n defineField({\n name: 'shippingRegions',\n title: 'Shipping regions',\n type: 'array',\n description:\n 'Buyers choose one in the cart. Its rate is added at checkout, and Stripe only accepts addresses in its countries.',\n of: [defineArrayMember({type: SHIPPING_REGION_TYPE})],\n components: {input: createRegionsInput(regions)},\n ...(regions.length ? {initialValue: regions} : {}),\n validation: (rule) =>\n rule\n .required()\n .min(1)\n .custom((value: ShippingRegionValue[] | undefined) => {\n if (!value) return true\n if (value.filter((region) => region.everywhereElse).length > 1) {\n return 'Only one region can cover everywhere else.'\n }\n const seen = new Map<string, string>()\n for (const region of value) {\n if (region.everywhereElse) continue\n for (const country of region.countries ?? []) {\n const other = seen.get(country)\n if (other) return `${country} is in both ${other} and ${region.title}.`\n seen.set(country, region.title)\n }\n }\n return true\n }),\n }),\n defineField({\n name: 'autoSoldOut',\n title: 'Sell out automatically',\n type: 'boolean',\n description: 'When the stock count of a product or variant reaches 0, mark it sold out.',\n initialValue: true,\n }),\n defineField({\n name: 'subscriptionDelivery',\n title: 'Delivery for subscriptions',\n type: 'string',\n description:\n 'Only matters when a product offers a subscription. With the region’s rate, the rate is charged with the first delivery and every one after it, unless the subscription reaches the region’s \"free from\" amount.',\n options: {list: SUBSCRIPTION_DELIVERY, layout: 'radio'},\n initialValue: 'free',\n }),\n ],\n preview: {prepare: () => ({title: 'Shop settings'})},\n })\n}\n","import {definePlugin} from 'sanity'\n\nimport {SHOP_SETTINGS_TYPE} from '../core/ids'\nimport {resolveConfig, type StripeCommerceConfig} from './config'\nimport {createCommerceType, createSubscriptionType, createVariantType} from './schema/commerce'\nimport {createShippingRegionType, createShopSettingsType} from './schema/settings'\n\n/** What an editor can do with the one shop settings document: not duplicate or delete it. */\nconst SETTINGS_ACTIONS = new Set(['publish', 'discardChanges', 'restore'])\n\n/** There is one shop settings document, made by opening it in the structure: no \"new\" for it. */\nexport function withoutSettingsTemplate<T extends {templateId: string}>(options: T[]): T[] {\n return options.filter((option) => option.templateId !== SHOP_SETTINGS_TYPE)\n}\n\n/** The shop settings keep publish, discard and restore; every other document keeps everything. */\nexport function settingsActions<T extends {action?: string}>(\n actions: T[],\n schemaType: string,\n): T[] {\n return schemaType === SHOP_SETTINGS_TYPE\n ? actions.filter(({action}) => action !== undefined && SETTINGS_ACTIONS.has(action))\n : actions\n}\n\n/**\n * Selling with Stripe Checkout from the Studio: the `stripeCommerce` field (price, tax class,\n * on sale or sold out, an optional subscription, variants, and stock counts kept out of the\n * public dataset) and the shop settings document (shipping regions, selling out at 0, delivery\n * for subscriptions).\n *\n * ```ts\n * plugins: [structureTool({structure}), stripeCommerce({currency: 'eur'})]\n * ```\n *\n * Give a product the field with `defineField({name: 'commerce', type: 'stripeCommerce'})`, and\n * put the settings in the structure with `shopSettingsListItem(S)`.\n */\nexport const stripeCommerce = definePlugin<StripeCommerceConfig>((options) => {\n const config = resolveConfig(options)\n return {\n name: 'sanity-plugin-stripe',\n schema: {\n types: [\n createCommerceType(config),\n createVariantType(),\n createSubscriptionType(config),\n createShippingRegionType(config),\n createShopSettingsType(config),\n ],\n },\n document: {\n newDocumentOptions: (previous) => withoutSettingsTemplate(previous),\n actions: (previous, context) => settingsActions(previous, context.schemaType),\n },\n }\n})\n","import {CogIcon} from '@sanity/icons/Cog'\nimport type {ListItemBuilder, StructureBuilder} from 'sanity/structure'\n\nimport {SHOP_SETTINGS_ID, SHOP_SETTINGS_TYPE} from '../core/ids'\n\n/**\n * The shop settings as one item in the Studio's structure, opening the one document:\n *\n * ```ts\n * S.list().items([S.documentTypeListItem('product'), shopSettingsListItem(S)])\n * ```\n */\nexport function shopSettingsListItem(\n S: StructureBuilder,\n options: {title?: string} = {},\n): ListItemBuilder {\n const title = options.title ?? 'Shop settings'\n return S.listItem()\n .title(title)\n .id(SHOP_SETTINGS_ID)\n .icon(CogIcon)\n .child(S.document().schemaType(SHOP_SETTINGS_TYPE).documentId(SHOP_SETTINGS_ID).title(title))\n}\n\n/** True for the shop settings type's own list item, to leave it out of `S.documentTypeListItems()`. */\nexport function isShopSettingsListItem(item: ListItemBuilder): boolean {\n return item.getId() === SHOP_SETTINGS_TYPE\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Formats one of Stripe's amounts, such as `2500` and `eur`, for people: "€25.00". */
2
+ declare function formatMoney(minor: number, currency: string, locale?: string): string;
3
+ /** Formats a price as entered in the Studio. */
4
+ declare function formatPrice(amount: number, currency: string, locale?: string): string;
5
+ export { formatPrice as n, formatMoney as t };
package/dist/react.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * A server component takes the plain helpers (queries, regions, prices) from
4
4
  * `sanity-plugin-stripe/server`, since everything here is client code to a React server.
5
5
  */
6
- import { a as CartVariant, c as Unavailable, d as lineKey, i as CartRow, l as cartRows, n as CartLine, o as CheckoutProblem, r as CartProduct, s as LineStatus, t as Availability, u as cartSubtotal } from "./cart-D2KxuwQz.js";
7
- import { o as formatMoney, s as formatPrice, t as RegionOption } from "./regions-DbCIM0I1.js";
6
+ import { C as readSubscription, _ as Unavailable, b as describeInterval, c as shippingQuote, d as CartProduct, f as CartRow, g as LineStatus, h as Interval, l as Availability, m as CheckoutProblem, n as ShippingQuote, p as CartVariant, t as RegionOption, u as CartLine, v as cartRows, x as lineKey, y as cartSubtotal } from "./regions-Bfhg4QDs.js";
7
+ import { n as formatPrice, t as formatMoney } from "./money-XMQaHsbV.js";
8
8
  import { ReactNode } from "react";
9
9
  interface CartProviderProps {
10
10
  children: ReactNode;
@@ -15,7 +15,7 @@ interface CartProviderProps {
15
15
  }
16
16
  /** Holds the cart for everything inside it. Put it in the site's layout. */
17
17
  export declare function CartProvider({ children, storageKey, maxQuantity }: CartProviderProps): import("react").JSX.Element;
18
- type LineRef = Pick<CartLine, 'productId' | 'variantKey'>;
18
+ type LineRef = Pick<CartLine, 'productId' | 'variantKey' | 'plan'>;
19
19
  interface Cart {
20
20
  lines: CartLine[];
21
21
  /** Number of items, counting quantities. */
@@ -24,7 +24,10 @@ interface Cart {
24
24
  ready: boolean;
25
25
  /** Most of one item a buyer can order at once. */
26
26
  maxQuantity: number;
27
- /** Adds to the line for this product and variant. `limit` caps the total, for example at the stock count. */
27
+ /**
28
+ * Adds to the line for this product, variant and way of buying (once, or `plan: 'subscribe'`).
29
+ * `limit` caps the total, for example at the stock count.
30
+ */
28
31
  add: (line: CartLine, limit?: number) => void;
29
32
  setQuantity: (line: LineRef, quantity: number) => void;
30
33
  remove: (line: LineRef) => void;
@@ -49,21 +52,29 @@ interface BuyBox {
49
52
  /** The chosen variant: the first one on sale, until the buyer picks another. */
50
53
  variantKey: string | undefined;
51
54
  chooseVariant: (variantKey: string) => void;
55
+ /** The subscription the product offers: its price per delivery and how often. Null without one. */
56
+ subscription: {
57
+ price: number;
58
+ interval: Interval;
59
+ } | null;
60
+ /** Whether the buyer buys once or subscribes. `once` until they choose. */
61
+ plan: 'once' | 'subscribe';
62
+ choosePlan: (plan: 'once' | 'subscribe') => void;
52
63
  /** Nothing of this product can be bought. */
53
64
  soldOut: boolean;
54
- /** Whether the chosen product and variant can be bought, its price and how many. */
65
+ /** Whether the chosen product, variant and plan can be bought, its price and how many. */
55
66
  status: LineStatus;
56
- /** How many of the chosen product and variant the cart holds. */
67
+ /** How many of the chosen product, variant and plan the cart holds. */
57
68
  inCart: number;
58
69
  /** The cart already holds as many as can be ordered. */
59
70
  full: boolean;
60
- /** True after `add`, until the buyer picks another variant. */
71
+ /** True after `add`, until the buyer picks another variant or plan. */
61
72
  added: boolean;
62
73
  add: (quantity?: number) => void;
63
74
  }
64
75
  /**
65
- * What a product page's buy button needs: the variants, which one is chosen, whether it can be
66
- * bought, and adding it to the cart. The markup is yours.
76
+ * What a product page's buy button needs: the variants, which one is chosen, buying once or
77
+ * subscribing, whether it can be bought, and adding it to the cart. The markup is yours.
67
78
  */
68
79
  export declare function useBuyBox(product: CartProduct): BuyBox;
69
80
  type CheckoutState = {
@@ -104,4 +115,4 @@ interface Checkout {
104
115
  }
105
116
  /** Starts Stripe Checkout for the cart, and says what went wrong when it can't. */
106
117
  export declare function useCheckout(options?: UseCheckoutOptions): Checkout;
107
- export { type Availability, type BuyBox, type Cart, type CartLine, type CartProduct, type CartProviderProps, type CartRow, type CartVariant, type Checkout, type CheckoutProblem, type CheckoutState, type ClearCartOnReturnProps, type LineStatus, type RegionOption, type Unavailable, type UseCheckoutOptions, cartRows, cartSubtotal, formatMoney, formatPrice, lineKey };
118
+ export { type Availability, type BuyBox, type Cart, type CartLine, type CartProduct, type CartProviderProps, type CartRow, type CartVariant, type Checkout, type CheckoutProblem, type CheckoutState, type ClearCartOnReturnProps, type Interval, type LineStatus, type RegionOption, type ShippingQuote, type Unavailable, type UseCheckoutOptions, cartRows, cartSubtotal, describeInterval, formatMoney, formatPrice, lineKey, readSubscription, shippingQuote };
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { a as lineStatus, c as sanitizeLines, n as cartSubtotal, r as lineKey, s as readCheckoutProblems, t as cartRows } from "./cart-CSjxtjq-.js";
3
- import { a as formatMoney, i as isObject, o as formatPrice, r as asString } from "./guards-CAcETsf7.js";
2
+ import { a as shippingQuote, c as describeInterval, d as lineStatus, h as sanitizeLines, l as lineKey, m as readSubscription, o as cartRows, p as readCheckoutProblems, s as cartSubtotal } from "./regions-CFDYMokP.js";
3
+ import { a as asString, c as formatPrice, o as isObject, s as formatMoney } from "./countries-Csjmv4lY.js";
4
4
  import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  /**
@@ -105,13 +105,14 @@ function ClearCartOnReturn({ param = "session_id" }) {
105
105
  ]), null;
106
106
  }
107
107
  /**
108
- * What a product page's buy button needs: the variants, which one is chosen, whether it can be
109
- * bought, and adding it to the cart. The markup is yours.
108
+ * What a product page's buy button needs: the variants, which one is chosen, buying once or
109
+ * subscribing, whether it can be bought, and adding it to the cart. The markup is yours.
110
110
  */
111
111
  function useBuyBox(product) {
112
- let cart = useCart(), max = cart.maxQuantity, variants = (product.commerce?.variants ?? []).map((variant) => Object.assign({}, variant, { available: lineStatus(product, variant._key, max).available })), firstAvailable = variants.find((variant) => variant.available), [chosen, setChosen] = useState(), [added, setAdded] = useState(!1), variantKey = chosen ?? firstAvailable?._key, status = lineStatus(product, variantKey, max), soldOut = variants.length ? !firstAvailable : !status.available, line = {
112
+ let cart = useCart(), max = cart.maxQuantity, variants = (product.commerce?.variants ?? []).map((variant) => Object.assign({}, variant, { available: lineStatus(product, variant._key, max).available })), firstAvailable = variants.find((variant) => variant.available), [chosen, setChosen] = useState(), [wanted, setWanted] = useState("once"), [added, setAdded] = useState(!1), subscription = readSubscription(product.commerce?.subscription), plan = subscription ? wanted : "once", subscribe = plan === "subscribe" ? { plan } : {}, variantKey = chosen ?? firstAvailable?._key, status = lineStatus(product, variantKey, max, subscribe.plan), soldOut = variants.length ? !firstAvailable : !lineStatus(product, variantKey, max).available, line = {
113
113
  productId: product._id,
114
- ...variantKey ? { variantKey } : {}
114
+ ...variantKey ? { variantKey } : {},
115
+ ...subscribe
115
116
  }, inCart = cart.lines.find((candidate) => lineKey(candidate) === lineKey(line))?.quantity ?? 0;
116
117
  return {
117
118
  variants,
@@ -119,6 +120,11 @@ function useBuyBox(product) {
119
120
  chooseVariant(key) {
120
121
  setChosen(key), setAdded(!1);
121
122
  },
123
+ subscription,
124
+ plan,
125
+ choosePlan(next) {
126
+ setWanted(next), setAdded(!1);
127
+ },
122
128
  soldOut,
123
129
  status,
124
130
  inCart,
@@ -194,6 +200,6 @@ function useCheckout(options = {}) {
194
200
  reset: useCallback(() => setState({ status: "idle" }), [])
195
201
  };
196
202
  }
197
- export { CartProvider, ClearCartOnReturn, cartRows, cartSubtotal, formatMoney, formatPrice, lineKey, useBuyBox, useCart, useCheckout };
203
+ export { CartProvider, ClearCartOnReturn, cartRows, cartSubtotal, describeInterval, formatMoney, formatPrice, lineKey, readSubscription, shippingQuote, useBuyBox, useCart, useCheckout };
198
204
 
199
205
  //# sourceMappingURL=react.js.map