@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 +3 -3
- package/docs/developer-api.md +1 -1
- package/package.json +1 -1
- package/src/access-admin.ts +1 -1
- package/src/access.ts +1 -1
- package/src/blocks-names.ts +6 -8
- package/src/components/AccessBadge.astro +4 -8
- package/src/components/Content.astro +6 -6
- package/src/files.ts +4 -5
- package/src/plugin.ts +5 -13
- package/src/settings.ts +1 -1
- package/src/staff.ts +1 -1
- package/src/version.ts +1 -1
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.
|
|
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.
|
|
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` |
|
|
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 |
|
package/docs/developer-api.md
CHANGED
|
@@ -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.
|
|
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
package/src/access-admin.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
package/src/blocks-names.ts
CHANGED
|
@@ -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
|
|
43
|
-
* members part
|
|
44
|
-
*
|
|
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: [],
|
|
55
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const required = split.paywall ?
|
|
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
|
|
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
|
|
83
|
-
*
|
|
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`):
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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: "
|
|
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
|
|
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
|
|
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.
|
|
1
|
+
export const VERSION = "0.0.24";
|