@redeemid/emdash 0.0.30 → 0.0.32
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/access.md +7 -5
- package/docs/content.md +1 -1
- package/docs/developer-api.md +2 -2
- package/docs/getting-started.md +4 -4
- package/docs/production.md +3 -3
- package/docs/reference.md +3 -3
- package/docs/shopify-flow.md +2 -2
- package/docs/shopify-setup.md +4 -4
- package/docs/staff.md +21 -9
- package/package.json +11 -2
- package/src/access-admin.ts +211 -76
- package/src/access-map.ts +143 -0
- package/src/admin.ts +505 -0
- package/src/descriptor.ts +16 -9
- package/src/monitor.ts +20 -33
- package/src/plugin.ts +327 -81
- package/src/setup.ts +33 -38
- package/src/staff.ts +148 -66
- package/src/version.ts +1 -1
package/src/setup.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* RedeemID → Connection: connecting a store, step by step, from the admin
|
|
3
|
+
* alone, one tab per step. Each step says what to do in Shopify (with the exact values
|
|
4
4
|
* to paste), what to set on the site, and has a button for whatever
|
|
5
5
|
* RedeemID can do itself: register the webhooks, add the first level.
|
|
6
6
|
*
|
|
7
7
|
* Nothing about progress is stored. Each step's state comes from the site
|
|
8
|
-
* as it is (setupFacts), so opening
|
|
9
|
-
* and it always agrees with the Status
|
|
8
|
+
* as it is (setupFacts), so opening Connection again picks up where things stand
|
|
9
|
+
* and it always agrees with the Status tab.
|
|
10
10
|
*
|
|
11
11
|
* Secrets stay where they are today, in the site's environment: the wizard
|
|
12
12
|
* checks them and gives the command to set each one, and never stores one.
|
|
13
13
|
* It writes only what staff can already write (a level, through Access) and
|
|
14
14
|
* never grants access: levels still come only from Shopify tags.
|
|
15
15
|
*
|
|
16
|
-
* setupSteps() and
|
|
16
|
+
* setupSteps() and setupPanels() are pure.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { adminConfigured, adminScopes, grantedScopes, topicName, webhookSubscriptions, WEBHOOK_TOPICS } from "./admin-api.js";
|
|
@@ -24,7 +24,7 @@ import type { Stores } from "./storage.js";
|
|
|
24
24
|
type Toast = { message: string; type: "success" | "error" | "info" };
|
|
25
25
|
export type Blocks = { blocks: unknown[]; toast?: Toast };
|
|
26
26
|
|
|
27
|
-
/** Scopes the
|
|
27
|
+
/** Scopes the Admin API app needs for staff tools and webhook registration. */
|
|
28
28
|
export const SETUP_SCOPES = ["read_customers", "write_customers"] as const;
|
|
29
29
|
|
|
30
30
|
export const CALLBACK = "/_emdash/api/auth/redeemid/callback";
|
|
@@ -134,8 +134,8 @@ export function setupSteps(f: SetupFacts): Step[] {
|
|
|
134
134
|
return [
|
|
135
135
|
{ key: "store", title: "Connect the store", state: store ? "done" : "todo" },
|
|
136
136
|
{ key: "sign_in", title: "Create the sign-in client", state: signIn ? "done" : store ? "todo" : "waiting", waitingFor: "Connect the store first." },
|
|
137
|
-
{ key: "app", title: "Connect the
|
|
138
|
-
{ key: "webhooks", title: "Register the webhooks", state: hooks ? "done" : app ? "todo" : "waiting", waitingFor: "Connect the
|
|
137
|
+
{ key: "app", title: "Connect the Admin API app", state: app ? "done" : store ? "todo" : "waiting", waitingFor: "Connect the store first." },
|
|
138
|
+
{ key: "webhooks", title: "Register the webhooks", state: hooks ? "done" : app ? "todo" : "waiting", waitingFor: "Connect the Admin API app first: webhooks are registered through it." },
|
|
139
139
|
{ key: "level", title: "Create your first level", state: level ? "done" : "todo" },
|
|
140
140
|
{ key: "try", title: "Try a sign-in", state: f.signedIn > 0 ? "done" : ready ? "todo" : "waiting", waitingFor: "Finish the steps above first." },
|
|
141
141
|
{ key: "storefront", title: "Add Storefront tokens (for plans and the shop)", state: f.storefront ? "done" : "optional" },
|
|
@@ -182,7 +182,7 @@ function stepBlocks(step: Step, f: SetupFacts): unknown[] {
|
|
|
182
182
|
text: "In Shopify admin, open Sales channels, then Headless (add the channel if it isn't there), and Create storefront. Name it after this site. Open its Customer Account API settings, pick a client type (Public needs no secret; Confidential gives one) and, under Application setup, add these:",
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
|
-
type: "
|
|
185
|
+
type: "redeemid_copy",
|
|
186
186
|
fields: [
|
|
187
187
|
{ label: "Callback URI", value: `${f.origin}${CALLBACK}` },
|
|
188
188
|
{ label: "JavaScript origin (public clients)", value: f.origin },
|
|
@@ -200,7 +200,7 @@ function stepBlocks(step: Step, f: SetupFacts): unknown[] {
|
|
|
200
200
|
? `Shopify refused the app: ${f.admin} The app must be installed on this store and be in the same Shopify organization.`
|
|
201
201
|
: f.admin === "ok"
|
|
202
202
|
? `The app is connected but lacks ${missingScopes(f).join(" and ")}. Add the scopes, release a new version and let the store accept it.`
|
|
203
|
-
: "
|
|
203
|
+
: "RedeemID uses an Admin API app for the staff tools (resyncs, tag edits, member lookups) and to register webhooks, and Shopify signs webhooks with its client secret.";
|
|
204
204
|
return [
|
|
205
205
|
...out,
|
|
206
206
|
{ type: "section", text: why },
|
|
@@ -266,34 +266,29 @@ function stepBlocks(step: Step, f: SetupFacts): unknown[] {
|
|
|
266
266
|
return out;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
/**
|
|
270
|
-
export
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
/** Connection's tab address for each step (/connection/sign-in). */
|
|
270
|
+
export const STEP_TABS: Record<Step["key"], string> = { store: "store", sign_in: "sign-in", app: "app", webhooks: "webhooks", level: "level", try: "try", storefront: "storefront" };
|
|
271
|
+
const SHORT: Record<Step["key"], string> = { store: "Store", sign_in: "Sign-in", app: "Admin app", webhooks: "Webhooks", level: "First level", try: "Try it", storefront: "Storefront" };
|
|
272
|
+
|
|
273
|
+
/** The steps that count toward "set up" (Storefront is optional). */
|
|
274
|
+
export const requiredSteps = (steps: Step[]) => steps.filter((s) => s.key !== "storefront");
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* One tab per step, for Connection: the step, with a Check again button
|
|
278
|
+
* until it's done. Labels carry a check mark once done. Pure.
|
|
279
|
+
*/
|
|
280
|
+
export function setupPanels(f: SetupFacts): Array<{ tab: string; label: string; blocks: unknown[] }> {
|
|
281
|
+
return setupSteps(f).map((step) => ({
|
|
282
|
+
tab: STEP_TABS[step.key],
|
|
283
|
+
label: step.state === "done" ? `${SHORT[step.key]} ✓` : SHORT[step.key],
|
|
275
284
|
blocks: [
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
? { type: "banner", variant: "default", title: "RedeemID is set up", description: "Members can sign in with their Shopify customer account. Levels, rules and the meter are under Access." }
|
|
279
|
-
: {
|
|
280
|
-
type: "banner",
|
|
281
|
-
variant: "alert",
|
|
282
|
-
title: `${done} of ${required.length} steps done`,
|
|
283
|
-
description: "Work down the steps. Each one checks the site as it is, so set a secret, redeploy or restart, and press Check again.",
|
|
284
|
-
},
|
|
285
|
-
{ type: "actions", elements: [{ type: "button", action_id: "setup_check", label: "Check again", style: "secondary" }] },
|
|
286
|
-
{
|
|
287
|
-
type: "table",
|
|
288
|
-
page_action_id: "setup_steps",
|
|
289
|
-
columns: [
|
|
290
|
-
{ key: "step", label: "Step" },
|
|
291
|
-
{ key: "state", label: "State", format: "badge" },
|
|
292
|
-
],
|
|
293
|
-
rows: steps.map((s) => ({ step: s.title, state: BADGE[s.state] })),
|
|
294
|
-
},
|
|
295
|
-
...steps.flatMap((s) => [{ type: "divider" }, ...stepBlocks(s, f)]),
|
|
285
|
+
...stepBlocks(step, f),
|
|
286
|
+
...(step.state === "done" ? [] : [{ type: "actions", elements: [{ type: "button", action_id: "setup_check", value: STEP_TABS[step.key], label: "Check again", style: "secondary" }] }]),
|
|
296
287
|
],
|
|
297
|
-
|
|
298
|
-
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Where Connection opens: the first step not done, or null once every required one is. Pure. */
|
|
292
|
+
export function nextStep(f: SetupFacts): Step | null {
|
|
293
|
+
return requiredSteps(setupSteps(f)).find((s) => s.state !== "done") ?? null;
|
|
299
294
|
}
|
package/src/staff.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { adminConfigured, adminScopes, editTags, registerWebhooks, resync, webhookSubscriptions, WEBHOOK_TOPICS } from "./admin-api.js";
|
|
8
8
|
import { applyEntryRule, describe, entryRuleFor, levelsFor, membership, passes, requirement, termTree, type Terms } from "./access.js";
|
|
9
|
-
import { PROVIDER_ID, type RedeemIDConfig } from "./config.js";
|
|
9
|
+
import { PROVIDER_ID, shopOrigin, type RedeemIDConfig } from "./config.js";
|
|
10
10
|
import { levelChange } from "./events.js";
|
|
11
11
|
import { featuresFor } from "./features.js";
|
|
12
12
|
import { record } from "./log.js";
|
|
@@ -32,76 +32,157 @@ function consentLabel(state: string | null | undefined): string {
|
|
|
32
32
|
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/** The member page's tabs, in order. */
|
|
36
|
+
export const MEMBER_TABS = ["membership", "tags", "account", "privacy"] as const;
|
|
37
|
+
export type MemberTab = (typeof MEMBER_TABS)[number];
|
|
38
|
+
|
|
39
|
+
/** When, to the minute, in UTC: "2026-11-13 18:59 UTC". */
|
|
40
|
+
const stamp = (iso: string | null | undefined) => (iso ? `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`.trim() : "");
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One member: who they are and where they stand at the top, then tabs for
|
|
44
|
+
* their membership, their Shopify tags, their account and privacy.
|
|
45
|
+
*/
|
|
46
|
+
export function memberDetail(config: RedeemIDConfig, m: MemberRecord, toast?: Blocks["toast"], tab: MemberTab = "membership", exported?: unknown): Blocks {
|
|
36
47
|
const levels = config.levels ?? [];
|
|
37
48
|
const { current, past } = membership(m, levels);
|
|
38
49
|
const features = featuresFor(m, config.features, levels);
|
|
39
50
|
const featureNames = (config.features ?? []).filter((f) => features.includes(f.slug)).map((f) => f.name);
|
|
40
51
|
const name = [m.firstName, m.lastName].filter(Boolean).join(" ") || m.email || `Customer ${m.customerId}`;
|
|
52
|
+
let shop: string | null = null;
|
|
53
|
+
try {
|
|
54
|
+
const origin = new URL(shopOrigin(config));
|
|
55
|
+
if (origin.hostname.endsWith(".myshopify.com")) shop = origin.origin;
|
|
56
|
+
} catch {
|
|
57
|
+
shop = null;
|
|
58
|
+
}
|
|
59
|
+
const admin = adminConfigured();
|
|
60
|
+
|
|
61
|
+
const membershipTab = [
|
|
62
|
+
{
|
|
63
|
+
type: "fields",
|
|
64
|
+
fields: [
|
|
65
|
+
{ label: "Levels now", value: current.map((l) => `${l.name} (since ${date(l.since)})`).join(", ") || "None" },
|
|
66
|
+
{ label: "Features", value: featureNames.join(", ") || "None" },
|
|
67
|
+
{ label: "Member since (Shopify)", value: m.dates?.since ?? "" },
|
|
68
|
+
{ label: m.dates?.ends ? "Ends on (Shopify)" : "Renews on (Shopify)", value: m.dates?.ends ?? m.dates?.renews ?? "" },
|
|
69
|
+
{ label: "Gift until (Shopify)", value: m.dates?.gift ?? "" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{ type: "header", text: "Level history" },
|
|
73
|
+
past.length
|
|
74
|
+
? {
|
|
75
|
+
type: "table",
|
|
76
|
+
page_action_id: "member_levels",
|
|
77
|
+
columns: [
|
|
78
|
+
{ key: "level", label: "Level" },
|
|
79
|
+
{ key: "from", label: "From" },
|
|
80
|
+
{ key: "to", label: "To" },
|
|
81
|
+
],
|
|
82
|
+
rows: past.map((l) => ({ level: l.name, from: date(l.from), to: date(l.to) })),
|
|
83
|
+
}
|
|
84
|
+
: { type: "context", text: "No earlier levels." },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const tagsTab = [
|
|
88
|
+
{ type: "context", text: "Levels come only from these tags. Edits are written to Shopify, then read back, so Shopify stays the only source of access." },
|
|
89
|
+
...(admin
|
|
90
|
+
? [
|
|
91
|
+
{
|
|
92
|
+
type: "redeemid_modal",
|
|
93
|
+
block_id: `modal:tags:${m.customerId}`,
|
|
94
|
+
title: "Add tags in Shopify",
|
|
95
|
+
description: "Adds the tags to the customer in Shopify, then reads the customer back. Access follows.",
|
|
96
|
+
trigger: { label: "Add tags", style: "primary" },
|
|
97
|
+
blocks: [
|
|
98
|
+
{
|
|
99
|
+
type: "form",
|
|
100
|
+
block_id: `tags:${m.customerId}`,
|
|
101
|
+
fields: [{ type: "text_input", action_id: "tags", label: "Tags (comma separated)", placeholder: "gold-member" }],
|
|
102
|
+
submit: { label: "Add in Shopify", action_id: "member_tag_add" },
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
: [{ type: "context", text: "Set the Admin API app's client ID to edit tags and resync from here." }]),
|
|
108
|
+
m.tags.length
|
|
109
|
+
? {
|
|
110
|
+
type: "table",
|
|
111
|
+
page_action_id: "member_tags",
|
|
112
|
+
columns: [
|
|
113
|
+
{ key: "tag", label: "Tag", format: "code" },
|
|
114
|
+
{ key: "since", label: "Since" },
|
|
115
|
+
{ key: "remove", label: "", format: "element" },
|
|
116
|
+
],
|
|
117
|
+
rows: m.tags.map((t) => ({
|
|
118
|
+
tag: t,
|
|
119
|
+
since: date(m.tagsSince?.[t.toLowerCase()]),
|
|
120
|
+
remove: admin
|
|
121
|
+
? {
|
|
122
|
+
type: "button",
|
|
123
|
+
action_id: "member_tag_remove",
|
|
124
|
+
label: "Remove",
|
|
125
|
+
style: "secondary",
|
|
126
|
+
value: { customerId: m.customerId, tag: t },
|
|
127
|
+
confirm: { title: "Remove this tag in Shopify?", text: `Removes "${t}" from the customer in Shopify. Access follows.`, confirm: "Remove", deny: "Cancel", style: "danger" },
|
|
128
|
+
}
|
|
129
|
+
: null,
|
|
130
|
+
})),
|
|
131
|
+
}
|
|
132
|
+
: { type: "empty", title: "No tags", description: "This customer has no tags in Shopify, so no level.", size: "sm" },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const accountTab = [
|
|
136
|
+
{
|
|
137
|
+
type: "fields",
|
|
138
|
+
fields: [
|
|
139
|
+
{ label: "Email", value: m.email ?? "" },
|
|
140
|
+
{ label: "Shopify customer ID", value: m.customerId },
|
|
141
|
+
{ label: "Account", value: m.status === "enabled" ? "On" : "Turned off in Shopify" },
|
|
142
|
+
{ label: "Email verified", value: m.emailVerified === undefined ? "" : m.emailVerified ? "Yes" : "No" },
|
|
143
|
+
{ label: "Email marketing", value: consentLabel(m.emailMarketing) },
|
|
144
|
+
{ label: "SMS marketing", value: consentLabel(m.smsMarketing) },
|
|
145
|
+
{ label: "Last synced", value: stamp(m.syncedAt) },
|
|
146
|
+
{ label: "Synced by", value: m.source || "unknown" },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{ type: "divider" },
|
|
150
|
+
...linkSection(m),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const privacyTab = [...privacySection(m.customerId), ...(exported === undefined ? [] : [{ type: "header", text: "Export" }, { type: "code", language: "jsonc", code: JSON.stringify(exported, null, 2) }])];
|
|
154
|
+
|
|
41
155
|
return {
|
|
42
156
|
toast,
|
|
43
157
|
blocks: [
|
|
44
|
-
{
|
|
158
|
+
{
|
|
159
|
+
type: "actions",
|
|
160
|
+
elements: [
|
|
161
|
+
{ type: "button", action_id: "members_back", label: "\u2190 All members", style: "secondary" },
|
|
162
|
+
...(admin ? [{ type: "button", action_id: "member_resync", label: "Resync from Shopify", style: "primary", value: m.customerId }] : []),
|
|
163
|
+
...(shop ? [{ type: "link", label: "Open in Shopify", target: { kind: "external", url: `${shop}/admin/customers/${m.customerId}` }, appearance: "secondary" }] : []),
|
|
164
|
+
],
|
|
165
|
+
},
|
|
45
166
|
{ type: "header", text: name },
|
|
167
|
+
{ type: "context", text: [m.email, `Shopify customer ${m.customerId}`, m.userId ? `signed in here since ${date(m.linkedAt)}` : "hasn't signed in here yet"].filter(Boolean).join(" \u00b7 ") },
|
|
46
168
|
{
|
|
47
|
-
type: "
|
|
48
|
-
|
|
49
|
-
{ label: "
|
|
50
|
-
{ label: "
|
|
51
|
-
{ label: "
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{ label: "
|
|
59
|
-
{ label: "
|
|
60
|
-
{ label: "
|
|
61
|
-
{ label: "
|
|
62
|
-
{ label: "Last synced", value: `${m.syncedAt} (${m.source || "unknown"})` },
|
|
169
|
+
type: "stats",
|
|
170
|
+
items: [
|
|
171
|
+
{ label: "Account", value: m.status === "enabled" ? "On" : "Off" },
|
|
172
|
+
{ label: "Levels", value: current.map((l) => l.name).join(", ") || "None" },
|
|
173
|
+
{ label: "Tags", value: String(m.tags.length) },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: "tab",
|
|
178
|
+
default_tab: MEMBER_TABS.indexOf(tab),
|
|
179
|
+
panels: [
|
|
180
|
+
{ label: "Membership", blocks: membershipTab },
|
|
181
|
+
{ label: "Tags", blocks: tagsTab },
|
|
182
|
+
{ label: "Account", blocks: accountTab },
|
|
183
|
+
{ label: "Privacy", blocks: privacyTab },
|
|
63
184
|
],
|
|
64
185
|
},
|
|
65
|
-
{ type: "header", text: "Shopify tags" },
|
|
66
|
-
m.tags.length
|
|
67
|
-
? {
|
|
68
|
-
type: "table",
|
|
69
|
-
page_action_id: "member_tags",
|
|
70
|
-
columns: [
|
|
71
|
-
{ key: "tag", label: "Tag" },
|
|
72
|
-
{ key: "since", label: "Since" },
|
|
73
|
-
{ key: "remove", label: "", format: "element" },
|
|
74
|
-
],
|
|
75
|
-
rows: m.tags.map((t) => ({
|
|
76
|
-
tag: t,
|
|
77
|
-
since: date(m.tagsSince?.[t.toLowerCase()]),
|
|
78
|
-
remove: adminConfigured()
|
|
79
|
-
? {
|
|
80
|
-
type: "button",
|
|
81
|
-
action_id: "member_tag_remove",
|
|
82
|
-
label: "Remove in Shopify",
|
|
83
|
-
style: "danger",
|
|
84
|
-
value: { customerId: m.customerId, tag: t },
|
|
85
|
-
confirm: { title: "Remove this tag in Shopify?", text: `Removes "${t}" from the customer in Shopify. Access follows.`, confirm: "Remove", deny: "Cancel", style: "danger" },
|
|
86
|
-
}
|
|
87
|
-
: null,
|
|
88
|
-
})),
|
|
89
|
-
}
|
|
90
|
-
: { type: "context", text: "No tags." },
|
|
91
|
-
adminConfigured()
|
|
92
|
-
? {
|
|
93
|
-
type: "form",
|
|
94
|
-
block_id: `tags:${m.customerId}`,
|
|
95
|
-
fields: [{ type: "text_input", action_id: "tags", label: "Add tags in Shopify (comma separated)", placeholder: "gold-member" }],
|
|
96
|
-
submit: { label: "Add in Shopify", action_id: "member_tag_add" },
|
|
97
|
-
}
|
|
98
|
-
: { type: "context", text: "Set the Admin API app's client ID to edit tags and resync from here." },
|
|
99
|
-
adminConfigured()
|
|
100
|
-
? { type: "actions", elements: [{ type: "button", action_id: "member_resync", label: "Resync from Shopify", style: "primary", value: m.customerId }] }
|
|
101
|
-
: { type: "divider" },
|
|
102
|
-
{ type: "context", text: "Tags are written to Shopify, then read back. Shopify stays the only source of access." },
|
|
103
|
-
...linkSection(m),
|
|
104
|
-
...privacySection(m.customerId),
|
|
105
186
|
],
|
|
106
187
|
};
|
|
107
188
|
}
|
|
@@ -228,17 +309,18 @@ function privacySection(customerId: string): unknown[] {
|
|
|
228
309
|
]
|
|
229
310
|
: [];
|
|
230
311
|
return [
|
|
231
|
-
{ type: "header", text: "Privacy" },
|
|
232
|
-
{ type: "actions", elements: [{ type: "button", action_id: "member_export", label: "Export data", style: "secondary", value: customerId }, erase, ...shopify] },
|
|
233
312
|
{ type: "context", text: "Export shows everything RedeemID keeps about this member. Erasing removes it from the site; Shopify stays the record of the customer." },
|
|
313
|
+
{ type: "actions", elements: [{ type: "button", action_id: "member_export", label: "Export data", style: "secondary", value: customerId }, erase] },
|
|
314
|
+
...(shopify.length
|
|
315
|
+
? [
|
|
316
|
+
{ type: "header", text: "In Shopify" },
|
|
317
|
+
{ type: "context", text: "Shopify erases the customer's personal data across the store, orders included, after its waiting period. Cancel stops a request that hasn't run yet." },
|
|
318
|
+
{ type: "actions", elements: shopify },
|
|
319
|
+
]
|
|
320
|
+
: []),
|
|
234
321
|
];
|
|
235
322
|
}
|
|
236
323
|
|
|
237
|
-
/** The member's export, as JSON, under their detail page. */
|
|
238
|
-
export function exportBlocks(detail: Blocks, data: unknown): Blocks {
|
|
239
|
-
return { ...detail, blocks: [...detail.blocks, { type: "header", text: "Export" }, { type: "code", language: "jsonc", code: JSON.stringify(data, null, 2) }] };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
324
|
/** Every privacy request: what came in, from where, and what's still open. */
|
|
243
325
|
export async function privacyPage(s: Stores, toast?: Blocks["toast"], extra: unknown[] = []): Promise<Blocks> {
|
|
244
326
|
const items = await recentRequests(s);
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.0.
|
|
1
|
+
export const VERSION = "0.0.32";
|