@redeemid/emdash 0.0.4 → 0.0.5
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 +13 -2
- package/package.json +1 -1
- package/src/blocks/PaywallBreak.astro +3 -2
- package/src/blocks/index.ts +4 -2
- package/src/blocks-names.ts +39 -0
- package/src/components/Content.astro +52 -29
- package/src/components/Gate.astro +4 -12
- package/src/components/LockedPanel.astro +48 -0
- package/src/components/Notices.astro +36 -0
- package/src/config.ts +6 -0
- package/src/plugin.ts +27 -3
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Shopify customer accounts, member levels and paywalls for [EmDash](https://github.com/emdash-cms/emdash) sites. Shopify runs the store and the customer account; EmDash runs the content.
|
|
4
4
|
|
|
5
|
-
**Status: 0.0.
|
|
5
|
+
**Status: 0.0.5, early.** Runs on Node and Cloudflare Workers. Sign-in, the member mirror, webhooks, levels, rules, teasers, badges, the page gate, the Shopify cart and checkout are covered by an end-to-end test against a fake store.
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
|
@@ -14,6 +14,8 @@ Shopify customer accounts, member levels and paywalls for [EmDash](https://githu
|
|
|
14
14
|
- **Plans, cart and checkout.** `<Plan>` shows a Shopify product's price and adds it to the visitor's Shopify cart; `<Cart>` shows the count and opens Shopify checkout.
|
|
15
15
|
- **Account.** Members get an Account link to their Shopify customer account (orders, profile, addresses) and Sign out, which also ends the Shopify session.
|
|
16
16
|
- **Paywall break block.** Editors write one body and drop a "Paywall break" block where members-only content starts, picking which levels may read on, or leaving it to the entry's rule. `<Content>` renders the body and leaves the rest off the page for everyone else.
|
|
17
|
+
- **Members only block.** For a members part in the middle of a body: "Members only" (pick the levels) and "Members only: end". Everyone else sees the wall in its place and reads on after it.
|
|
18
|
+
- **The wall.** Readers kept out see why and what to do next: sign in, or join, with each level's own wall text. `<Notices>` shows the messages sign-in, Buy, Cart and Account send readers back with.
|
|
17
19
|
- **Plan block.** Editors add a Shopify product to any page with the "Plan" block: title, price and a buy button.
|
|
18
20
|
- **In the admin.** RedeemID shows under Plugins with a Members page (Shopify customers, tags, levels), a Status page (secrets, store discovery, the URLs to give Shopify, levels and rules) and a dashboard widget.
|
|
19
21
|
- **Keeps members out of EmDash's own APIs**, so the gate is the only way to read locked content.
|
|
@@ -34,7 +36,8 @@ const config = {
|
|
|
34
36
|
shop: "my-store.myshopify.com",
|
|
35
37
|
levels: [
|
|
36
38
|
{ slug: "member", name: "Member", tags: ["member"] },
|
|
37
|
-
|
|
39
|
+
// wall: what the wall says to readers this level would let in; joinUrl overrides join.href.
|
|
40
|
+
{ slug: "pro", name: "Pro", tags: ["pro"], wall: "Pro members read every report." },
|
|
38
41
|
// match: "all" needs every tag, e.g. both memberships:
|
|
39
42
|
{ slug: "both", name: "Member + Pro", tags: ["member", "pro"], match: "all" },
|
|
40
43
|
],
|
|
@@ -42,6 +45,8 @@ const config = {
|
|
|
42
45
|
{ collection: "posts", taxonomy: "category", term: "members", levels: ["member", "pro"] },
|
|
43
46
|
{ collection: "posts", taxonomy: "category", term: "pro", levels: ["pro"] },
|
|
44
47
|
],
|
|
48
|
+
// Optional: where the wall's join link goes.
|
|
49
|
+
join: { href: "/join/", label: "Become a member" },
|
|
45
50
|
// Optional: locked posts with no paywall break show the paragraphs within their first 40 words.
|
|
46
51
|
teaser: { posts: 40 },
|
|
47
52
|
};
|
|
@@ -107,6 +112,10 @@ import Content from "@redeemid/emdash/components/Content.astro";
|
|
|
107
112
|
|
|
108
113
|
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 when none are picked the entry's rule. With none picked and no rule covering the entry, the break changes nothing, as on WordPress. A body with no break follows the entry's rule as a whole; readers it keeps out see the collection's `teaser` (the paragraphs within that many words, never half a paragraph), if one is set. Plain `<PortableText>` ignores the break and shows everything, so use `<Content>` for any body that may hold one.
|
|
109
114
|
|
|
115
|
+
A "Members only" block starts a part for the levels picked on it (none picked: the entry's rule, or any signed-in member) that runs to the next "Members only: end" block. Readers who can't read it see the wall there, and the body goes on after the end block.
|
|
116
|
+
|
|
117
|
+
The wall is `<LockedPanel>`: it says why the reader is kept out and offers sign-in or the join link, with the wall text of the first level they are missing. `<Content>` and `<Gate>` show it; its default slot (and `<Content>`'s "locked" slot) goes on top.
|
|
118
|
+
|
|
110
119
|
To lock a part of a template that isn't the body, wrap it in `<Gate>` (`<Gate levels={["pro"]}>…</Gate>`).
|
|
111
120
|
|
|
112
121
|
Show what an entry needs with `<AccessBadge>`, on cards and on the entry. It reads the entry's rules (and a paywall break's levels when you pass `content`), shows "Free" for open entries (`free=""` hides it), and adds `is-locked` and a lock for readers it keeps out. `data-redeemid-level` holds the level slugs, for colours:
|
|
@@ -120,6 +129,8 @@ import AccessBadge from "@redeemid/emdash/components/AccessBadge.astro";
|
|
|
120
129
|
|
|
121
130
|
In your own code (`@redeemid/emdash/access`), `entryRequirement(config, collection, terms)` gives what an entry needs (a list of clauses, each passed by any of its levels, or `null` when open), and `canRead(Astro, entry)` gives the verdict for the visitor.
|
|
122
131
|
|
|
132
|
+
Put `<Notices />` near the top of your layout. It shows the message for a `?redeemid_notice=` code (see [docs/shopify-setup.md](docs/shopify-setup.md)) and nothing otherwise; pass `messages={{ cart_empty: "…" }}` to reword one.
|
|
133
|
+
|
|
123
134
|
Add the account box and cart to your header. Visitors see Sign in; members see their name, levels, Account and Sign out. The cart shows how many items are in the visitor's Shopify cart and goes to its checkout:
|
|
124
135
|
|
|
125
136
|
```astro
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* RedeemID's marker blocks (Paywall break, Members only and its end) render
|
|
4
|
+
* nothing themselves. <Content> splits the body at them and leaves out what
|
|
5
|
+
* readers can't read.
|
|
5
6
|
*/
|
|
6
7
|
---
|
package/src/blocks/index.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Site-side renderers for RedeemID's Portable Text blocks, picked up by
|
|
3
3
|
* EmDash from the plugin's `componentsEntry`.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import Marker from "./PaywallBreak.astro";
|
|
6
6
|
import PlanBlock from "./PlanBlock.astro";
|
|
7
7
|
|
|
8
8
|
export const blockComponents = {
|
|
9
|
-
"redeemid-paywall":
|
|
9
|
+
"redeemid-paywall": Marker,
|
|
10
|
+
"redeemid-members": Marker,
|
|
11
|
+
"redeemid-members-end": Marker,
|
|
10
12
|
"redeemid-plan": PlanBlock,
|
|
11
13
|
};
|
package/src/blocks-names.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Portable Text block types RedeemID adds to the editor. */
|
|
2
2
|
export const PAYWALL_BLOCK = "redeemid-paywall";
|
|
3
3
|
export const PLAN_BLOCK = "redeemid-plan";
|
|
4
|
+
/** Starts a members-only part of the body; it runs to the next end block. */
|
|
5
|
+
export const MEMBERS_BLOCK = "redeemid-members";
|
|
6
|
+
export const MEMBERS_END_BLOCK = "redeemid-members-end";
|
|
4
7
|
|
|
5
8
|
type Node = { _type?: unknown; levels?: unknown };
|
|
6
9
|
|
|
@@ -44,3 +47,39 @@ export function teaser<T>(value: T[] | null | undefined, words: number): T[] {
|
|
|
44
47
|
}
|
|
45
48
|
return out;
|
|
46
49
|
}
|
|
50
|
+
|
|
51
|
+
export interface Section<T> {
|
|
52
|
+
/** True for a part between a "Members only" block and its end. */
|
|
53
|
+
members: boolean;
|
|
54
|
+
/** Levels picked on the "Members only" block (empty = none picked). */
|
|
55
|
+
levels: string[];
|
|
56
|
+
blocks: T[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const levelsOf = (raw: unknown) =>
|
|
60
|
+
Array.isArray(raw) ? raw.filter((l): l is string => typeof l === "string" && l !== "") : [];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Split blocks into open parts and members-only parts. A "Members only" block
|
|
64
|
+
* starts a part that runs to the next "Members only: end" block (or the end of
|
|
65
|
+
* the blocks); the marker blocks themselves are dropped. A stray end block is
|
|
66
|
+
* dropped too.
|
|
67
|
+
*/
|
|
68
|
+
export function sections<T>(value: T[] | null | undefined): Section<T>[] {
|
|
69
|
+
const out: Section<T>[] = [];
|
|
70
|
+
let current: Section<T> = { members: false, levels: [], blocks: [] };
|
|
71
|
+
for (const b of Array.isArray(value) ? value : []) {
|
|
72
|
+
const type = (b as Node)?._type;
|
|
73
|
+
if (type === MEMBERS_BLOCK || type === MEMBERS_END_BLOCK) {
|
|
74
|
+
if (current.blocks.length > 0 || current.members) out.push(current);
|
|
75
|
+
current =
|
|
76
|
+
type === MEMBERS_BLOCK
|
|
77
|
+
? { members: true, levels: levelsOf((b as Node).levels), blocks: [] }
|
|
78
|
+
: { members: false, levels: [], blocks: [] };
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
current.blocks.push(b);
|
|
82
|
+
}
|
|
83
|
+
if (current.blocks.length > 0 || current.members) out.push(current);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* An entry's body, with RedeemID's
|
|
3
|
+
* An entry's body, with RedeemID's blocks applied.
|
|
4
4
|
*
|
|
5
5
|
* <Content value={post.data.content} collection="posts" terms={post.data.terms} />
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* picked on the break, or when none are picked the entry's rule
|
|
10
|
-
* the break changes nothing, as on WordPress).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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, as on WordPress).
|
|
11
|
+
* - "Members only" … "Members only: end": the part between is for the levels
|
|
12
|
+
* picked on the start block, or the entry's rule, or any signed-in member.
|
|
13
|
+
* Readers who can't read it see the wall in its place, and the body goes on.
|
|
14
|
+
* - No break: the entry's rule covers the whole body; readers it keeps out see
|
|
15
|
+
* the collection's teaser (config `teaser`, in words), if any.
|
|
16
|
+
*
|
|
17
|
+
* Readers kept out get the wall (LockedPanel), with the "locked" slot on top.
|
|
13
18
|
*/
|
|
14
19
|
import { PortableText } from "emdash/ui";
|
|
15
20
|
|
|
16
|
-
import { canRead, entryRequirement,
|
|
17
|
-
import { splitAtPaywall, teaser } from "../blocks-names.js";
|
|
21
|
+
import { canRead, entryRequirement, type Terms, type Verdict } from "../access.js";
|
|
22
|
+
import { sections, splitAtPaywall, teaser } from "../blocks-names.js";
|
|
18
23
|
import { siteConfig } from "../site-config.js";
|
|
24
|
+
import LockedPanel from "./LockedPanel.astro";
|
|
19
25
|
|
|
20
26
|
interface Props {
|
|
21
27
|
value?: unknown[] | null;
|
|
@@ -26,35 +32,52 @@ interface Props {
|
|
|
26
32
|
const { value, collection = "", terms } = Astro.props;
|
|
27
33
|
const split = splitAtPaywall(value);
|
|
28
34
|
const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
|
|
35
|
+
const entryRule = await entryRequirement(config, collection, terms);
|
|
29
36
|
// A break with levels picked locks the rest to them; with none picked it
|
|
30
37
|
// follows the entry's rule, and does nothing when no rule covers the entry.
|
|
31
|
-
const required = split.paywall
|
|
32
|
-
|
|
33
|
-
? [split.levels]
|
|
34
|
-
: await entryRequirement(config, collection, terms)
|
|
35
|
-
: undefined;
|
|
36
|
-
const verdict =
|
|
38
|
+
const required = split.paywall ? (split.levels.length ? [split.levels] : entryRule) : undefined;
|
|
39
|
+
const verdict: Verdict | { allowed: true; reason: "open" } =
|
|
37
40
|
split.paywall && required === null
|
|
38
|
-
? { allowed: true, reason: "open"
|
|
41
|
+
? { allowed: true, reason: "open" }
|
|
39
42
|
: await canRead(Astro, { collection, terms }, required ? { required } : undefined);
|
|
40
43
|
const words = config?.teaser?.[collection] ?? 0;
|
|
41
44
|
const shown = split.paywall ? split.open : verdict.allowed ? split.open : teaser(split.open, words);
|
|
42
|
-
const
|
|
45
|
+
const rest = verdict.allowed ? split.locked : [];
|
|
46
|
+
|
|
47
|
+
// Each "Members only" part gets its own verdict.
|
|
48
|
+
async function parts(blocks: unknown[]) {
|
|
49
|
+
const out: Array<{ blocks: unknown[]; verdict?: Verdict }> = [];
|
|
50
|
+
for (const section of sections(blocks)) {
|
|
51
|
+
if (!section.members) {
|
|
52
|
+
out.push({ blocks: section.blocks });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const needs = section.levels.length ? [section.levels] : (entryRule ?? [[]]);
|
|
56
|
+
out.push({ blocks: section.blocks, verdict: await canRead(Astro, { collection, terms }, { required: needs }) });
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
const before = await parts(shown);
|
|
61
|
+
const after = await parts(rest);
|
|
43
62
|
---
|
|
44
63
|
|
|
45
|
-
{
|
|
64
|
+
{before.map((p) =>
|
|
65
|
+
!p.verdict || p.verdict.allowed ? (
|
|
66
|
+
p.blocks.length > 0 && <PortableText value={p.blocks} />
|
|
67
|
+
) : (
|
|
68
|
+
<LockedPanel reason={p.verdict.reason} required={p.verdict.required} held={p.verdict.viewer.levels} />
|
|
69
|
+
),
|
|
70
|
+
)}
|
|
46
71
|
{verdict.allowed ? (
|
|
47
|
-
|
|
72
|
+
after.map((p) =>
|
|
73
|
+
!p.verdict || p.verdict.allowed ? (
|
|
74
|
+
p.blocks.length > 0 && <PortableText value={p.blocks} />
|
|
75
|
+
) : (
|
|
76
|
+
<LockedPanel reason={p.verdict.reason} required={p.verdict.required} held={p.verdict.viewer.levels} />
|
|
77
|
+
),
|
|
78
|
+
)
|
|
48
79
|
) : (
|
|
49
|
-
<
|
|
80
|
+
<LockedPanel reason={verdict.reason} required={verdict.required} held={verdict.viewer.levels}>
|
|
50
81
|
<slot name="locked" />
|
|
51
|
-
|
|
52
|
-
<p>
|
|
53
|
-
This is for members. <a href={signInUrl(here)}>Sign in</a> to read on.
|
|
54
|
-
</p>
|
|
55
|
-
)}
|
|
56
|
-
{verdict.reason === "upgrade" && <p>Your membership doesn't include the rest of this yet.</p>}
|
|
57
|
-
{verdict.reason === "disabled" && <p>Your account is turned off. Contact the store for help.</p>}
|
|
58
|
-
{verdict.reason === "unconfigured" && <p>This is for members.</p>}
|
|
59
|
-
</div>
|
|
82
|
+
</LockedPanel>
|
|
60
83
|
)}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Pass `levels` to lock a part of a page to specific levels instead.
|
|
12
12
|
*/
|
|
13
|
-
import { canRead,
|
|
13
|
+
import { canRead, type Terms } from "../access.js";
|
|
14
|
+
import LockedPanel from "./LockedPanel.astro";
|
|
14
15
|
|
|
15
16
|
interface Props {
|
|
16
17
|
collection?: string;
|
|
@@ -20,21 +21,12 @@ interface Props {
|
|
|
20
21
|
|
|
21
22
|
const { collection = "", terms, levels } = Astro.props;
|
|
22
23
|
const verdict = await canRead(Astro, { collection, terms }, levels ? { levels } : undefined);
|
|
23
|
-
const here = Astro.url.pathname + Astro.url.search;
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
{verdict.allowed ? (
|
|
27
27
|
<slot />
|
|
28
28
|
) : (
|
|
29
|
-
<
|
|
29
|
+
<LockedPanel reason={verdict.reason} required={verdict.required} held={verdict.viewer.levels}>
|
|
30
30
|
<slot name="locked" />
|
|
31
|
-
|
|
32
|
-
<p>
|
|
33
|
-
This is for members. <a href={signInUrl(here)}>Sign in</a> to read it.
|
|
34
|
-
</p>
|
|
35
|
-
)}
|
|
36
|
-
{verdict.reason === "upgrade" && <p>Your membership doesn't include this yet.</p>}
|
|
37
|
-
{verdict.reason === "disabled" && <p>Your account is turned off. Contact the store for help.</p>}
|
|
38
|
-
{verdict.reason === "unconfigured" && <p>This is for members.</p>}
|
|
39
|
-
</div>
|
|
31
|
+
</LockedPanel>
|
|
40
32
|
)}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The wall readers see in place of what they can't read: why, and what to do
|
|
4
|
+
* next (sign in, or join). <Content> and <Gate> show it; use it directly for
|
|
5
|
+
* your own locked parts.
|
|
6
|
+
*
|
|
7
|
+
* The level it offers is the first one the reader is missing. Its `wall` text
|
|
8
|
+
* and `joinUrl` come from the level's config, the join link from `join`.
|
|
9
|
+
* Anything in the default slot is shown above the built-in text.
|
|
10
|
+
*/
|
|
11
|
+
import { passes, signInUrl, type Reason } from "../access.js";
|
|
12
|
+
import { siteConfig } from "../site-config.js";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
reason: Reason;
|
|
16
|
+
/** What the locked part needs (Verdict.required). */
|
|
17
|
+
required?: string[][];
|
|
18
|
+
/** Levels the reader holds (Verdict.viewer.levels). */
|
|
19
|
+
held?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { reason, required = [], held = [] } = Astro.props;
|
|
23
|
+
const config = siteConfig((Astro.locals as { emdash?: { config?: unknown } }).emdash?.config);
|
|
24
|
+
const missing = required.find((clause) => clause.length > 0 && !passes(held, [clause]));
|
|
25
|
+
const level = missing ? config?.levels?.find((l) => l.slug === missing[0]) : undefined;
|
|
26
|
+
const joinHref = level?.joinUrl ?? config?.join?.href;
|
|
27
|
+
const joinLabel = config?.join?.label ?? "Become a member";
|
|
28
|
+
const here = Astro.url.pathname + Astro.url.search;
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<div class="redeemid-locked" data-redeemid-reason={reason} data-redeemid-level={level?.slug}>
|
|
32
|
+
<slot />
|
|
33
|
+
{level?.wall && (reason === "sign_in" || reason === "upgrade") && <p class="redeemid-locked__wall">{level.wall}</p>}
|
|
34
|
+
{reason === "sign_in" && (
|
|
35
|
+
<p>
|
|
36
|
+
This is for members. <a href={signInUrl(here)}>Sign in</a> to read on.
|
|
37
|
+
{joinHref && <> Not a member yet? <a href={joinHref}>{joinLabel}</a>.</>}
|
|
38
|
+
</p>
|
|
39
|
+
)}
|
|
40
|
+
{reason === "upgrade" && (
|
|
41
|
+
<p>
|
|
42
|
+
{level ? `This is for ${level.name} members.` : "Your membership doesn't include this yet."}
|
|
43
|
+
{joinHref && <> <a href={joinHref}>{joinLabel}</a></>}
|
|
44
|
+
</p>
|
|
45
|
+
)}
|
|
46
|
+
{reason === "disabled" && <p>Your account is turned off. Contact the store for help.</p>}
|
|
47
|
+
{reason === "unconfigured" && <p>This is for members.</p>}
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The message for a `?redeemid_notice=` code that sign-in, Buy, Cart and
|
|
4
|
+
* Account send readers back with. Renders nothing without one. Pass
|
|
5
|
+
* `messages` to reword any of them.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
messages?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULTS: Record<string, string> = {
|
|
12
|
+
not_configured: "Sign-in isn't set up on this site yet.",
|
|
13
|
+
sign_in_unavailable: "Sign-in isn't available right now. Try again in a few minutes.",
|
|
14
|
+
sign_in_expired: "That sign-in took too long. Try again.",
|
|
15
|
+
staff_account: "This account can't sign in here. Use a different email.",
|
|
16
|
+
site_not_ready: "This site isn't ready for sign-in yet.",
|
|
17
|
+
email_required: "Your account needs an email address to sign in.",
|
|
18
|
+
token_failed: "Sign-in didn't finish. Try again.",
|
|
19
|
+
bad_id_token: "Sign-in didn't finish. Try again.",
|
|
20
|
+
identity_failed: "Sign-in didn't finish. Try again.",
|
|
21
|
+
plan_unavailable: "That plan isn't available right now.",
|
|
22
|
+
checkout_failed: "Checkout didn't open. Try again.",
|
|
23
|
+
cart_empty: "Your cart is empty.",
|
|
24
|
+
account_unavailable: "Your account page isn't available right now. Try again in a few minutes.",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const { messages = {} } = Astro.props;
|
|
28
|
+
const code = Astro.url.searchParams.get("redeemid_notice") ?? "";
|
|
29
|
+
const text = code ? (messages[code] ?? DEFAULTS[code]) : undefined;
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
{text && (
|
|
33
|
+
<div class="redeemid-notice" role="status" data-redeemid-notice={code}>
|
|
34
|
+
<p>{text}</p>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
package/src/config.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface LevelConfig {
|
|
|
15
15
|
tags: string[];
|
|
16
16
|
/** "any" (default): one of the tags is enough. "all": every tag is needed. */
|
|
17
17
|
match?: "any" | "all";
|
|
18
|
+
/** What the wall says to readers this level would let in, e.g. "Gold members read every investigation." */
|
|
19
|
+
wall?: string;
|
|
20
|
+
/** Where the wall's join link goes for this level. Falls back to `join.href`. */
|
|
21
|
+
joinUrl?: string;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -57,6 +61,8 @@ export interface RedeemIDConfig {
|
|
|
57
61
|
label?: string;
|
|
58
62
|
levels?: LevelConfig[];
|
|
59
63
|
rules?: RuleConfig[];
|
|
64
|
+
/** The wall's join link: where readers become members, e.g. { href: "/join/", label: "Become a member" }. */
|
|
65
|
+
join?: { href: string; label?: string };
|
|
60
66
|
/**
|
|
61
67
|
* How several term rules on one entry combine: "all" (default, the most
|
|
62
68
|
* restrictive: pass every rule) or "any" (pass one of them).
|
package/src/plugin.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* plugins: [redeemidPlugin(config)],
|
|
7
7
|
* })
|
|
8
8
|
*
|
|
9
|
-
* It adds
|
|
9
|
+
* It adds four Portable Text blocks to the editor, a Members page, a Status
|
|
10
10
|
* page and a dashboard widget. Sign-in, webhooks and checkout stay on the
|
|
11
11
|
* auth provider's routes.
|
|
12
12
|
*
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* for members. Pick the levels that may read on ("Signed in" for any
|
|
15
15
|
* member); none picked means the entry's rule, and nothing is locked when
|
|
16
16
|
* no rule covers the entry, as on WordPress.
|
|
17
|
+
* - "Members only" (`redeemid-members`) and "Members only: end"
|
|
18
|
+
* (`redeemid-members-end`): the part between is for the levels picked on
|
|
19
|
+
* the start block; everyone else sees the wall there and reads on after.
|
|
17
20
|
* - "Plan" (`redeemid-plan`): a Shopify product's title and price with a
|
|
18
21
|
* button that adds it to the cart and opens checkout.
|
|
19
22
|
*
|
|
@@ -31,9 +34,9 @@ import { stores, type MemberRecord } from "./storage.js";
|
|
|
31
34
|
import { PAGES, WIDGETS } from "./descriptor.js";
|
|
32
35
|
import { VERSION } from "./version.js";
|
|
33
36
|
|
|
34
|
-
import { PAYWALL_BLOCK, PLAN_BLOCK } from "./blocks-names.js";
|
|
37
|
+
import { MEMBERS_BLOCK, MEMBERS_END_BLOCK, PAYWALL_BLOCK, PLAN_BLOCK } from "./blocks-names.js";
|
|
35
38
|
|
|
36
|
-
export { PAYWALL_BLOCK, PLAN_BLOCK };
|
|
39
|
+
export { MEMBERS_BLOCK, MEMBERS_END_BLOCK, PAYWALL_BLOCK, PLAN_BLOCK };
|
|
37
40
|
|
|
38
41
|
function blocks(config: RedeemIDConfig) {
|
|
39
42
|
const levelOptions = [
|
|
@@ -55,6 +58,27 @@ function blocks(config: RedeemIDConfig) {
|
|
|
55
58
|
},
|
|
56
59
|
],
|
|
57
60
|
},
|
|
61
|
+
{
|
|
62
|
+
type: MEMBERS_BLOCK,
|
|
63
|
+
label: "Members only",
|
|
64
|
+
icon: "users",
|
|
65
|
+
description: "What follows, up to \"Members only: end\", is for members",
|
|
66
|
+
fields: [
|
|
67
|
+
{
|
|
68
|
+
type: "checkbox" as const,
|
|
69
|
+
action_id: "levels",
|
|
70
|
+
label: "Who can read it (none = the entry's rule, or any signed-in member)",
|
|
71
|
+
options: levelOptions,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: MEMBERS_END_BLOCK,
|
|
77
|
+
label: "Members only: end",
|
|
78
|
+
icon: "users",
|
|
79
|
+
description: "Ends the members-only part; everyone reads on from here",
|
|
80
|
+
fields: [],
|
|
81
|
+
},
|
|
58
82
|
{
|
|
59
83
|
type: PLAN_BLOCK,
|
|
60
84
|
label: "Plan",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.0.
|
|
1
|
+
export const VERSION = "0.0.5";
|