@marktiderman/genesis-notifications 0.0.0-stage → 0.1.0

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -2
  3. package/dist/blocks.d.ts +10 -0
  4. package/dist/blocks.js +35 -0
  5. package/dist/blocks.js.map +1 -0
  6. package/dist/chunk-2BG336II.js +124 -0
  7. package/dist/chunk-2BG336II.js.map +1 -0
  8. package/dist/chunk-7PHA3WVG.js +51 -0
  9. package/dist/chunk-7PHA3WVG.js.map +1 -0
  10. package/dist/chunk-EWZB35V6.js +113 -0
  11. package/dist/chunk-EWZB35V6.js.map +1 -0
  12. package/dist/chunk-UXNWI7OS.js +118 -0
  13. package/dist/chunk-UXNWI7OS.js.map +1 -0
  14. package/dist/email-render-CID_pXDy.d.ts +53 -0
  15. package/dist/email.d.ts +46 -0
  16. package/dist/email.js +64 -0
  17. package/dist/email.js.map +1 -0
  18. package/dist/index.d.ts +26 -0
  19. package/dist/index.js +151 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/native.d.ts +92 -0
  22. package/dist/native.js +170 -0
  23. package/dist/native.js.map +1 -0
  24. package/dist/react.d.ts +98 -0
  25. package/dist/react.js +4 -0
  26. package/dist/react.js.map +1 -0
  27. package/dist/runner.d.ts +174 -0
  28. package/dist/runner.js +3 -0
  29. package/dist/runner.js.map +1 -0
  30. package/dist/server.d.ts +658 -0
  31. package/dist/server.js +995 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/supabase.d.ts +118 -0
  34. package/dist/supabase.js +3 -0
  35. package/dist/supabase.js.map +1 -0
  36. package/dist/types-BilguCPW.d.ts +332 -0
  37. package/dist/web.d.ts +79 -0
  38. package/dist/web.js +233 -0
  39. package/dist/web.js.map +1 -0
  40. package/functions/genesis-notifications/index.ts +60 -0
  41. package/functions/genesis-notifications/notifications.ts +18 -0
  42. package/migrations/0001_genesis_notifications.sql +2057 -0
  43. package/migrations/tests/0001_genesis_notifications.test.sql +473 -0
  44. package/package.json +191 -3
  45. package/src/blocks.ts +42 -0
  46. package/src/core/index.ts +43 -0
  47. package/src/core/registry.ts +354 -0
  48. package/src/core/types.ts +90 -0
  49. package/src/core/validate.ts +73 -0
  50. package/src/email/index.tsx +142 -0
  51. package/src/index.ts +6 -0
  52. package/src/native/index.tsx +283 -0
  53. package/src/react/index.tsx +226 -0
  54. package/src/runner/index.ts +299 -0
  55. package/src/server/crypto.ts +44 -0
  56. package/src/server/dns.ts +106 -0
  57. package/src/server/email-render.ts +90 -0
  58. package/src/server/errors.ts +61 -0
  59. package/src/server/expo.ts +133 -0
  60. package/src/server/handler.ts +127 -0
  61. package/src/server/index.ts +57 -0
  62. package/src/server/notify.ts +100 -0
  63. package/src/server/rate.ts +47 -0
  64. package/src/server/resend.ts +143 -0
  65. package/src/server/runner.ts +524 -0
  66. package/src/server/secret-links.ts +53 -0
  67. package/src/server/store.ts +150 -0
  68. package/src/server/unsubscribe.ts +78 -0
  69. package/src/server/webhook.ts +55 -0
  70. package/src/supabase/index.ts +210 -0
  71. package/src/web/index.tsx +428 -0
package/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ export { SecretInDataError, assertNoSecrets, recipientsToJson } from './chunk-7PHA3WVG.js';
2
+
3
+ // src/core/registry.ts
4
+ var CHANNELS = ["in_app", "push", "email"];
5
+ var KEY_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
6
+ var IOS_ID_PATTERN = /^[A-Za-z0-9_]{1,64}$/;
7
+ var NotificationRegistryError = class extends Error {
8
+ constructor(problems) {
9
+ super(`Invalid notifications:
10
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
11
+ this.problems = problems;
12
+ this.name = "NotificationRegistryError";
13
+ }
14
+ problems;
15
+ };
16
+ var MAX_EMAIL_DELAY_SECONDS = 7 * 24 * 3600;
17
+ function defineNotifications(input) {
18
+ const problems = [];
19
+ const categories = input.categories;
20
+ const actions = input.actions ?? {};
21
+ const types = input.types;
22
+ const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
23
+ for (const [id, c] of Object.entries(categories)) {
24
+ if (!KEY_PATTERN.test(id)) problems.push(`category "${id}": an id is lower case letters, digits, ".", "_" or "-"`);
25
+ if (!c.label?.trim()) problems.push(`category "${id}": needs a label`);
26
+ }
27
+ for (const [id, set] of Object.entries(actions)) {
28
+ if (!IOS_ID_PATTERN.test(id)) problems.push(`actions "${id}": a device category id is letters, digits or "_" (no ":" or "-")`);
29
+ const seen = /* @__PURE__ */ new Set();
30
+ for (const a of set.actions) {
31
+ if (!IOS_ID_PATTERN.test(a.id)) problems.push(`actions "${id}": action "${a.id}" is letters, digits or "_"`);
32
+ if (seen.has(a.id)) problems.push(`actions "${id}": action "${a.id}" appears twice`);
33
+ seen.add(a.id);
34
+ }
35
+ if (set.actions.length === 0 || set.actions.length > 4) problems.push(`actions "${id}": one to four actions`);
36
+ }
37
+ for (const [key, t] of Object.entries(types)) {
38
+ if (!KEY_PATTERN.test(key)) problems.push(`type "${key}": a key is lower case letters, digits, ".", "_" or "-"`);
39
+ if (!own(categories, t.category)) problems.push(`type "${key}": unknown category "${String(t.category)}"`);
40
+ const offered = Object.keys(t.channels ?? {});
41
+ if (offered.length === 0) problems.push(`type "${key}": offers no channel`);
42
+ for (const ch of offered) if (!CHANNELS.includes(ch)) problems.push(`type "${key}": unknown channel "${ch}"`);
43
+ if (t.actions !== void 0 && !own(actions, t.actions)) problems.push(`type "${key}": unknown actions "${String(t.actions)}"`);
44
+ const delay = t.emailDelaySeconds ?? 0;
45
+ if (!Number.isInteger(delay) || delay < 0 || delay > MAX_EMAIL_DELAY_SECONDS) {
46
+ problems.push(`type "${key}": emailDelaySeconds is a whole number from 0 to ${MAX_EMAIL_DELAY_SECONDS}`);
47
+ }
48
+ if (delay > 0 && t.channels?.email === void 0) problems.push(`type "${key}": emailDelaySeconds needs the email channel`);
49
+ if (delay > 0 && t.channels?.in_app === void 0) problems.push(`type "${key}": an email delay skips the mail once read in the app, so it needs the in_app channel`);
50
+ if (t.secretLink && t.channels?.email === void 0) problems.push(`type "${key}": secretLink is for email, and it offers no email channel`);
51
+ if (typeof t.render !== "function") problems.push(`type "${key}": needs a render function`);
52
+ }
53
+ if (problems.length) throw new NotificationRegistryError(problems);
54
+ const hiddenPreview = input.hiddenPreview ?? { title: "New notification" };
55
+ const typeOf = (key) => {
56
+ if (!own(types, key)) throw new NotificationRegistryError([`unknown notification type "${key}"`]);
57
+ return types[key];
58
+ };
59
+ const isCritical = (key) => {
60
+ const t = typeOf(key);
61
+ return Boolean(t.critical || categories[t.category]?.critical);
62
+ };
63
+ return {
64
+ categories,
65
+ actions,
66
+ types,
67
+ hiddenPreview,
68
+ type: typeOf,
69
+ has: (key) => own(types, key),
70
+ isCritical,
71
+ render(key, data) {
72
+ try {
73
+ const out = typeOf(key).render(data ?? {});
74
+ return {
75
+ title: typeof out?.title === "string" && out.title ? out.title : key,
76
+ ...typeof out?.body === "string" ? { body: out.body } : {},
77
+ ...typeof out?.url === "string" && out.url ? { url: out.url } : {},
78
+ ...out?.push && typeof out.push === "object" ? { push: out.push } : {}
79
+ };
80
+ } catch {
81
+ return { title: key };
82
+ }
83
+ },
84
+ toTypeRows() {
85
+ return Object.entries(types).map(([key, t]) => ({
86
+ key,
87
+ category: t.category,
88
+ channels: { ...t.channels },
89
+ critical: isCritical(key),
90
+ transactional: Boolean(t.transactional),
91
+ email_delay_seconds: t.emailDelaySeconds ?? 0,
92
+ secret_link: Boolean(t.secretLink),
93
+ push_privacy: t.pushPrivacy ?? "default",
94
+ ios_category: t.actions ?? null
95
+ }));
96
+ },
97
+ matrix() {
98
+ const rows = [];
99
+ for (const [id, c] of Object.entries(categories)) {
100
+ const defaults = {};
101
+ let critical = Boolean(c.critical);
102
+ for (const [key, t] of Object.entries(types)) {
103
+ if (t.category !== id) continue;
104
+ for (const [ch, on] of Object.entries(t.channels)) defaults[ch] = Boolean(defaults[ch] || on);
105
+ if (isCritical(key)) critical = true;
106
+ }
107
+ const channels = CHANNELS.filter((ch) => ch in defaults);
108
+ if (channels.length) rows.push({ id, label: c.label, ...c.description ? { description: c.description } : {}, critical, channels, defaults });
109
+ }
110
+ return rows;
111
+ },
112
+ deviceCategories() {
113
+ return Object.entries(actions).map(([identifier, set]) => ({
114
+ identifier,
115
+ actions: [...set.actions],
116
+ ...set.previewPlaceholder ? { previewPlaceholder: set.previewPlaceholder } : {}
117
+ }));
118
+ },
119
+ androidChannels() {
120
+ return Object.entries(categories).map(([id, c]) => ({
121
+ id,
122
+ name: c.android?.name ?? c.label,
123
+ ...c.description ? { description: c.description } : {},
124
+ importance: c.android?.importance ?? "default"
125
+ }));
126
+ }
127
+ };
128
+ }
129
+ var MESSAGE_ACTIONS = {
130
+ actions: [
131
+ { id: "reply", title: "Reply", textInput: { placeholder: "Message", submitTitle: "Send" } },
132
+ { id: "mark_read", title: "Mark as read" }
133
+ ]
134
+ };
135
+ var GENESIS_TEST_CATEGORY = { genesis: { label: "Tests", description: "Test notifications an admin sends themself." } };
136
+ var GENESIS_TEST_TYPE = {
137
+ "genesis.test": {
138
+ category: "genesis",
139
+ channels: { in_app: true, push: true, email: true },
140
+ critical: true,
141
+ transactional: true,
142
+ render: () => ({ title: "Test notification", body: "If you can read this, this channel works." })
143
+ }
144
+ };
145
+
146
+ // src/core/types.ts
147
+ var DELIVERY_STATUSES = ["pending", "sending", "sent", "delivered", "failed", "skipped", "cancelled"];
148
+
149
+ export { CHANNELS, DELIVERY_STATUSES, GENESIS_TEST_CATEGORY, GENESIS_TEST_TYPE, IOS_ID_PATTERN, KEY_PATTERN, MAX_EMAIL_DELAY_SECONDS, MESSAGE_ACTIONS, NotificationRegistryError, defineNotifications };
150
+ //# sourceMappingURL=index.js.map
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/registry.ts","../src/core/types.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `defineNotifications`: the one list of what an app notifies people about,\n * as `defineFeatures` is the list of what it does. Each type names its\n * category, the channels it may use (and which are on by default), and two\n * flags: `critical` (preferences can't turn it off) and `transactional` (no\n * unsubscribe link, no quiet hours, no digest). The registry also defines the\n * device notification categories (iOS action buttons) and the Android\n * channels the app makes on the phone.\n *\n * It is plain data plus small render functions, so the browser, the phone\n * and the server import the same list. `toTypeRows()` is what is mirrored\n * into the database's `notification_types`.\n */\n\n/** Where a notification can go. `in_app` is the feed. */\nexport const CHANNELS = [\"in_app\", \"push\", \"email\"] as const;\nexport type Channel = (typeof CHANNELS)[number];\n\n/** Lower case letters, digits, \".\", \"_\" or \"-\", up to 64 characters: type keys, category ids. */\nexport const KEY_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;\n/** Device category and action ids: Expo asks for no \":\" or \"-\". */\nexport const IOS_ID_PATTERN = /^[A-Za-z0-9_]{1,64}$/;\n\n/** Android channel importance (Expo's `AndroidImportance`). */\nexport type ChannelImportance = \"min\" | \"low\" | \"default\" | \"high\" | \"max\";\n\nexport interface NotificationCategoryDef {\n /** What a person sees in their preferences (\"Messages\"). */\n label: string;\n description?: string;\n /** Preferences can't turn this category off (account security, say). */\n critical?: boolean;\n /** The Android channel made for it (its id is the category id). Default importance `\"default\"`. */\n android?: { importance?: ChannelImportance; name?: string };\n}\n\n/** An action button on a notification. */\nexport interface NotificationActionDef {\n id: string;\n title: string;\n /** A reply field instead of a plain button. */\n textInput?: { placeholder?: string; submitTitle?: string };\n /** Opens the app (default false: the action is handled in the background). */\n opensApp?: boolean;\n destructive?: boolean;\n /** Needs the phone unlocked. */\n requiresAuth?: boolean;\n}\n\n/** A notification category on the device (iOS's word): the actions a notification shows. */\nexport interface NotificationActionSetDef {\n actions: readonly NotificationActionDef[];\n /** Shown when the person has hidden previews for the app. */\n previewPlaceholder?: string;\n}\n\n/** What a notification says, made from its `data`. */\nexport interface RenderedText {\n title: string;\n body?: string;\n /** The app path a tap opens (`/messages/42`). */\n url?: string;\n /**\n * Push-only extras, for a type whose app reads more than the link: `ids` always travel\n * (a Reply action needs the conversation and message), `display` only when previews are\n * shown (a sender's name and photo for iOS rich push), and `mutableContent` lets the app's\n * Notification Service Extension run (dropped with hidden previews). Flat values only; the\n * notifier's own keys (`notification_id`, `type`, `url`, `thread_key`) can't be replaced.\n */\n push?: {\n ids?: Record<string, string | number>;\n display?: Record<string, string | number>;\n mutableContent?: boolean;\n };\n}\n\n/** A notification's `data`: references and display values, never a secret. */\nexport type NotificationData = Record<string, unknown>;\n\nexport interface NotificationTypeDef<C extends string = string, A extends string = string> {\n category: C;\n /**\n * The channels this type may use, and whether each is on for someone who\n * never chose: `{ in_app: true, push: true, email: false }` offers email\n * as an opt-in. A channel left out is never used.\n */\n channels: Partial<Record<Channel, boolean>>;\n /** Preferences can't turn it off: each channel it offers stays as defaulted. */\n critical?: boolean;\n /**\n * Mail the person needs for something they did (a receipt, a reset): no\n * unsubscribe link, never held by quiet hours or folded into a digest.\n * Rate caps are the domain's.\n */\n transactional?: boolean;\n /**\n * Wait this long before emailing, and skip the email if the person read\n * the notification in the app meanwhile. Default 0 (send at once).\n */\n emailDelaySeconds?: number;\n /** The device category (a key of `actions`) its pushes carry. */\n actions?: A;\n /** `\"generic\"` always sends neutral push text, whatever the person chose. */\n pushPrivacy?: \"default\" | \"generic\";\n /**\n * The email holds a secret link (a one-time token). It is never stored in\n * `data` or pushed: the server builds it at send time with\n * `resolveSecretLink(type, ref)`.\n */\n secretLink?: boolean;\n /** What the feed and the push say. */\n render: (data: NotificationData) => RenderedText;\n}\n\nexport interface NotificationsInput<\n C extends Record<string, NotificationCategoryDef>,\n A extends Record<string, NotificationActionSetDef>,\n T extends Record<string, NotificationTypeDef<Extract<keyof C, string>, Extract<keyof A, string>>>,\n> {\n categories: C;\n actions?: A;\n types: T;\n /** Neutral text for hidden previews. Default \"New notification\". */\n hiddenPreview?: { title: string; body?: string };\n}\n\n/** One row of `notification_types`, as `genesis_notifications_sync_types` takes it. */\nexport interface NotificationTypeRow {\n key: string;\n category: string;\n channels: Partial<Record<Channel, boolean>>;\n critical: boolean;\n transactional: boolean;\n email_delay_seconds: number;\n secret_link: boolean;\n push_privacy: \"default\" | \"generic\";\n ios_category: string | null;\n}\n\nexport interface MatrixRow {\n id: string;\n label: string;\n description?: string;\n critical: boolean;\n /** The channels some type in the category offers, in `CHANNELS` order. */\n channels: Channel[];\n /** Each offered channel's default: on when any type in the category has it on. */\n defaults: Partial<Record<Channel, boolean>>;\n}\n\nexport interface NotificationRegistry<\n C extends Record<string, NotificationCategoryDef> = Record<string, NotificationCategoryDef>,\n A extends Record<string, NotificationActionSetDef> = Record<string, NotificationActionSetDef>,\n T extends Record<string, NotificationTypeDef> = Record<string, NotificationTypeDef>,\n> {\n readonly categories: C;\n readonly actions: A;\n readonly types: T;\n readonly hiddenPreview: { title: string; body?: string };\n /** The type, or throws: an unknown key is a bug, never dropped quietly. */\n type(key: string): NotificationTypeDef;\n has(key: string): boolean;\n /** Preferences can't turn it off: a critical type, or a type in a critical category. */\n isCritical(key: string): boolean;\n /** What the feed and the push say. A render that throws gives the type's key as the title. */\n render(key: string, data: NotificationData): RenderedText;\n /** The rows mirrored into `notification_types`. */\n toTypeRows(): NotificationTypeRow[];\n /** For a preferences matrix: each category with the channels its types offer. */\n matrix(): MatrixRow[];\n /** The device categories to register (`setNotificationCategoryAsync`). */\n deviceCategories(): { identifier: string; actions: NotificationActionDef[]; previewPlaceholder?: string }[];\n /** The Android channels, one per category. */\n androidChannels(): { id: string; name: string; description?: string; importance: ChannelImportance }[];\n}\n\n/** Thrown by `defineNotifications`; lists every problem at once. */\nexport class NotificationRegistryError extends Error {\n constructor(readonly problems: string[]) {\n super(`Invalid notifications:\\n${problems.map((p) => ` - ${p}`).join(\"\\n\")}`);\n this.name = \"NotificationRegistryError\";\n }\n}\n\n/** The longest email delay: a week. */\nexport const MAX_EMAIL_DELAY_SECONDS = 7 * 24 * 3600;\n\n/**\n * The notification registry.\n *\n * ```ts\n * export const notifications = defineNotifications({\n * categories: { messages: { label: \"Messages\", android: { importance: \"high\" } } },\n * actions: { message: { actions: [{ id: \"reply\", title: \"Reply\", textInput: {} }, { id: \"mark_read\", title: \"Mark as read\" }] } },\n * types: {\n * \"chat.message\": {\n * category: \"messages\",\n * channels: { in_app: true, push: true, email: true },\n * emailDelaySeconds: 600,\n * actions: \"message\",\n * render: (d) => ({ title: String(d.sender_name), body: String(d.preview), url: `/messages/${d.conversation_id}` }),\n * },\n * },\n * });\n * ```\n *\n * @stability Beta\n */\nexport function defineNotifications<\n const C extends Record<string, NotificationCategoryDef>,\n const A extends Record<string, NotificationActionSetDef>,\n const T extends Record<string, NotificationTypeDef<Extract<keyof C, string>, Extract<keyof A, string>>>,\n>(input: NotificationsInput<C, A, T>): NotificationRegistry<C, A, T> {\n const problems: string[] = [];\n const categories = input.categories as C & Record<string, NotificationCategoryDef>;\n const actions = (input.actions ?? {}) as A & Record<string, NotificationActionSetDef>;\n const types = input.types as T & Record<string, NotificationTypeDef>;\n const own = (o: object, k: string) => Object.prototype.hasOwnProperty.call(o, k);\n\n for (const [id, c] of Object.entries(categories)) {\n if (!KEY_PATTERN.test(id)) problems.push(`category \"${id}\": an id is lower case letters, digits, \".\", \"_\" or \"-\"`);\n if (!c.label?.trim()) problems.push(`category \"${id}\": needs a label`);\n }\n for (const [id, set] of Object.entries(actions)) {\n if (!IOS_ID_PATTERN.test(id)) problems.push(`actions \"${id}\": a device category id is letters, digits or \"_\" (no \":\" or \"-\")`);\n const seen = new Set<string>();\n for (const a of set.actions) {\n if (!IOS_ID_PATTERN.test(a.id)) problems.push(`actions \"${id}\": action \"${a.id}\" is letters, digits or \"_\"`);\n if (seen.has(a.id)) problems.push(`actions \"${id}\": action \"${a.id}\" appears twice`);\n seen.add(a.id);\n }\n if (set.actions.length === 0 || set.actions.length > 4) problems.push(`actions \"${id}\": one to four actions`);\n }\n for (const [key, t] of Object.entries(types)) {\n if (!KEY_PATTERN.test(key)) problems.push(`type \"${key}\": a key is lower case letters, digits, \".\", \"_\" or \"-\"`);\n if (!own(categories, t.category)) problems.push(`type \"${key}\": unknown category \"${String(t.category)}\"`);\n const offered = Object.keys(t.channels ?? {});\n if (offered.length === 0) problems.push(`type \"${key}\": offers no channel`);\n for (const ch of offered) if (!(CHANNELS as readonly string[]).includes(ch)) problems.push(`type \"${key}\": unknown channel \"${ch}\"`);\n if (t.actions !== undefined && !own(actions, t.actions)) problems.push(`type \"${key}\": unknown actions \"${String(t.actions)}\"`);\n const delay = t.emailDelaySeconds ?? 0;\n if (!Number.isInteger(delay) || delay < 0 || delay > MAX_EMAIL_DELAY_SECONDS) {\n problems.push(`type \"${key}\": emailDelaySeconds is a whole number from 0 to ${MAX_EMAIL_DELAY_SECONDS}`);\n }\n if (delay > 0 && t.channels?.email === undefined) problems.push(`type \"${key}\": emailDelaySeconds needs the email channel`);\n if (delay > 0 && t.channels?.in_app === undefined) problems.push(`type \"${key}\": an email delay skips the mail once read in the app, so it needs the in_app channel`);\n if (t.secretLink && t.channels?.email === undefined) problems.push(`type \"${key}\": secretLink is for email, and it offers no email channel`);\n if (typeof t.render !== \"function\") problems.push(`type \"${key}\": needs a render function`);\n }\n if (problems.length) throw new NotificationRegistryError(problems);\n\n const hiddenPreview = input.hiddenPreview ?? { title: \"New notification\" };\n const typeOf = (key: string): NotificationTypeDef => {\n if (!own(types, key)) throw new NotificationRegistryError([`unknown notification type \"${key}\"`]);\n return types[key]!;\n };\n const isCritical = (key: string) => {\n const t = typeOf(key);\n return Boolean(t.critical || categories[t.category]?.critical);\n };\n\n return {\n categories,\n actions,\n types,\n hiddenPreview,\n type: typeOf,\n has: (key) => own(types, key),\n isCritical,\n render(key, data) {\n try {\n const out = typeOf(key).render(data ?? {});\n return {\n title: typeof out?.title === \"string\" && out.title ? out.title : key,\n ...(typeof out?.body === \"string\" ? { body: out.body } : {}),\n ...(typeof out?.url === \"string\" && out.url ? { url: out.url } : {}),\n ...(out?.push && typeof out.push === \"object\" ? { push: out.push } : {}),\n };\n } catch {\n return { title: key };\n }\n },\n toTypeRows() {\n return Object.entries(types).map(([key, t]) => ({\n key,\n category: t.category,\n channels: { ...t.channels },\n critical: isCritical(key),\n transactional: Boolean(t.transactional),\n email_delay_seconds: t.emailDelaySeconds ?? 0,\n secret_link: Boolean(t.secretLink),\n push_privacy: t.pushPrivacy ?? \"default\",\n ios_category: t.actions ?? null,\n }));\n },\n matrix() {\n const rows: MatrixRow[] = [];\n for (const [id, c] of Object.entries(categories)) {\n const defaults: Partial<Record<Channel, boolean>> = {};\n let critical = Boolean(c.critical);\n for (const [key, t] of Object.entries(types)) {\n if (t.category !== id) continue;\n for (const [ch, on] of Object.entries(t.channels) as [Channel, boolean][]) defaults[ch] = Boolean(defaults[ch] || on);\n if (isCritical(key)) critical = true;\n }\n const channels = CHANNELS.filter((ch) => ch in defaults);\n if (channels.length) rows.push({ id, label: c.label, ...(c.description ? { description: c.description } : {}), critical, channels, defaults });\n }\n return rows;\n },\n deviceCategories() {\n return Object.entries(actions).map(([identifier, set]) => ({\n identifier,\n actions: [...set.actions],\n ...(set.previewPlaceholder ? { previewPlaceholder: set.previewPlaceholder } : {}),\n }));\n },\n androidChannels() {\n return Object.entries(categories).map(([id, c]) => ({\n id,\n name: c.android?.name ?? c.label,\n ...(c.description ? { description: c.description } : {}),\n importance: c.android?.importance ?? \"default\",\n }));\n },\n };\n}\n\n/**\n * The actions a message notification shows: Reply (a text field) and Mark as\n * read. Put it in `actions` under an id (`message`) and name that id in the\n * message type's `actions`. A tap on either reaches the push block's\n * `onResponse`; `notificationAction(response)` gives the action id, the typed\n * text and the notification id (use it as the reply's client id, so a\n * retried reply stays one reply).\n */\nexport const MESSAGE_ACTIONS = {\n actions: [\n { id: \"reply\", title: \"Reply\", textInput: { placeholder: \"Message\", submitTitle: \"Send\" } },\n { id: \"mark_read\", title: \"Mark as read\" },\n ],\n} as const satisfies NotificationActionSetDef;\n\n/** The category and type of Admin -> Send a test (`genesis.test`, which the migration creates). Spread them into your registry. */\nexport const GENESIS_TEST_CATEGORY = { genesis: { label: \"Tests\", description: \"Test notifications an admin sends themself.\" } } as const satisfies Record<string, NotificationCategoryDef>;\nexport const GENESIS_TEST_TYPE = {\n \"genesis.test\": {\n category: \"genesis\",\n channels: { in_app: true, push: true, email: true },\n critical: true,\n transactional: true,\n render: () => ({ title: \"Test notification\", body: \"If you can read this, this channel works.\" }),\n },\n} as const satisfies Record<string, NotificationTypeDef<\"genesis\">>;\n","/**\n * The shapes the notifier's parts share: what a domain passes to `notify()`,\n * a feed row as the database returns it, and a person's preferences.\n */\nimport type { Channel, NotificationData } from \"./registry\";\n\n/** A recipient with a login, by user id. */\nexport interface UserRecipient {\n userId: string;\n /** Their own app icon badge (an unread count), over the call's `badge`. */\n badge?: number;\n}\n\n/**\n * Someone with no login (an invitee, a form respondent): email only, never a\n * feed. `ref` is the domain's id for them (\"people:42\"), so erasing them\n * finds their rows.\n */\nexport interface EmailRecipient {\n email: string;\n name?: string;\n ref?: string;\n}\n\n/** A user id string, or one of the two shapes. */\nexport type Recipient = string | UserRecipient | EmailRecipient;\n\n/** A brand the caller passes (the tenant's default is used for what it leaves out). */\nexport interface NotificationBrand {\n name?: string;\n logoUrl?: string;\n /** A CSS colour (`#1d4ed8`), for the email's accent. */\n color?: string;\n /** Where a recipient goes from the email's footer. */\n url?: string;\n}\n\n/** What a domain asks for. The notifier decides channel, timing and preferences. */\nexport interface NotifyInput<K extends string = string> {\n type: K;\n recipients: readonly Recipient[];\n /** References and display values the type's render reads. Never a secret or a secret link. */\n data?: NotificationData;\n /** Unique per recipient: the same key again is the same notification, never a second one. */\n idempotencyKey: string;\n /** Groups pushes on the lock screen (iOS `threadId`): a conversation id. */\n threadKey?: string;\n /** A newer one replaces an older one with the same key that hasn't gone out (and on screen). */\n collapseKey?: string;\n /** The app icon badge, as the caller counts it. */\n badge?: number;\n /** The practice, workspace or organization it belongs to: its sender identity and defaults apply. */\n tenantId?: string;\n brand?: NotificationBrand;\n /**\n * A plaintext token that exists only hashed in the domain's tables (an\n * invite). It travels only on the email's send job, encrypted, and is\n * cleared once the email is sent; `resolveSecretLink` receives it.\n */\n secret?: string;\n}\n\n/** One row `notify()` made or found again. */\nexport interface NotifyResult {\n notificationId: string;\n recipientKey: string;\n /** False when the idempotency key had been used for this recipient already. */\n created: boolean;\n}\n\n/** A feed row, as the browser reads it under row level security. */\nexport interface NotificationRow {\n id: string;\n type: string;\n category: string;\n tenant_id: string | null;\n data: NotificationData;\n thread_key: string | null;\n created_at: string;\n seen_at: string | null;\n read_at: string | null;\n archived_at: string | null;\n}\n\n/** Delivery states. */\nexport const DELIVERY_STATUSES = [\"pending\", \"sending\", \"sent\", \"delivered\", \"failed\", \"skipped\", \"cancelled\"] as const;\nexport type DeliveryStatus = (typeof DELIVERY_STATUSES)[number];\n\n/** A person's explicit choices: category id → channel → on. Absent means the default. */\nexport type ChannelChoices = Record<string, Partial<Record<Channel, boolean>>>;\n"]}
@@ -0,0 +1,92 @@
1
+ import * as react from 'react';
2
+ import { NotificationsPageProps } from '@marktiderman/genesis-ui-native/layouts';
3
+ import { CategoriesPartProps, NotificationSettingsProps, PushBackend, PushOptions } from '@marktiderman/genesis-native/push';
4
+ export { NotificationAction, notificationAction } from '@marktiderman/genesis-native/push';
5
+ import { a as Channel, l as NotificationRegistry } from './types-BilguCPW.js';
6
+ import { FeedItem } from './react.js';
7
+ import './supabase.js';
8
+
9
+ declare const CHANNEL_LABELS: Record<Channel, string>;
10
+ /** How many notifications the feed (and its header) reads: one query, shared by both. */
11
+ declare const NOTIFICATIONS_FEED_LIMIT = 100;
12
+ interface NotificationsFeedProps extends Omit<NotificationsPageProps, "items" | "refreshing" | "onRefresh" | "onMarkAllRead"> {
13
+ /** Opens a notification's link (`router.push(item.url)`); it is marked read first. */
14
+ onOpen?: (item: FeedItem) => void;
15
+ limit?: number;
16
+ }
17
+ /**
18
+ * The feed on ui-native's `NotificationsPage`: pull to refresh, unread dots,
19
+ * a tap marks read and opens. Realtime refetches it (NotificationsProvider).
20
+ *
21
+ * @stability Beta
22
+ */
23
+ declare function NotificationsFeed({ onOpen, limit, ...page }: NotificationsFeedProps): react.JSX.Element;
24
+ /**
25
+ * The feed's `Stack.Screen` options, with "Mark all read" while anything is
26
+ * unread. Pass the feed's `limit` if you changed it, so both read one query.
27
+ */
28
+ declare function useNotificationsFeedHeader(options?: {
29
+ headerTitle?: string;
30
+ limit?: number;
31
+ }): Record<string, unknown>;
32
+ /**
33
+ * Category × channel switches (push and email, and the feed when a type
34
+ * offers it as optional), for NotificationSettings' `categories` slot. It
35
+ * ignores the push block's own category switches and reads the notifier's
36
+ * preferences instead.
37
+ *
38
+ * @stability Beta
39
+ */
40
+ declare function ChannelPreferences(_props: Partial<CategoriesPartProps> & {
41
+ channelLabels?: Partial<Record<Channel, string>>;
42
+ }): react.JSX.Element;
43
+ /**
44
+ * Settings → Notifications with the notifier's channel switches: the push
45
+ * block's permission, quiet hours and previews, and `ChannelPreferences` for
46
+ * the categories. Needs the push block and NotificationsProvider.
47
+ *
48
+ * @stability Beta
49
+ */
50
+ declare function NotificationPreferences(props: NotificationSettingsProps): react.JSX.Element;
51
+ /**
52
+ * What the push block needs from the registry: a category per notification
53
+ * category (its Android channel; NotificationSettings shows the categories
54
+ * section only when there are some), and the device categories with their
55
+ * action buttons.
56
+ *
57
+ * @stability Beta
58
+ */
59
+ declare function pushOptionsFrom(registry: NotificationRegistry): Pick<PushOptions, "categories" | "deviceCategories">;
60
+ type Result = {
61
+ data: unknown;
62
+ error: {
63
+ code?: string | null;
64
+ } | null;
65
+ };
66
+ /** The parts of a Supabase client the backend uses. */
67
+ interface PushBackendClientLike {
68
+ rpc: (fn: any, args?: any) => PromiseLike<Result>;
69
+ from(table: string): unknown;
70
+ }
71
+ /** A database refusal, with its SQLSTATE; never a token. */
72
+ declare class NotificationsPushBackendError extends Error {
73
+ readonly operation: string;
74
+ readonly code: string | null;
75
+ constructor(operation: string, code: string | null);
76
+ }
77
+ /**
78
+ * The push block's `PushBackend` on genesis-notifications' migration:
79
+ * registers with this install's id (so a token moves between logins only on
80
+ * its own phone), and keeps the push block's category switches as each
81
+ * category's push channel (`genesis_notifications_set_preference`, one cell
82
+ * at a time, so nothing else in the row is overwritten). A category no
83
+ * notifier type sends push in (chat's `messages` while chat still sends
84
+ * through 0005's sender) keeps its switch in 0005's `categories`, which that
85
+ * sender reads, instead of failing the whole save. Quiet hours and previews
86
+ * are the same columns as before.
87
+ *
88
+ * @stability Beta
89
+ */
90
+ declare function notificationsPushBackend(client: PushBackendClientLike): PushBackend;
91
+
92
+ export { CHANNEL_LABELS, ChannelPreferences, NOTIFICATIONS_FEED_LIMIT, NotificationPreferences, NotificationsFeed, type NotificationsFeedProps, NotificationsPushBackendError, type PushBackendClientLike, notificationsPushBackend, pushOptionsFrom, useNotificationsFeedHeader };
package/dist/native.js ADDED
@@ -0,0 +1,170 @@
1
+ import { useNotifications, usePreferences } from './chunk-2BG336II.js';
2
+ import { choicesOf } from './chunk-EWZB35V6.js';
3
+ import { useMemo } from 'react';
4
+ import { NativeListItem, NativeSwitch } from '@marktiderman/genesis-ui-native/components';
5
+ import { NotificationsPage, getNotificationsPageStackOptions } from '@marktiderman/genesis-ui-native/layouts';
6
+ import { NotificationSettings } from '@marktiderman/genesis-native/push';
7
+ export { notificationAction } from '@marktiderman/genesis-native/push';
8
+ import { jsx, Fragment } from 'react/jsx-runtime';
9
+
10
+ var CHANNEL_LABELS = { in_app: "In the app", push: "Push", email: "Email" };
11
+ var NOTIFICATIONS_FEED_LIMIT = 100;
12
+ function NotificationsFeed({ onOpen, limit = NOTIFICATIONS_FEED_LIMIT, ...page }) {
13
+ const { items, isRefetching, refetch, markRead } = useNotifications({ limit });
14
+ const rows = useMemo(
15
+ () => items.map((item) => ({
16
+ id: item.id,
17
+ title: item.title,
18
+ ...item.body ? { body: item.body } : {},
19
+ timestamp: item.created_at,
20
+ unread: item.unread,
21
+ onPress: () => {
22
+ if (item.unread) void markRead(item.id);
23
+ onOpen?.(item);
24
+ }
25
+ })),
26
+ [items, markRead, onOpen]
27
+ );
28
+ return /* @__PURE__ */ jsx(
29
+ NotificationsPage,
30
+ {
31
+ testID: "notifications",
32
+ ...page,
33
+ items: rows,
34
+ refreshing: isRefetching,
35
+ onRefresh: async () => {
36
+ await refetch();
37
+ }
38
+ }
39
+ );
40
+ }
41
+ function useNotificationsFeedHeader(options = {}) {
42
+ const { items, markAllRead } = useNotifications({ limit: options.limit ?? NOTIFICATIONS_FEED_LIMIT });
43
+ const unread = items.some((i) => i.unread);
44
+ return getNotificationsPageStackOptions({
45
+ headerTitle: options.headerTitle,
46
+ ...unread ? { onMarkAllRead: () => void markAllRead() } : { headerRight: () => null }
47
+ });
48
+ }
49
+ function ChannelPreferences(_props) {
50
+ const { rows, status, set } = usePreferences();
51
+ const labels = { ...CHANNEL_LABELS, ..._props.channelLabels };
52
+ return /* @__PURE__ */ jsx(Fragment, { children: rows.flatMap(
53
+ (row) => row.channels.map((ch) => /* @__PURE__ */ jsx(
54
+ NativeListItem,
55
+ {
56
+ testID: `notification-preference-${row.id}-${ch}`,
57
+ title: row.label,
58
+ subtitle: row.critical ? `${labels[ch]} \xB7 Always on` : labels[ch],
59
+ trailing: /* @__PURE__ */ jsx(
60
+ NativeSwitch,
61
+ {
62
+ testID: `notification-preference-${row.id}-${ch}-switch`,
63
+ accessibilityLabel: `${row.label}: ${labels[ch]}`,
64
+ checked: Boolean(row.values[ch]),
65
+ disabled: status !== "ready" || row.critical,
66
+ onCheckedChange: (on) => void set(row.id, ch, on).catch(() => void 0)
67
+ }
68
+ )
69
+ },
70
+ `${row.id}:${ch}`
71
+ ))
72
+ ) });
73
+ }
74
+ function NotificationPreferences(props) {
75
+ return /* @__PURE__ */ jsx(NotificationSettings, { ...props, slots: { categories: ChannelPreferences, ...props.slots } });
76
+ }
77
+ function pushOptionsFrom(registry) {
78
+ const categories = registry.androidChannels().map((c) => ({
79
+ id: c.id,
80
+ title: c.name,
81
+ ...c.description ? { description: c.description } : {},
82
+ importance: c.importance
83
+ }));
84
+ const deviceCategories = registry.deviceCategories();
85
+ return { categories, deviceCategories };
86
+ }
87
+ var NotificationsPushBackendError = class extends Error {
88
+ constructor(operation, code) {
89
+ super(`${operation} failed${code ? ` (${code})` : ""}`);
90
+ this.operation = operation;
91
+ this.code = code;
92
+ this.name = "NotificationsPushBackendError";
93
+ }
94
+ operation;
95
+ code;
96
+ };
97
+ function clock(value, fallback) {
98
+ const match = typeof value === "string" ? /^(\d{2}):(\d{2})/.exec(value) : null;
99
+ return match ? `${match[1]}:${match[2]}` : fallback;
100
+ }
101
+ function notificationsPushBackend(client) {
102
+ const must = (operation, result) => {
103
+ if (result.error) throw new NotificationsPushBackendError(operation, result.error.code ?? null);
104
+ return result.data;
105
+ };
106
+ let known = {};
107
+ let legacy = {};
108
+ return {
109
+ usesInstallationId: true,
110
+ async register(token, platform, appVersion, installationId) {
111
+ must("genesis_push_register", await client.rpc("genesis_push_register", { p_token: token, p_platform: platform, p_app_version: appVersion, p_installation_id: installationId ?? null }));
112
+ },
113
+ async forget(token) {
114
+ must("genesis_push_forget", await client.rpc("genesis_push_forget", { p_token: token }));
115
+ },
116
+ async loadPreferences(userId) {
117
+ const data = must(
118
+ "notification_preferences",
119
+ await client.from("notification_preferences").select("channels, categories, quiet_hours_enabled, quiet_hours_start, quiet_hours_end, time_zone, hide_previews").eq("user_id", userId).maybeSingle()
120
+ );
121
+ known = {};
122
+ legacy = {};
123
+ if (!data) return null;
124
+ if (data.categories && typeof data.categories === "object") {
125
+ for (const [category, on] of Object.entries(data.categories)) if (typeof on === "boolean") legacy[category] = on;
126
+ }
127
+ for (const [category, row] of Object.entries(choicesOf(data))) if (typeof row.push === "boolean") known[category] = row.push;
128
+ return {
129
+ categories: { ...known },
130
+ quietHours: {
131
+ enabled: data.quiet_hours_enabled === true,
132
+ start: clock(data.quiet_hours_start, "22:00"),
133
+ end: clock(data.quiet_hours_end, "07:00"),
134
+ timeZone: typeof data.time_zone === "string" ? data.time_zone : null
135
+ },
136
+ hidePreviews: data.hide_previews === true
137
+ };
138
+ },
139
+ async savePreferences(userId, p) {
140
+ let legacyChanged = false;
141
+ for (const [category, on] of Object.entries(p.categories)) {
142
+ if (known[category] === on) continue;
143
+ const result = await client.rpc("genesis_notifications_set_preference", { p_category: category, p_channel: "push", p_on: on });
144
+ if (result.error?.code === "42704") {
145
+ legacy[category] = on;
146
+ legacyChanged = true;
147
+ } else {
148
+ must("genesis_notifications_set_preference", result);
149
+ if (category in legacy) delete legacy[category];
150
+ }
151
+ known[category] = on;
152
+ }
153
+ const row = {
154
+ ...legacyChanged ? { categories: { ...legacy } } : {},
155
+ user_id: userId,
156
+ quiet_hours_enabled: p.quietHours.enabled,
157
+ quiet_hours_start: p.quietHours.start,
158
+ quiet_hours_end: p.quietHours.end,
159
+ time_zone: p.quietHours.timeZone,
160
+ hide_previews: p.hidePreviews
161
+ };
162
+ const data = must("notification_preferences", await client.from("notification_preferences").upsert(row, { onConflict: "user_id" }).select("user_id"));
163
+ if (!Array.isArray(data) || data.length !== 1) throw new NotificationsPushBackendError("notification_preferences", "no_rows");
164
+ }
165
+ };
166
+ }
167
+
168
+ export { CHANNEL_LABELS, ChannelPreferences, NOTIFICATIONS_FEED_LIMIT, NotificationPreferences, NotificationsFeed, NotificationsPushBackendError, notificationsPushBackend, pushOptionsFrom, useNotificationsFeedHeader };
169
+ //# sourceMappingURL=native.js.map
170
+ //# sourceMappingURL=native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/native/index.tsx"],"names":[],"mappings":"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","file":"native.js","sourcesContent":["/**\n * `@marktiderman/genesis-notifications/native`: the notifier on the phone.\n *\n * NotificationsFeed the feed on genesis-ui-native's NotificationsPage\n * useNotificationsFeedHeader the \"Mark all read\" header for the feed's Stack.Screen\n * ChannelPreferences category × channel switches, as NotificationSettings' `categories` part\n * NotificationPreferences genesis-native's NotificationSettings with that part in place\n * notificationsPushBackend the push block's `PushBackend` on this package's migration\n * pushOptionsFrom(registry) the push block's categories, Android channels and device categories\n * notificationAction a tapped action button (Reply, Mark as read), with the notification id\n *\n * The device side stays in genesis-native's push block (registration,\n * permission, routing, badge, hiding a push for the screen already open);\n * this is what connects it to the notifier.\n *\n * @packageDocumentation\n */\nimport { useMemo } from \"react\";\nimport { NativeListItem, NativeSwitch } from \"@marktiderman/genesis-ui-native/components\";\nimport { NotificationsPage, getNotificationsPageStackOptions, type NotificationItem, type NotificationsPageProps } from \"@marktiderman/genesis-ui-native/layouts\";\nimport {\n NotificationSettings,\n notificationAction,\n type CategoriesPartProps,\n type NotificationAction,\n type NotificationCategory,\n type NotificationPreferences as PushPreferences,\n type NotificationSettingsProps,\n type PushBackend,\n type PushDeviceCategory,\n type PushOptions,\n} from \"@marktiderman/genesis-native/push\";\nimport type { Channel, NotificationRegistry } from \"../core/index\";\nimport { useNotifications, usePreferences, type FeedItem } from \"../react/index\";\nimport { choicesOf } from \"../supabase/index\";\n\nexport { notificationAction, type NotificationAction };\n\nexport const CHANNEL_LABELS: Record<Channel, string> = { in_app: \"In the app\", push: \"Push\", email: \"Email\" };\n\n/** How many notifications the feed (and its header) reads: one query, shared by both. */\nexport const NOTIFICATIONS_FEED_LIMIT = 100;\n\nexport interface NotificationsFeedProps extends Omit<NotificationsPageProps, \"items\" | \"refreshing\" | \"onRefresh\" | \"onMarkAllRead\"> {\n /** Opens a notification's link (`router.push(item.url)`); it is marked read first. */\n onOpen?: (item: FeedItem) => void;\n limit?: number;\n}\n\n/**\n * The feed on ui-native's `NotificationsPage`: pull to refresh, unread dots,\n * a tap marks read and opens. Realtime refetches it (NotificationsProvider).\n *\n * @stability Beta\n */\nexport function NotificationsFeed({ onOpen, limit = NOTIFICATIONS_FEED_LIMIT, ...page }: NotificationsFeedProps) {\n const { items, isRefetching, refetch, markRead } = useNotifications({ limit });\n const rows = useMemo<NotificationItem[]>(\n () =>\n items.map((item) => ({\n id: item.id,\n title: item.title,\n ...(item.body ? { body: item.body } : {}),\n timestamp: item.created_at,\n unread: item.unread,\n onPress: () => {\n if (item.unread) void markRead(item.id);\n onOpen?.(item);\n },\n })),\n [items, markRead, onOpen],\n );\n return (\n <NotificationsPage\n testID=\"notifications\"\n {...page}\n items={rows}\n refreshing={isRefetching}\n onRefresh={async () => {\n await refetch();\n }}\n />\n );\n}\n\n/**\n * The feed's `Stack.Screen` options, with \"Mark all read\" while anything is\n * unread. Pass the feed's `limit` if you changed it, so both read one query.\n */\nexport function useNotificationsFeedHeader(options: { headerTitle?: string; limit?: number } = {}): Record<string, unknown> {\n const { items, markAllRead } = useNotifications({ limit: options.limit ?? NOTIFICATIONS_FEED_LIMIT });\n const unread = items.some((i) => i.unread);\n // With nothing unread the header gets an explicit empty right side: leaving it out would keep the old button.\n return getNotificationsPageStackOptions({\n headerTitle: options.headerTitle,\n ...(unread ? { onMarkAllRead: () => void markAllRead() } : { headerRight: () => null }),\n });\n}\n\n/**\n * Category × channel switches (push and email, and the feed when a type\n * offers it as optional), for NotificationSettings' `categories` slot. It\n * ignores the push block's own category switches and reads the notifier's\n * preferences instead.\n *\n * @stability Beta\n */\nexport function ChannelPreferences(_props: Partial<CategoriesPartProps> & { channelLabels?: Partial<Record<Channel, string>> }) {\n const { rows, status, set } = usePreferences();\n const labels = { ...CHANNEL_LABELS, ..._props.channelLabels };\n return (\n <>\n {rows.flatMap((row) =>\n row.channels.map((ch) => (\n <NativeListItem\n key={`${row.id}:${ch}`}\n testID={`notification-preference-${row.id}-${ch}`}\n title={row.label}\n subtitle={row.critical ? `${labels[ch]} · Always on` : labels[ch]}\n trailing={\n <NativeSwitch\n testID={`notification-preference-${row.id}-${ch}-switch`}\n accessibilityLabel={`${row.label}: ${labels[ch]}`}\n checked={Boolean(row.values[ch])}\n disabled={status !== \"ready\" || row.critical}\n onCheckedChange={(on) => void set(row.id, ch, on).catch(() => undefined)}\n />\n }\n />\n )),\n )}\n </>\n );\n}\n\n/**\n * Settings → Notifications with the notifier's channel switches: the push\n * block's permission, quiet hours and previews, and `ChannelPreferences` for\n * the categories. Needs the push block and NotificationsProvider.\n *\n * @stability Beta\n */\nexport function NotificationPreferences(props: NotificationSettingsProps) {\n return <NotificationSettings {...props} slots={{ categories: ChannelPreferences, ...props.slots }} />;\n}\n\n/**\n * What the push block needs from the registry: a category per notification\n * category (its Android channel; NotificationSettings shows the categories\n * section only when there are some), and the device categories with their\n * action buttons.\n *\n * @stability Beta\n */\nexport function pushOptionsFrom(registry: NotificationRegistry): Pick<PushOptions, \"categories\" | \"deviceCategories\"> {\n const categories: NotificationCategory[] = registry.androidChannels().map((c) => ({\n id: c.id,\n title: c.name,\n ...(c.description ? { description: c.description } : {}),\n importance: c.importance,\n }));\n const deviceCategories: PushDeviceCategory[] = registry.deviceCategories();\n return { categories, deviceCategories };\n}\n\ntype Result = { data: unknown; error: { code?: string | null } | null };\n\n/** The parts of a Supabase client the backend uses. */\nexport interface PushBackendClientLike {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n rpc: (fn: any, args?: any) => PromiseLike<Result>;\n from(table: string): unknown;\n}\n\ninterface PreferencesQuery {\n select(columns: string): { eq(column: string, value: string): { maybeSingle(): PromiseLike<Result> } };\n upsert(row: Record<string, unknown>, options: { onConflict: string }): { select(columns: string): PromiseLike<Result> };\n}\n\n/** A database refusal, with its SQLSTATE; never a token. */\nexport class NotificationsPushBackendError extends Error {\n constructor(\n readonly operation: string,\n readonly code: string | null,\n ) {\n super(`${operation} failed${code ? ` (${code})` : \"\"}`);\n this.name = \"NotificationsPushBackendError\";\n }\n}\n\nfunction clock(value: unknown, fallback: string): string {\n const match = typeof value === \"string\" ? /^(\\d{2}):(\\d{2})/.exec(value) : null;\n return match ? `${match[1]}:${match[2]}` : fallback;\n}\n\n/**\n * The push block's `PushBackend` on genesis-notifications' migration:\n * registers with this install's id (so a token moves between logins only on\n * its own phone), and keeps the push block's category switches as each\n * category's push channel (`genesis_notifications_set_preference`, one cell\n * at a time, so nothing else in the row is overwritten). A category no\n * notifier type sends push in (chat's `messages` while chat still sends\n * through 0005's sender) keeps its switch in 0005's `categories`, which that\n * sender reads, instead of failing the whole save. Quiet hours and previews\n * are the same columns as before.\n *\n * @stability Beta\n */\nexport function notificationsPushBackend(client: PushBackendClientLike): PushBackend {\n const must = (operation: string, result: Result) => {\n if (result.error) throw new NotificationsPushBackendError(operation, result.error.code ?? null);\n return result.data;\n };\n // The push channel per category as last loaded or saved, so a save writes only what changed.\n let known: Record<string, boolean> = {};\n // 0005's `categories` as last loaded or saved: the switches of categories the notifier has no push type in.\n let legacy: Record<string, boolean> = {};\n return {\n usesInstallationId: true,\n async register(token, platform, appVersion, installationId) {\n must(\"genesis_push_register\", await client.rpc(\"genesis_push_register\", { p_token: token, p_platform: platform, p_app_version: appVersion, p_installation_id: installationId ?? null }));\n },\n async forget(token) {\n must(\"genesis_push_forget\", await client.rpc(\"genesis_push_forget\", { p_token: token }));\n },\n async loadPreferences(userId) {\n const data = must(\n \"notification_preferences\",\n await (client.from(\"notification_preferences\") as PreferencesQuery)\n .select(\"channels, categories, quiet_hours_enabled, quiet_hours_start, quiet_hours_end, time_zone, hide_previews\")\n .eq(\"user_id\", userId)\n .maybeSingle(),\n ) as Record<string, unknown> | null;\n known = {};\n legacy = {};\n if (!data) return null;\n if (data.categories && typeof data.categories === \"object\") {\n for (const [category, on] of Object.entries(data.categories as Record<string, unknown>)) if (typeof on === \"boolean\") legacy[category] = on;\n }\n // As the server reads it: the push channel chosen, or 0005's switch for the whole category.\n for (const [category, row] of Object.entries(choicesOf(data))) if (typeof row.push === \"boolean\") known[category] = row.push;\n return {\n categories: { ...known },\n quietHours: {\n enabled: data.quiet_hours_enabled === true,\n start: clock(data.quiet_hours_start, \"22:00\"),\n end: clock(data.quiet_hours_end, \"07:00\"),\n timeZone: typeof data.time_zone === \"string\" ? data.time_zone : null,\n },\n hidePreviews: data.hide_previews === true,\n } satisfies PushPreferences;\n },\n async savePreferences(userId, p) {\n let legacyChanged = false;\n for (const [category, on] of Object.entries(p.categories)) {\n if (known[category] === on) continue;\n const result = await client.rpc(\"genesis_notifications_set_preference\", { p_category: category, p_channel: \"push\", p_on: on });\n // 42704 is only \"no type in that category uses push\"; any other refusal (a critical category, 22023) is thrown.\n if (result.error?.code === \"42704\") {\n // No notifier type sends push in this category: keep the switch where 0005's sender reads it.\n legacy[category] = on;\n legacyChanged = true;\n } else {\n must(\"genesis_notifications_set_preference\", result);\n // The server moved this category's 0005 switch into channel choices.\n if (category in legacy) delete legacy[category];\n }\n known[category] = on;\n }\n const row = {\n ...(legacyChanged ? { categories: { ...legacy } } : {}),\n user_id: userId,\n quiet_hours_enabled: p.quietHours.enabled,\n quiet_hours_start: p.quietHours.start,\n quiet_hours_end: p.quietHours.end,\n time_zone: p.quietHours.timeZone,\n hide_previews: p.hidePreviews,\n };\n const data = must(\"notification_preferences\", await (client.from(\"notification_preferences\") as PreferencesQuery).upsert(row, { onConflict: \"user_id\" }).select(\"user_id\"));\n if (!Array.isArray(data) || data.length !== 1) throw new NotificationsPushBackendError(\"notification_preferences\", \"no_rows\");\n },\n };\n}\n"]}
@@ -0,0 +1,98 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { n as NotificationRow, l as NotificationRegistry, g as MatrixRow, a as Channel, b as ChannelChoices } from './types-BilguCPW.js';
4
+ import { NotificationsSupabaseLike, NotificationsClient, FeedFilter } from './supabase.js';
5
+ export { notificationsClient } from './supabase.js';
6
+
7
+ interface Context {
8
+ client: NotificationsClient;
9
+ registry: NotificationRegistry;
10
+ userId: string | null;
11
+ }
12
+ interface NotificationsProviderProps {
13
+ /** A Supabase client signed in as the person (or a `notificationsClient`). */
14
+ client: NotificationsSupabaseLike | NotificationsClient;
15
+ registry: NotificationRegistry;
16
+ /** The signed-in user; null while signed out (nothing is read). */
17
+ userId: string | null;
18
+ /** Listen on `user:<id>` and refetch on a change. Default true. */
19
+ realtime?: boolean;
20
+ children?: ReactNode;
21
+ }
22
+ /** The query keys, for an app that invalidates them itself. */
23
+ declare const notificationKeys: {
24
+ all: (userId: string | null) => readonly ["genesis-notifications", string | null];
25
+ feed: (userId: string | null, filter: FeedFilter, limit: number) => readonly ["genesis-notifications", string | null, "feed", FeedFilter, number];
26
+ unread: (userId: string | null) => readonly ["genesis-notifications", string | null, "unread"];
27
+ choices: (userId: string | null) => readonly ["genesis-notifications", string | null, "choices"];
28
+ };
29
+ /**
30
+ * Provides the notifier to the hooks.
31
+ *
32
+ * @stability Beta
33
+ */
34
+ declare function NotificationsProvider({ client, registry, userId, realtime, children }: NotificationsProviderProps): react.JSX.Element;
35
+ /** The provider's client, registry and user. Throws outside one. */
36
+ declare function useNotificationsContext(): Context;
37
+ /** A feed row with what the registry says about it. */
38
+ interface FeedItem extends NotificationRow {
39
+ title: string;
40
+ body?: string;
41
+ url?: string;
42
+ unread: boolean;
43
+ }
44
+ interface UseNotificationsResult {
45
+ items: FeedItem[];
46
+ isLoading: boolean;
47
+ isRefetching: boolean;
48
+ error: Error | null;
49
+ refetch: () => Promise<unknown>;
50
+ markRead: (ids: string | readonly string[]) => Promise<void>;
51
+ markUnread: (ids: string | readonly string[]) => Promise<void>;
52
+ markAllRead: () => Promise<void>;
53
+ archive: (ids: string | readonly string[]) => Promise<void>;
54
+ /** Mark everything shown as seen (opening the feed); the unread count is about read, not seen. */
55
+ markAllSeen: () => Promise<void>;
56
+ }
57
+ /**
58
+ * The person's feed.
59
+ *
60
+ * @stability Beta
61
+ */
62
+ declare function useNotifications(options?: {
63
+ filter?: FeedFilter;
64
+ limit?: number;
65
+ }): UseNotificationsResult;
66
+ /**
67
+ * The unread count (0 while signed out).
68
+ *
69
+ * @stability Beta
70
+ */
71
+ declare function useUnreadCount(): number;
72
+ /** One category's row in the matrix. */
73
+ interface PreferenceRow extends MatrixRow {
74
+ /** What each offered channel is now: the person's choice, else the default. */
75
+ values: Partial<Record<Channel, boolean>>;
76
+ /** Which values the person chose (the rest are defaults). */
77
+ chosen: Partial<Record<Channel, boolean>>;
78
+ }
79
+ interface UsePreferencesResult {
80
+ rows: PreferenceRow[];
81
+ status: "loading" | "ready" | "error";
82
+ error: Error | null;
83
+ saving: boolean;
84
+ /** Turn one category's channel on or off; `null` goes back to the default. Critical rows can't be turned off. */
85
+ set: (category: string, channel: Channel, on: boolean | null) => Promise<void>;
86
+ }
87
+ /** The matrix with the person's choices applied. Exported for tests and custom screens. */
88
+ declare function preferenceRows(registry: NotificationRegistry, choices: ChannelChoices): PreferenceRow[];
89
+ /**
90
+ * The person's preferences by category and channel. A tenant's default
91
+ * isn't shown here (the server applies it); what shows for a channel the
92
+ * person never chose is the type's default.
93
+ *
94
+ * @stability Beta
95
+ */
96
+ declare function usePreferences(): UsePreferencesResult;
97
+
98
+ export { type FeedItem, NotificationsClient, NotificationsProvider, type NotificationsProviderProps, type PreferenceRow, type UseNotificationsResult, type UsePreferencesResult, notificationKeys, preferenceRows, useNotifications, useNotificationsContext, usePreferences, useUnreadCount };
package/dist/react.js ADDED
@@ -0,0 +1,4 @@
1
+ export { NotificationsProvider, notificationKeys, preferenceRows, useNotifications, useNotificationsContext, usePreferences, useUnreadCount } from './chunk-2BG336II.js';
2
+ export { notificationsClient } from './chunk-EWZB35V6.js';
3
+ //# sourceMappingURL=react.js.map
4
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"react.js"}