@redeemid/emdash 0.0.16 → 0.0.18

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.
Files changed (73) hide show
  1. package/README.md +447 -232
  2. package/bin/redeemid-emdash.mjs +13 -1
  3. package/docs/developer-api.md +126 -0
  4. package/docs/shopify-setup.md +1 -1
  5. package/package.json +3 -1
  6. package/src/access-admin.ts +754 -0
  7. package/src/access.ts +84 -18
  8. package/src/admin-api.ts +9 -2
  9. package/src/agents.ts +62 -3
  10. package/src/api-version.ts +6 -0
  11. package/src/blocks/CollectionBlock.astro +4 -1
  12. package/src/blocks/SearchBlock.astro +14 -0
  13. package/src/blocks/index.ts +2 -0
  14. package/src/blocks-names.ts +1 -0
  15. package/src/cart-ops.ts +164 -0
  16. package/src/catalog.ts +287 -0
  17. package/src/checkout.ts +73 -0
  18. package/src/client/redeemid.ts +563 -0
  19. package/src/components/AccessBadge.astro +7 -5
  20. package/src/components/Account.astro +1 -1
  21. package/src/components/BrowseControls.astro +87 -0
  22. package/src/components/Cart.astro +12 -6
  23. package/src/components/CartDrawer.astro +59 -0
  24. package/src/components/CartView.astro +2 -1
  25. package/src/components/Collection.astro +23 -18
  26. package/src/components/Content.astro +10 -5
  27. package/src/components/Contribution.astro +1 -1
  28. package/src/components/Gate.astro +4 -2
  29. package/src/components/LockedPanel.astro +5 -3
  30. package/src/components/MemberCard.astro +1 -1
  31. package/src/components/Notices.astro +3 -28
  32. package/src/components/Plan.astro +1 -1
  33. package/src/components/Preferences.astro +1 -1
  34. package/src/components/Product.astro +116 -25
  35. package/src/components/ProductGrid.astro +52 -0
  36. package/src/components/ProductSearch.astro +83 -0
  37. package/src/components/Recommendations.astro +45 -0
  38. package/src/components/RedeemIDClient.astro +45 -0
  39. package/src/components/YourData.astro +1 -1
  40. package/src/config.ts +31 -5
  41. package/src/contract.ts +50 -0
  42. package/src/descriptor.ts +7 -0
  43. package/src/events.ts +3 -0
  44. package/src/features.ts +1 -1
  45. package/src/files.ts +1 -1
  46. package/src/guard.ts +5 -3
  47. package/src/index.ts +3 -0
  48. package/src/log.ts +96 -0
  49. package/src/monitor.ts +560 -0
  50. package/src/notices.ts +29 -0
  51. package/src/plugin.ts +234 -88
  52. package/src/rate-limit.ts +7 -0
  53. package/src/routes/api/cart.ts +84 -0
  54. package/src/routes/api/search.ts +49 -0
  55. package/src/routes/api/shared.ts +4 -0
  56. package/src/routes/api/state.ts +69 -0
  57. package/src/routes/buy.ts +4 -3
  58. package/src/routes/callback.ts +23 -5
  59. package/src/routes/cart-edit.ts +15 -66
  60. package/src/routes/cart.ts +4 -2
  61. package/src/routes/preferences.ts +2 -2
  62. package/src/routes/privacy.ts +2 -2
  63. package/src/routes/shared.ts +11 -0
  64. package/src/routes/webhook.ts +24 -11
  65. package/src/settings.ts +195 -0
  66. package/src/shopify.ts +37 -1
  67. package/src/site-config.ts +38 -5
  68. package/src/staff.ts +12 -23
  69. package/src/storage.ts +12 -0
  70. package/src/storefront.ts +231 -59
  71. package/src/text.ts +43 -2
  72. package/src/version.ts +1 -1
  73. package/src/webhooks.ts +2 -1
@@ -0,0 +1,87 @@
1
+ ---
2
+ /**
3
+ * Sort, filter and paging controls for a page of products (<Collection
4
+ * browse> and <ProductSearch>). A plain GET form: the choice lives in the
5
+ * page URL, so it can be shared, bookmarked and cached, and works without
6
+ * JavaScript. Filters are the ones Shopify offers for these products.
7
+ */
8
+ import { browseHref, SEARCH_SORTS, SORTS, type Filter, type PageInfo, type Sort } from "../catalog.js";
9
+ import { siteConfig } from "../site-config.js";
10
+ import { text } from "../text.js";
11
+
12
+ interface Props {
13
+ filters: Filter[];
14
+ pageInfo: PageInfo;
15
+ /** Search pages offer fewer sort orders. */
16
+ search?: boolean;
17
+ /** The search words, kept in the form. */
18
+ query?: string;
19
+ id: string;
20
+ }
21
+
22
+ const { filters, pageInfo, search = false, query, id } = Astro.props;
23
+ const config = await siteConfig(Astro.locals);
24
+ const url = Astro.url;
25
+ const active = new Set(url.searchParams.getAll("filter"));
26
+ const sort = url.searchParams.get("sort") ?? "";
27
+ const labels: Record<Sort, string> = {
28
+ relevance: text(config, "Relevance"),
29
+ "best-selling": text(config, "Best selling"),
30
+ newest: text(config, "Newest"),
31
+ "price-asc": text(config, "Price, low to high"),
32
+ "price-desc": text(config, "Price, high to low"),
33
+ "title-asc": text(config, "Title, A to Z"),
34
+ "title-desc": text(config, "Title, Z to A"),
35
+ };
36
+ const sorts = search ? SEARCH_SORTS : SORTS.filter((s) => s !== "relevance");
37
+ const lists = filters.filter((f) => f.type !== "PRICE_RANGE" && f.values.length > 0);
38
+ const price = filters.find((f) => f.type === "PRICE_RANGE");
39
+ const filtered = active.size > 0 || url.searchParams.has("price_min") || url.searchParams.has("price_max");
40
+ const clearHref = browseHref(url, { filter: null, price_min: null, price_max: null });
41
+ ---
42
+
43
+ <form method="get" action={url.pathname} class="redeemid-browse" aria-labelledby={`${id}-heading`}>
44
+ <h2 id={`${id}-heading`} style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap">{text(config, "Sort and filter")}</h2>
45
+ {query !== undefined && <input type="hidden" name="q" value={query} />}
46
+ <p class="redeemid-browse__sort">
47
+ <label for={`${id}-sort`}>{text(config, "Sort by")}</label>
48
+ <select id={`${id}-sort`} name="sort">
49
+ <option value="">{text(config, search ? "Relevance" : "Featured")}</option>
50
+ {sorts.filter((s) => !(search && s === "relevance")).map((s) => (
51
+ <option value={s} selected={sort === s}>{labels[s]}</option>
52
+ ))}
53
+ </select>
54
+ </p>
55
+ {lists.map((f, i) => (
56
+ <fieldset class="redeemid-browse__filter">
57
+ <legend>{f.label}</legend>
58
+ {f.values.map((v, j) => (
59
+ <p>
60
+ <input type="checkbox" id={`${id}-f${i}-${j}`} name="filter" value={v.input} checked={active.has(v.input)} />
61
+ <label for={`${id}-f${i}-${j}`}>{text(config, "{label} ({count})", { label: v.label, count: v.count })}</label>
62
+ </p>
63
+ ))}
64
+ </fieldset>
65
+ ))}
66
+ {price && (
67
+ <fieldset class="redeemid-browse__filter">
68
+ <legend>{price.label || text(config, "Price")}</legend>
69
+ <p>
70
+ <label for={`${id}-min`}>{text(config, "From")}</label>
71
+ <input id={`${id}-min`} name="price_min" type="number" min="0" step="any" inputmode="decimal" value={url.searchParams.get("price_min") ?? ""} />
72
+ <label for={`${id}-max`}>{text(config, "To")}</label>
73
+ <input id={`${id}-max`} name="price_max" type="number" min="0" step="any" inputmode="decimal" value={url.searchParams.get("price_max") ?? ""} />
74
+ </p>
75
+ </fieldset>
76
+ )}
77
+ <p class="redeemid-browse__actions">
78
+ <button type="submit">{text(config, "Apply")}</button>
79
+ {filtered && <a href={clearHref}>{text(config, "Clear filters")}</a>}
80
+ </p>
81
+ </form>
82
+ {(pageInfo.hasPreviousPage || pageInfo.hasNextPage) && (
83
+ <nav class="redeemid-browse__pages" aria-label={text(config, "Pages")}>
84
+ {pageInfo.hasPreviousPage && pageInfo.startCursor && <a rel="prev" href={browseHref(url, { before: pageInfo.startCursor, after: null })}>{text(config, "Previous page")}</a>}
85
+ {pageInfo.hasNextPage && pageInfo.endCursor && <a rel="next" href={browseHref(url, { after: pageInfo.endCursor, before: null })}>{text(config, "Next page")}</a>}
86
+ </nav>
87
+ )}
@@ -1,7 +1,9 @@
1
1
  ---
2
2
  /**
3
- * Cart link with the item count from the visitor's Shopify cart. It goes
4
- * to that cart's Shopify checkout.
3
+ * Cart link with the item count from the visitor's Shopify cart. It goes to
4
+ * that cart's Shopify checkout, or to `href` (a cart page with <CartView>).
5
+ * With `drawer`, and <CartDrawer /> and <RedeemIDClient /> on the page, it
6
+ * opens the drawer instead; the link still works without JavaScript.
5
7
  */
6
8
  import { cartUrl } from "../access.js";
7
9
  import { readCart } from "../cart.js";
@@ -10,15 +12,19 @@ import { text } from "../text.js";
10
12
 
11
13
  interface Props {
12
14
  label?: string;
15
+ /** A cart page to link to instead of checkout. */
16
+ href?: string;
17
+ /** Open the cart drawer. */
18
+ drawer?: boolean;
13
19
  }
14
20
 
15
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
16
- const { label = text(config, "Cart") } = Astro.props;
21
+ const config = await siteConfig(Astro.locals);
22
+ const { label = text(config, "Cart"), href, drawer = false } = Astro.props;
17
23
  const ref = await readCart(Astro.session);
18
24
  const count = ref?.count ?? 0;
19
25
  const here = Astro.url.pathname + Astro.url.search;
20
26
  ---
21
27
 
22
- <a class="redeemid-cart" href={cartUrl(here)} data-redeemid-cart-count={count}>
23
- {label}{count > 0 && <span class="redeemid-cart__count"> ({count})</span>}
28
+ <a class="redeemid-cart" href={href ?? cartUrl(here)} data-redeemid-cart-count={count} data-redeemid-cart-link={drawer ? "" : undefined}>
29
+ {label}<span class="redeemid-cart__count" hidden={count === 0}>{count > 0 ? ` (${count})` : ""}</span>
24
30
  </a>
@@ -0,0 +1,59 @@
1
+ ---
2
+ /**
3
+ * A cart that slides in over the page. Put it once in the site layout,
4
+ * with <RedeemIDClient />. Add to cart then opens it in place, and
5
+ * <Cart drawer /> opens it instead of going to checkout. It is a native
6
+ * modal dialog: focus stays inside while it's open, Escape closes it, and
7
+ * focus goes back to what opened it.
8
+ *
9
+ * Without JavaScript it never shows, and the cart forms and links work as
10
+ * they do on their own.
11
+ */
12
+ import { siteConfig } from "../site-config.js";
13
+ import { text } from "../text.js";
14
+
15
+ interface Props {
16
+ title?: string;
17
+ }
18
+
19
+ const config = await siteConfig(Astro.locals);
20
+ const { title = text(config, "Your cart") } = Astro.props;
21
+ ---
22
+
23
+ <dialog class="redeemid-drawer" data-redeemid-cart-drawer aria-labelledby="redeemid-drawer-title">
24
+ <div class="redeemid-drawer__head">
25
+ <h2 id="redeemid-drawer-title">{title}</h2>
26
+ <button type="button" class="redeemid-drawer__close" data-redeemid-drawer-close>{text(config, "Close")}</button>
27
+ </div>
28
+ <p class="redeemid-drawer__status" role="status" data-redeemid-drawer-status></p>
29
+ <div class="redeemid-cartview redeemid-drawer__body" data-redeemid-drawer-body></div>
30
+ </dialog>
31
+
32
+ <style is:global>
33
+ /* Low-specificity defaults; a site's own styles win. */
34
+ :where(dialog.redeemid-drawer) {
35
+ box-sizing: border-box;
36
+ inset: 0 0 0 auto;
37
+ margin: 0;
38
+ height: 100dvh;
39
+ max-height: 100dvh;
40
+ width: min(28rem, 100vw);
41
+ max-width: 100vw;
42
+ padding: 1rem;
43
+ border: 0;
44
+ overflow-y: auto;
45
+ }
46
+ :where(dialog.redeemid-drawer)::backdrop {
47
+ background: rgb(0 0 0 / 0.4);
48
+ }
49
+ :where(.redeemid-drawer img) {
50
+ max-width: 5rem;
51
+ height: auto;
52
+ }
53
+ :where(.redeemid-drawer__head) {
54
+ display: flex;
55
+ justify-content: space-between;
56
+ align-items: center;
57
+ gap: 1rem;
58
+ }
59
+ </style>
@@ -20,7 +20,7 @@ interface Props {
20
20
  empty?: string;
21
21
  }
22
22
 
23
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
23
+ const config = await siteConfig(Astro.locals);
24
24
  const { productHref, empty = text(config, "Your cart is empty.") } = Astro.props;
25
25
  const ref = await readCart(Astro.session);
26
26
  let cart: CartDetails | null = null;
@@ -57,6 +57,7 @@ const code = cart?.discountCodes[0];
57
57
  <p class="redeemid-cartview__name">
58
58
  {href(l.productHandle) ? <a href={href(l.productHandle)}>{l.productTitle}</a> : l.productTitle}
59
59
  {l.variantTitle && l.variantTitle !== "Default Title" && <span> · {l.variantTitle}</span>}
60
+ {l.sellingPlan && <span> · {l.sellingPlan}</span>}
60
61
  </p>
61
62
  <p class="redeemid-cartview__price">{text(config, "{price} each · {total}", { price: money(l.price), total: money(l.total) })}</p>
62
63
  <form method="post" action={action} class="redeemid-cartview__quantity">
@@ -5,35 +5,47 @@
5
5
  * site can give every product its own page with <Product>.
6
6
  *
7
7
  * <Collection handle="shop" productHref="/shop/{handle}/" />
8
+ * <Collection handle="shop" productHref="/shop/{handle}/" browse />
9
+ *
10
+ * `browse` adds Shopify's filters, sort orders and paging, read from the
11
+ * page URL (so the page must be rendered on demand). `quickAdd` adds Add to
12
+ * cart on cards that need no choice.
8
13
  */
14
+ import { browseFromUrl, collectionPage, type CollectionPage } from "../catalog.js";
9
15
  import { siteConfig } from "../site-config.js";
10
- import { collection as loadCollection, money, type Collection } from "../storefront.js";
11
- import { text } from "../text.js";
16
+ import { collection as loadCollection, storeContext, type Collection } from "../storefront.js";
17
+ import BrowseControls from "./BrowseControls.astro";
18
+ import ProductGrid from "./ProductGrid.astro";
12
19
 
13
20
  interface Props {
14
21
  handle: string;
15
- /** How many products, 1 to 50. */
22
+ /** How many products (per page with `browse`), 1 to 50. */
16
23
  limit?: number;
17
24
  /** Link for each product; `{handle}` is replaced. Leave out for cards without links. */
18
25
  productHref?: string;
19
26
  /** Show the collection's title and description above the grid. */
20
27
  showTitle?: boolean;
28
+ /** Filters, sorting and paging from the page URL. */
29
+ browse?: boolean;
30
+ quickAdd?: boolean;
21
31
  }
22
32
 
23
- const { handle, limit = 12, productHref, showTitle = false } = Astro.props;
24
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
25
- let c: Collection | null = null;
33
+ const { handle, limit = 12, productHref, showTitle = false, browse = false, quickAdd = false } = Astro.props;
34
+ const config = await siteConfig(Astro.locals);
35
+ const context = storeContext(config, Astro.request);
36
+ let c: Collection | CollectionPage | null = null;
26
37
  if (config) {
27
38
  try {
28
- c = await loadCollection(config, handle, limit);
39
+ c = browse ? await collectionPage(config, handle, browseFromUrl(Astro.url.searchParams, limit), { context }) : await loadCollection(config, handle, limit, undefined, context);
29
40
  } catch {
30
41
  c = null;
31
42
  }
32
43
  }
33
- const href = (h: string) => productHref?.replaceAll("{handle}", encodeURIComponent(h));
44
+ const page = c && "pageInfo" in c ? c : null;
45
+ const id = `redeemid-collection-${handle}`;
34
46
  ---
35
47
 
36
- {c && c.products.length > 0 ? (
48
+ {c && (c.products.length > 0 || page) ? (
37
49
  <section class="redeemid-collection" data-redeemid-collection={c.handle}>
38
50
  {showTitle && (
39
51
  <>
@@ -41,15 +53,8 @@ const href = (h: string) => productHref?.replaceAll("{handle}", encodeURICompone
41
53
  {c.description && <p class="redeemid-collection__description">{c.description}</p>}
42
54
  </>
43
55
  )}
44
- <ul class="redeemid-collection__grid">
45
- {c.products.map((p) => (
46
- <li class="redeemid-collection__item" data-redeemid-product={p.handle}>
47
- {p.image && <img src={p.image.url} alt={p.image.altText ?? ""} loading="lazy" />}
48
- <h3>{href(p.handle) ? <a href={href(p.handle)}>{p.title}</a> : p.title}</h3>
49
- <p class="redeemid-collection__price">{p.availableForSale ? (p.price ? text(config, "From {price}", { price: money(p.price) }) : "") : text(config, "Sold out")}</p>
50
- </li>
51
- ))}
52
- </ul>
56
+ {page && <BrowseControls filters={page.filters} pageInfo={page.pageInfo} id={id} />}
57
+ <ProductGrid products={c.products} productHref={productHref} quickAdd={quickAdd} />
53
58
  </section>
54
59
  ) : (
55
60
  <slot />
@@ -20,6 +20,10 @@
20
20
  *
21
21
  * Readers kept out get the wall (LockedPanel), with the "locked" slot on top.
22
22
  *
23
+ * `id` (the entry's id or slug) also applies the entry's own rule from the
24
+ * editor's Access panel; pass it in lists too when entries have their own
25
+ * rules.
26
+ *
23
27
  * Pass `id` on the entry's own page so drip and the meter apply: a rule's
24
28
  * `drip` keeps it shut until the member has held their level that many days,
25
29
  * and config `meter` lets signed-in readers open a few locked entries each
@@ -50,8 +54,8 @@ interface Props {
50
54
 
51
55
  const { value, collection = "", terms, id, title } = Astro.props;
52
56
  const split = splitAtPaywall(value);
53
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
54
- const entryRule = await entryRequirement(config, collection, terms);
57
+ const config = await siteConfig(Astro.locals);
58
+ const entryRule = await entryRequirement(config, collection, terms, id);
55
59
  // Bought on its own: the whole entry, no meter, no drip.
56
60
  const item = soldItem(value);
57
61
  const reader = item ? (await viewer(Astro)).member : null;
@@ -78,7 +82,7 @@ async function parts(blocks: unknown[]) {
78
82
  continue;
79
83
  }
80
84
  const needs = section.levels.length ? [section.levels] : (entryRule ?? [[]]);
81
- out.push({ blocks: section.blocks, verdict: await canRead(Astro, { collection, terms }, { required: needs }) });
85
+ out.push({ blocks: section.blocks, verdict: await canRead(Astro, { collection, terms, id }, { required: needs }) });
82
86
  }
83
87
  return out;
84
88
  }
@@ -118,7 +122,7 @@ const meterText =
118
122
  !p.verdict || p.verdict.allowed ? (
119
123
  p.blocks.length > 0 && <PortableText value={p.blocks} />
120
124
  ) : (
121
- <LockedPanel reason={p.verdict.reason} required={p.verdict.required} held={p.verdict.viewer.levels} item={item} />
125
+ <LockedPanel reason={p.verdict.reason} required={p.verdict.required} held={p.verdict.viewer.levels} item={item} entry={id} />
122
126
  ),
123
127
  )}
124
128
  {verdict.allowed ? (
@@ -126,7 +130,7 @@ const meterText =
126
130
  !p.verdict || p.verdict.allowed ? (
127
131
  p.blocks.length > 0 && <PortableText value={p.blocks} />
128
132
  ) : (
129
- <LockedPanel reason={p.verdict.reason} required={p.verdict.required} held={p.verdict.viewer.levels} item={item} />
133
+ <LockedPanel reason={p.verdict.reason} required={p.verdict.required} held={p.verdict.viewer.levels} item={item} entry={id} />
130
134
  ),
131
135
  )
132
136
  ) : (
@@ -138,6 +142,7 @@ const meterText =
138
142
  meterLeft={verdict.meterLeft}
139
143
  meterPeriod={verdict.meter?.period}
140
144
  item={item}
145
+ entry={id}
141
146
  >
142
147
  <slot name="locked" />
143
148
  </LockedPanel>
@@ -21,7 +21,7 @@ interface Props {
21
21
  title?: string;
22
22
  }
23
23
 
24
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
24
+ const config = await siteConfig(Astro.locals);
25
25
  const { handle, amounts = [5, 10, 25], initial = 10, label = text(config, "Give"), title } = Astro.props;
26
26
  let p: Product | null = null;
27
27
  if (config) {
@@ -17,10 +17,12 @@ interface Props {
17
17
  collection?: string;
18
18
  terms?: Terms;
19
19
  levels?: string[];
20
+ /** The entry's id or slug, so its own rule from the editor counts. */
21
+ id?: string;
20
22
  }
21
23
 
22
- const { collection = "", terms, levels } = Astro.props;
23
- const verdict = await canRead(Astro, { collection, terms }, levels ? { levels } : undefined);
24
+ const { collection = "", terms, levels, id } = Astro.props;
25
+ const verdict = await canRead(Astro, { collection, terms, id }, levels ? { levels } : undefined);
24
26
  ---
25
27
 
26
28
  {verdict.allowed ? (
@@ -27,13 +27,15 @@ interface Props {
27
27
  meterPeriod?: "month" | "week";
28
28
  /** The entry's "Sold on its own" block: offers "Buy just this". */
29
29
  item?: SoldItem | null;
30
+ /** The entry's id or slug, for the browser client's wall_shown event. */
31
+ entry?: string;
30
32
  }
31
33
 
32
- const { reason, required = [], held = [], opensAt, meterLeft, meterPeriod, item } = Astro.props;
34
+ const { reason, required = [], held = [], opensAt, meterLeft, meterPeriod, item, entry } = Astro.props;
33
35
  const opens = opensAt
34
36
  ? new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeZone: "UTC" }).format(opensAt)
35
37
  : null;
36
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
38
+ const config = await siteConfig(Astro.locals);
37
39
  const missing = required.find((clause) => clause.length > 0 && !passes(held, [clause]));
38
40
  const level = missing ? config?.levels?.find((l) => l.slug === missing[0]) : undefined;
39
41
  const joinHref = level?.joinUrl ?? config?.join?.href;
@@ -54,7 +56,7 @@ if (item?.product && config && (reason === "sign_in" || reason === "upgrade" ||
54
56
  const here = Astro.url.pathname + Astro.url.search;
55
57
  ---
56
58
 
57
- <div class="redeemid-locked" data-redeemid-reason={reason} data-redeemid-level={level?.slug}>
59
+ <div class="redeemid-locked" data-redeemid-reason={reason} data-redeemid-level={level?.slug} data-redeemid-entry={entry}>
58
60
  <slot />
59
61
  {level?.wall && (reason === "sign_in" || reason === "upgrade") && <p class="redeemid-locked__wall">{level.wall}</p>}
60
62
  {reason === "sign_in" && (
@@ -19,7 +19,7 @@ interface Props {
19
19
  title?: string;
20
20
  }
21
21
 
22
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
22
+ const config = await siteConfig(Astro.locals);
23
23
  const { title = text(config, "Your membership") } = Astro.props;
24
24
  const v = await viewer(Astro);
25
25
  const here = Astro.url.pathname + Astro.url.search;
@@ -5,43 +5,18 @@
5
5
  * `messages` to reword any of them (config `text` does too, for every page).
6
6
  */
7
7
  import { siteConfig } from "../site-config.js";
8
+ import { NOTICES as DEFAULTS } from "../notices.js";
8
9
  import { text as replace } from "../text.js";
9
10
 
10
11
  interface Props {
11
12
  messages?: Record<string, string>;
12
13
  }
13
14
 
14
- const DEFAULTS: Record<string, string> = {
15
- not_configured: "Sign-in isn't set up on this site yet.",
16
- sign_in_unavailable: "Sign-in isn't available right now. Try again in a few minutes.",
17
- sign_in_expired: "That sign-in took too long. Try again.",
18
- staff_account: "This account can't sign in here. Use a different email.",
19
- account_disabled: "Your account is turned off. Contact the store for help.",
20
- too_many_requests: "Too many tries. Wait a minute, then try again.",
21
- site_not_ready: "This site isn't ready for sign-in yet.",
22
- email_required: "Your account needs an email address to sign in.",
23
- token_failed: "Sign-in didn't finish. Try again.",
24
- bad_id_token: "Sign-in didn't finish. Try again.",
25
- identity_failed: "Sign-in didn't finish. Try again.",
26
- plan_unavailable: "That plan isn't available right now.",
27
- checkout_failed: "Checkout didn't open. Try again.",
28
- cart_empty: "Your cart is empty.",
29
- cart_added: "Added to your cart.",
30
- cart_updated: "Your cart is updated.",
31
- discount_applied: "Discount applied.",
32
- discount_not_applicable: "That discount code doesn't apply to this cart.",
33
- refreshed: "Your membership is up to date.",
34
- preferences_saved: "Your preferences are saved.",
35
- preferences_refused: "Some answers weren't saved. Check them and try again.",
36
- preferences_unavailable: "Your preferences can't be saved right now. Try again in a few minutes.",
37
- data_erased: "Your data on this site is deleted, and you're signed out.",
38
- erase_unconfirmed: "Tick the box to confirm, then try again.",
39
- account_unavailable: "Your account page isn't available right now. Try again in a few minutes.",
40
- };
15
+
41
16
 
42
17
  const { messages = {} } = Astro.props;
43
18
  const code = Astro.url.searchParams.get("redeemid_notice") ?? "";
44
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
19
+ const config = await siteConfig(Astro.locals);
45
20
  const fallback = DEFAULTS[code];
46
21
  const text = code ? (messages[code] ?? (fallback ? replace(config, fallback) : undefined)) : undefined;
47
22
  ---
@@ -25,7 +25,7 @@ interface Props {
25
25
  heldLabel?: string;
26
26
  }
27
27
 
28
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
28
+ const config = await siteConfig(Astro.locals);
29
29
  const { handle, label = text(config, "Join"), title, levels = [], heldLabel = text(config, "You're on this plan.") } = Astro.props;
30
30
  let p: Plan | null = null;
31
31
  if (config) {
@@ -20,7 +20,7 @@ interface Props {
20
20
  label?: string;
21
21
  }
22
22
 
23
- const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
23
+ const config = await siteConfig(Astro.locals);
24
24
  const { title = text(config, "Your preferences"), heading = "h2", label = text(config, "Save preferences") } = Astro.props;
25
25
  const v = await viewer(Astro);
26
26
  const m = v.member;