@redeemid/emdash 0.0.23 → 0.0.24

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/README.md CHANGED
@@ -7,7 +7,7 @@ Memberships and paywalls for [EmDash](https://github.com/emdash-cms/emdash) site
7
7
 
8
8
  Readers sign in with the store's own customer account. Their Shopify customer tags decide what they can read. Shopify keeps the store, checkout, orders and subscriptions, and EmDash keeps the content. Nothing on the site grants access by itself, so there is one source of truth for who paid for what.
9
9
 
10
- **Status: 0.0.23, early.** Runs on Node and Cloudflare Workers. Unit tests and an end-to-end test that boots a real EmDash site against a fake Shopify store cover the features below. Expect changes before 1.0.
10
+ **Status: 0.0.24, early.** Runs on Node and Cloudflare Workers. Unit tests and an end-to-end test that boots a real EmDash site against a fake Shopify store cover the features below. Expect changes before 1.0.
11
11
 
12
12
  ## Contents
13
13
 
@@ -161,7 +161,7 @@ Use `<Content>` in place of `<PortableText>` for every body that may be locked,
161
161
 
162
162
  EmDash has no content filter hook, so templates decide what to render.
163
163
 
164
- **`<Content>`** renders an entry body. Everything before the first Paywall break block is shown to everyone. What follows reaches the page only for readers the break allows: the levels picked on it ("Signed in" for any member) or, with none picked, the entry's rule. With none picked and no rule covering the entry, the break changes nothing. A body with no break follows the entry's rule as a whole, and readers it keeps out see the collection's `teaser` if one is set (whole paragraphs, never half of one).
164
+ **`<Content>`** renders an entry body. Levels come from the entry's rule (its content type, its terms, or its own Access panel); a Paywall break only says where that rule starts. Everything before the first Paywall break block is shown to everyone, and what follows reaches the page only for readers the rule lets in. With no rule covering the entry, the break changes nothing. A body with no break follows the entry's rule as a whole, and readers it keeps out see the collection's `teaser` if one is set (whole paragraphs, never half of one).
165
165
 
166
166
  ```astro
167
167
  <Content value={post.data.content} collection="posts" terms={post.data.terms} id={post.id} title={post.data.title}>
@@ -632,7 +632,7 @@ Import each from `@redeemid/emdash/components/<Name>.astro`.
632
632
 
633
633
  | Block | Type | Does |
634
634
  | --- | --- | --- |
635
- | Paywall break | `redeemid-paywall` | Members-only content starts here, for the levels picked or the entry's rule |
635
+ | Paywall break | `redeemid-paywall` | Everyone reads up to here; the rest follows the entry's rule |
636
636
  | Members only / Members only: end | `redeemid-members`, `redeemid-members-end` | A members-only part in the middle of a body |
637
637
  | Sold on its own | `redeemid-item` | Sells the entry by itself to holders of a tag |
638
638
  | Plan | `redeemid-plan` | A product with a buy button |
@@ -53,7 +53,7 @@ Under `/_emdash/api/auth/redeemid/`. Same-origin only: EmDash checks `Origin` on
53
53
  - `api/state` (GET): the visitor's own state.
54
54
 
55
55
  ```json
56
- { "apiVersion": "1.3.0", "version": "0.0.23", "signedIn": true, "linked": true,
56
+ { "apiVersion": "1.3.0", "version": "0.0.24", "signedIn": true, "linked": true,
57
57
  "levels": ["gold"], "features": ["ad-free"], "cart": { "count": 2 },
58
58
  "meter": { "used": 1, "limit": 3, "period": "month" } }
59
59
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redeemid/emdash",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Shopify customer accounts, member levels and paywalls for EmDash sites. Shopify runs commerce; EmDash runs content.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -780,7 +780,7 @@ export async function entryPanel(env: AccessEnv, entry: PanelEntry, toast?: Toas
780
780
  ],
781
781
  submit: { label: "Save access", action_id: "entry_save" },
782
782
  },
783
- { type: "context", text: "Paywall break and Members only blocks can still lock parts of the body." },
783
+ { type: "context", text: "With a Paywall break in the body, everyone reads up to it and this rule covers the rest. Members only blocks can lock parts to their own levels." },
784
784
  ],
785
785
  };
786
786
  }
package/src/access.ts CHANGED
@@ -533,7 +533,7 @@ async function readViewer(astro: AstroLike): Promise<Viewer> {
533
533
 
534
534
  /**
535
535
  * May this visitor read the entry? `override` replaces the entry's rule, for a
536
- * part of a page locked to its own levels (a paywall break with levels).
536
+ * part of a page locked to its own levels (a Members only part with levels).
537
537
  */
538
538
  export async function canRead(
539
539
  astro: AstroLike,
@@ -39,22 +39,20 @@ export function holdsItem(item: SoldItem | null, tags: string[] | undefined): bo
39
39
  type Node = { _type?: unknown; levels?: unknown };
40
40
 
41
41
  /**
42
- * Split a Portable Text body at its first paywall break: the open part, the
43
- * members part, and the levels picked on the break (empty = none picked).
44
- * With no break, everything is in `open` and `paywall` is false.
42
+ * Split a Portable Text body at its first paywall break: the open part and
43
+ * the members part. The break only says where the entry's rule starts to
44
+ * apply; it carries no levels of its own (levels an older break stored are
45
+ * ignored). With no break, everything is in `open` and `paywall` is false.
45
46
  */
46
47
  export function splitAtPaywall<T>(value: T[] | null | undefined): {
47
48
  open: T[];
48
49
  locked: T[];
49
- levels: string[];
50
50
  paywall: boolean;
51
51
  } {
52
52
  const blocks = Array.isArray(value) ? value : [];
53
53
  const at = blocks.findIndex((b) => (b as Node)?._type === PAYWALL_BLOCK);
54
- if (at === -1) return { open: blocks, locked: [], levels: [], paywall: false };
55
- const raw = (blocks[at] as Node).levels;
56
- const levels = Array.isArray(raw) ? raw.filter((l): l is string => typeof l === "string" && l !== "") : [];
57
- return { open: blocks.slice(0, at), locked: blocks.slice(at + 1), levels, paywall: true };
54
+ if (at === -1) return { open: blocks, locked: [], paywall: false };
55
+ return { open: blocks.slice(0, at), locked: blocks.slice(at + 1), paywall: true };
58
56
  }
59
57
 
60
58
  type TextNode = { _type?: unknown; children?: Array<{ text?: unknown }> };
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  /**
3
- * The level an entry needs, read from its rules (and from a paywall break
4
- * with levels):
3
+ * The level an entry needs, read from its rules:
5
4
  *
6
5
  * <AccessBadge collection="posts" terms={post.data.terms} content={post.data.content} id={post.id} />
7
6
  *
@@ -10,14 +9,14 @@
10
9
  * `is-locked` with a lock for readers who can't read the members part.
11
10
  */
12
11
  import { badgeColor, canRead, describe, entryRequirement, simplify, viewer, type Terms } from "../access.js";
13
- import { holdsItem, soldItem, splitAtPaywall } from "../blocks-names.js";
12
+ import { holdsItem, soldItem } from "../blocks-names.js";
14
13
  import { siteConfig } from "../site-config.js";
15
14
  import { text } from "../text.js";
16
15
 
17
16
  interface Props {
18
17
  collection: string;
19
18
  terms?: Terms;
20
- /** The entry's body, so a paywall break's levels count. */
19
+ /** The entry's body, for its "Sold on its own" block. */
21
20
  content?: unknown[] | null;
22
21
  /** The entry's id or slug, so its own rule from the editor counts. */
23
22
  id?: string;
@@ -27,10 +26,7 @@ interface Props {
27
26
 
28
27
  const config = await siteConfig(Astro.locals);
29
28
  const { collection, terms, content, id, free = text(config, "Free"), class: className } = Astro.props;
30
- const split = splitAtPaywall(content);
31
- const required = split.paywall && split.levels.length
32
- ? [split.levels]
33
- : await entryRequirement(config, collection, terms, id);
29
+ const required = await entryRequirement(config, collection, terms, id);
34
30
  const label = describe(required, config?.levels, free, text(config, "Signed in"));
35
31
  const item = soldItem(content);
36
32
  const owner = item ? (await viewer(Astro)).member : null;
@@ -5,9 +5,9 @@
5
5
  * <Content value={post.data.content} collection="posts" terms={post.data.terms} id={post.id} />
6
6
  *
7
7
  * - "Paywall break": everything before the first one is shown to everyone.
8
- * What follows reaches the page only for readers who may read it: the
9
- * levels picked on the break, or when none are picked the entry's rule
10
- * (with no rule, the break changes nothing).
8
+ * What follows reaches the page only for readers the entry's rule lets in
9
+ * (with no rule, the break changes nothing). The break says where the
10
+ * rule starts; the rule itself comes from the entry's settings.
11
11
  * - "Members only" … "Members only: end": the part between is for the levels
12
12
  * picked on the start block, or the entry's rule, or any signed-in member.
13
13
  * Readers who can't read it see the wall in its place, and the body goes on.
@@ -60,9 +60,9 @@ const entryRule = await entryRequirement(config, collection, terms, id);
60
60
  const item = soldItem(value);
61
61
  const reader = item ? (await viewer(Astro)).member : null;
62
62
  const owned = Boolean(reader && reader.status !== "disabled" && holdsItem(item, reader.tags));
63
- // A break with levels picked locks the rest to them; with none picked it
64
- // follows the entry's rule, and does nothing when no rule covers the entry.
65
- const required = split.paywall ? (split.levels.length ? [split.levels] : entryRule) : undefined;
63
+ // The rest after a break follows the entry's rule, and the break does
64
+ // nothing when no rule covers the entry.
65
+ const required = split.paywall ? entryRule : undefined;
66
66
  const verdict: Verdict | { allowed: true; reason: "open" } =
67
67
  owned || (split.paywall && required === null)
68
68
  ? { allowed: true, reason: "open" }
package/src/files.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { readEntry, SIGNED_IN, viewer, type AstroLike, type Terms } from "./access.js";
20
- import { holdsItem, ITEM_BLOCK, PAYWALL_BLOCK, sections, soldItem, splitAtPaywall } from "./blocks-names.js";
20
+ import { holdsItem, ITEM_BLOCK, PAYWALL_BLOCK, sections, soldItem } from "./blocks-names.js";
21
21
  import type { FileRule, RedeemIDConfig } from "./config.js";
22
22
  import { featuresFor } from "./features.js";
23
23
  import { text } from "./text.js";
@@ -79,15 +79,14 @@ async function entriesUsing(db: unknown, mediaId: string): Promise<Array<{ colle
79
79
  }
80
80
 
81
81
  /**
82
- * The levels picked on an entry body's breaks and members-only parts: readers
83
- * must hold one from each. A members-only part with none picked needs a
82
+ * The levels picked on an entry body's members-only parts: readers must
83
+ * hold one from each. A members-only part with none picked needs a
84
84
  * signed-in reader (on top of the entry's rule, which the caller checks).
85
+ * A paywall break adds nothing: what follows it is under the entry's rule.
85
86
  */
86
87
  export function bodyClauses(body: unknown): string[][] {
87
88
  const blocks = Array.isArray(body) ? body : [];
88
89
  const out: string[][] = [];
89
- const split = splitAtPaywall(blocks);
90
- if (split.paywall && split.levels.length) out.push(split.levels);
91
90
  for (const s of sections(blocks)) if (s.members) out.push(s.levels.length ? s.levels : [SIGNED_IN]);
92
91
  return out;
93
92
  }
package/src/plugin.ts CHANGED
@@ -11,10 +11,9 @@
11
11
  * test, Privacy), a daily webhook health check and a dashboard widget. Sign-in, webhooks and checkout stay on the
12
12
  * auth provider's routes.
13
13
  *
14
- * - "Paywall break" (`redeemid-paywall`): everything after it in the body is
15
- * for members. Pick the levels that may read on ("Signed in" for any
16
- * member); none picked means the entry's rule, and nothing is locked when
17
- * no rule covers the entry.
14
+ * - "Paywall break" (`redeemid-paywall`): marks where the entry's rule starts.
15
+ * Everything before it is for everyone; everything after it follows the
16
+ * entry's rule, and nothing is locked when no rule covers the entry.
18
17
  * - "Members only" (`redeemid-members`) and "Members only: end"
19
18
  * (`redeemid-members-end`): the part between is for the levels picked on
20
19
  * the start block; everyone else sees the wall there and reads on after.
@@ -84,15 +83,8 @@ function blocks(config: RedeemIDConfig) {
84
83
  type: PAYWALL_BLOCK,
85
84
  label: "Paywall break",
86
85
  icon: "lock",
87
- description: "Everything after this is for members",
88
- fields: [
89
- {
90
- type: "checkbox" as const,
91
- action_id: "levels",
92
- label: "Who can read on (none = the entry's rule)",
93
- options: levelOptions,
94
- },
95
- ],
86
+ description: "Everyone reads up to here; the rest follows this entry's access rule",
87
+ fields: [],
96
88
  },
97
89
  {
98
90
  type: MEMBERS_BLOCK,
package/src/settings.ts CHANGED
@@ -226,7 +226,7 @@ type LevelListener = (levels: LevelConfig[]) => void;
226
226
  const LISTENERS = Symbol.for("redeemid:level-listeners");
227
227
 
228
228
  /**
229
- * The editor's level pickers (on the Paywall break and other blocks) are
229
+ * The editor's level pickers (on the Members only block and others) are
230
230
  * declared when the plugin starts. The plugin registers a listener here so
231
231
  * they follow levels saved in the admin.
232
232
  */
package/src/staff.ts CHANGED
@@ -398,7 +398,7 @@ export async function accessTest(config: RedeemIDConfig, values: Record<string,
398
398
  type: "banner",
399
399
  variant: ok ? "default" : "alert",
400
400
  title: ok ? "This reader can read it" : "This reader is kept out",
401
- description: "A Paywall break with levels picked, a Members only part, drip and the meter can still change what a page shows.",
401
+ description: "A Members only part with levels picked, drip and the meter can still change what a page shows.",
402
402
  },
403
403
  );
404
404
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.0.23";
1
+ export const VERSION = "0.0.24";