@aglyn/tenant-feature-instance 1.0.0-beta.143

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 (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +3 -0
  3. package/jest.config.d.ts +36 -0
  4. package/package.json +45 -0
  5. package/src/index.d.ts +17 -0
  6. package/src/index.js +18 -0
  7. package/src/index.js.map +1 -0
  8. package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
  9. package/src/lib/components/duplicate-resource-dialog.js +217 -0
  10. package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
  11. package/src/lib/constants/firebase-config.d.ts +68 -0
  12. package/src/lib/constants/firebase-config.js +155 -0
  13. package/src/lib/constants/firebase-config.js.map +1 -0
  14. package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
  15. package/src/lib/constants/firebase-emulator-hosts.js +81 -0
  16. package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
  17. package/src/lib/constants/mock-data.d.ts +24 -0
  18. package/src/lib/constants/mock-data.js +49 -0
  19. package/src/lib/constants/mock-data.js.map +1 -0
  20. package/src/lib/constants/site-paths.d.ts +47 -0
  21. package/src/lib/constants/site-paths.js +66 -0
  22. package/src/lib/constants/site-paths.js.map +1 -0
  23. package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
  24. package/src/lib/hooks/firebase/auth-persistence.js +117 -0
  25. package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
  26. package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
  27. package/src/lib/hooks/firebase/firebase-services.js +522 -0
  28. package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
  29. package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
  30. package/src/lib/hooks/firebase/firestore-cache.js +152 -0
  31. package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
  32. package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
  33. package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
  34. package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
  35. package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
  36. package/src/lib/hooks/firestore-denial-reporter.js +293 -0
  37. package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
  38. package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
  39. package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
  40. package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
  41. package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
  42. package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
  43. package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
  44. package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
  45. package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
  46. package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
  47. package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
  48. package/src/lib/hooks/helpers/listen-options.js +92 -0
  49. package/src/lib/hooks/helpers/listen-options.js.map +1 -0
  50. package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
  51. package/src/lib/hooks/helpers/use-doc.js +213 -0
  52. package/src/lib/hooks/helpers/use-doc.js.map +1 -0
  53. package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
  54. package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
  55. package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
  56. package/src/lib/hooks/host-collection-queries.d.ts +125 -0
  57. package/src/lib/hooks/host-collection-queries.js +126 -0
  58. package/src/lib/hooks/host-collection-queries.js.map +1 -0
  59. package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
  60. package/src/lib/hooks/list-filter-constraints.js +288 -0
  61. package/src/lib/hooks/list-filter-constraints.js.map +1 -0
  62. package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
  63. package/src/lib/hooks/sorted-collection-window.js +150 -0
  64. package/src/lib/hooks/sorted-collection-window.js.map +1 -0
  65. package/src/lib/hooks/use-component-version.d.ts +43 -0
  66. package/src/lib/hooks/use-component-version.js +73 -0
  67. package/src/lib/hooks/use-component-version.js.map +1 -0
  68. package/src/lib/hooks/use-component.d.ts +39 -0
  69. package/src/lib/hooks/use-component.js +55 -0
  70. package/src/lib/hooks/use-component.js.map +1 -0
  71. package/src/lib/hooks/use-console-host-route.d.ts +24 -0
  72. package/src/lib/hooks/use-console-host-route.js +77 -0
  73. package/src/lib/hooks/use-console-host-route.js.map +1 -0
  74. package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
  75. package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
  76. package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
  77. package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
  78. package/src/lib/hooks/use-firestore-collection.js +307 -0
  79. package/src/lib/hooks/use-firestore-collection.js.map +1 -0
  80. package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
  81. package/src/lib/hooks/use-firestore-doc.js +192 -0
  82. package/src/lib/hooks/use-firestore-doc.js.map +1 -0
  83. package/src/lib/hooks/use-form-version.d.ts +44 -0
  84. package/src/lib/hooks/use-form-version.js +73 -0
  85. package/src/lib/hooks/use-form-version.js.map +1 -0
  86. package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
  87. package/src/lib/hooks/use-host-activity-logger.js +96 -0
  88. package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
  89. package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
  90. package/src/lib/hooks/use-host-campaigns.js +93 -0
  91. package/src/lib/hooks/use-host-campaigns.js.map +1 -0
  92. package/src/lib/hooks/use-host-org-id.d.ts +96 -0
  93. package/src/lib/hooks/use-host-org-id.js +154 -0
  94. package/src/lib/hooks/use-host-org-id.js.map +1 -0
  95. package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
  96. package/src/lib/hooks/use-host-resource-api.js +60 -0
  97. package/src/lib/hooks/use-host-resource-api.js.map +1 -0
  98. package/src/lib/hooks/use-host-template.d.ts +44 -0
  99. package/src/lib/hooks/use-host-template.js +82 -0
  100. package/src/lib/hooks/use-host-template.js.map +1 -0
  101. package/src/lib/hooks/use-host-version-api.d.ts +59 -0
  102. package/src/lib/hooks/use-host-version-api.js +69 -0
  103. package/src/lib/hooks/use-host-version-api.js.map +1 -0
  104. package/src/lib/hooks/use-host.d.ts +101 -0
  105. package/src/lib/hooks/use-host.js +45 -0
  106. package/src/lib/hooks/use-host.js.map +1 -0
  107. package/src/lib/hooks/use-layout-version.d.ts +33 -0
  108. package/src/lib/hooks/use-layout-version.js +64 -0
  109. package/src/lib/hooks/use-layout-version.js.map +1 -0
  110. package/src/lib/hooks/use-layout.d.ts +41 -0
  111. package/src/lib/hooks/use-layout.js +49 -0
  112. package/src/lib/hooks/use-layout.js.map +1 -0
  113. package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
  114. package/src/lib/hooks/use-live-artifact-count.js +102 -0
  115. package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
  116. package/src/lib/hooks/use-org-member-options.d.ts +57 -0
  117. package/src/lib/hooks/use-org-member-options.js +112 -0
  118. package/src/lib/hooks/use-org-member-options.js.map +1 -0
  119. package/src/lib/hooks/use-org-plan.d.ts +46 -0
  120. package/src/lib/hooks/use-org-plan.js +86 -0
  121. package/src/lib/hooks/use-org-plan.js.map +1 -0
  122. package/src/lib/hooks/use-paged-collection.d.ts +95 -0
  123. package/src/lib/hooks/use-paged-collection.js +130 -0
  124. package/src/lib/hooks/use-paged-collection.js.map +1 -0
  125. package/src/lib/hooks/use-plugin-config.d.ts +38 -0
  126. package/src/lib/hooks/use-plugin-config.js +120 -0
  127. package/src/lib/hooks/use-plugin-config.js.map +1 -0
  128. package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
  129. package/src/lib/hooks/use-scope-tokens.js +100 -0
  130. package/src/lib/hooks/use-scope-tokens.js.map +1 -0
  131. package/src/lib/hooks/use-screen-version.d.ts +33 -0
  132. package/src/lib/hooks/use-screen-version.js +65 -0
  133. package/src/lib/hooks/use-screen-version.js.map +1 -0
  134. package/src/lib/hooks/use-screen.d.ts +68 -0
  135. package/src/lib/hooks/use-screen.js +50 -0
  136. package/src/lib/hooks/use-screen.js.map +1 -0
  137. package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
  138. package/src/lib/hooks/use-sending-identity-api.js +95 -0
  139. package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
  140. package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
  141. package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
  142. package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
  143. package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
  144. package/src/lib/hooks/use-switcher-collection.js +275 -0
  145. package/src/lib/hooks/use-switcher-collection.js.map +1 -0
  146. package/src/lib/hooks/use-user-name.d.ts +51 -0
  147. package/src/lib/hooks/use-user-name.js +90 -0
  148. package/src/lib/hooks/use-user-name.js.map +1 -0
  149. package/src/lib/hooks/use-user-photo.d.ts +50 -0
  150. package/src/lib/hooks/use-user-photo.js +87 -0
  151. package/src/lib/hooks/use-user-photo.js.map +1 -0
  152. package/src/lib/tenant.d.ts +61 -0
  153. package/src/lib/tenant.js +62 -0
  154. package/src/lib/tenant.js.map +1 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { Timestamp } from "@aglyn/shared-util-timestamp";
18
+ import { collection, getCountFromServer, query, where } from "firebase/firestore";
19
+ import { useEffect, useState } from "react";
20
+ import { useFirestore } from "./firebase/firebase-services.js";
21
+ /**
22
+ * Every timestamp is greater than this one, and no `null` is.
23
+ *
24
+ * Firestore orders values by TYPE before value, and `null` sorts below every
25
+ * number, string and timestamp — so an inequality against the epoch selects
26
+ * exactly the documents whose `deletedAt` holds a real timestamp. Documents
27
+ * with no `deletedAt` field are excluded from an inequality outright, which
28
+ * is the other half of the same answer.
29
+ *
30
+ * Built where it is used rather than at module scope. This module is reached
31
+ * through the library's barrel, so a `Timestamp` call up here runs for every
32
+ * consumer of every hook in it — including the many that stand a stub in for
33
+ * `@aglyn/shared-util-timestamp` and never touch this one.
34
+ */ const beforeEveryTombstone = ()=>Timestamp.fromMillis(0);
35
+ /**
36
+ * How many artifacts of a kind a site actually holds — a server aggregate,
37
+ * not the length of a page (AGL-1716, the AGL-1706 shape).
38
+ *
39
+ * A list that pages holds ten rows. Handing that ten to a quota readout is a
40
+ * confident wrong number in the flattering direction: "10/10 layouts on your
41
+ * plan" on a site with sixty of them reads as room to spare, right up until
42
+ * `/api/hosts/resources` refuses the create. The list and the count are
43
+ * different questions and are asked separately — one aggregate per mount,
44
+ * re-asked when `epoch` moves.
45
+ *
46
+ * ## Why two aggregates rather than one
47
+ *
48
+ * Deleting an artifact stamps `deletedAt` and leaves the document in place,
49
+ * so a published tenant page keeps grafting until its next revalidate. The
50
+ * enforcing route counts the LIVE ones — `/api/hosts/resources` reads the
51
+ * collection with a `deletedAt` projection and keeps the entries where it is
52
+ * `== null` — so a console count over the whole collection would quote a cap
53
+ * usage the server does not enforce, and would disagree with the list beside
54
+ * it row for row.
55
+ *
56
+ * Firestore cannot express "field is absent" as a predicate, and the two live
57
+ * shapes are not one value: an artifact created through the resources route
58
+ * carries no `deletedAt` at all, while one installed from the marketplace
59
+ * carries an explicit `deletedAt: null` (`install.ts` writes it). So
60
+ * `where('deletedAt', '==', null)` would return the marketplace copies alone.
61
+ * Counting the TOMBSTONES instead is exact, because there is only one shape
62
+ * of tombstone — a `Timestamp` — and subtracting them from the total leaves
63
+ * precisely the documents the server calls live.
64
+ *
65
+ * Both aggregates bill one document read each at these collection sizes.
66
+ *
67
+ * @returns the live count, or `null` while it is pending or if it was
68
+ * refused. A caller must fall back to something that can only UNDERSTATE the
69
+ * truth, never to `0`: an entitlement check answers from whatever it is
70
+ * handed, and zero used is the one wrong answer that opens a gate.
71
+ */ export function useLiveArtifactCount(hostId, artifact, epoch = 0) {
72
+ const firestore = useFirestore();
73
+ const [count, setCount] = useState(null);
74
+ useEffect(()=>{
75
+ let active = true;
76
+ const artifacts = collection(firestore, 'hosts', hostId, artifact);
77
+ void Promise.all([
78
+ getCountFromServer(artifacts),
79
+ getCountFromServer(query(artifacts, where('deletedAt', '>', beforeEveryTombstone())))
80
+ ]).then(([total, tombstones])=>{
81
+ if (!active) return;
82
+ // Never below zero: the two aggregates are separate round-trips and a
83
+ // delete landing between them can make the second the larger read.
84
+ setCount(Math.max(0, total.data().count - tombstones.data().count));
85
+ }).catch(()=>{
86
+ // The caller's fallback stands. Deliberately not 0 — see the return
87
+ // contract above.
88
+ });
89
+ return ()=>{
90
+ active = false;
91
+ };
92
+ }, [
93
+ firestore,
94
+ hostId,
95
+ artifact,
96
+ epoch
97
+ ]);
98
+ return count;
99
+ }
100
+ export default useLiveArtifactCount;
101
+
102
+ //# sourceMappingURL=use-live-artifact-count.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-live-artifact-count.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { collection, getCountFromServer, query, where } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useFirestore } from './firebase/firebase-services'\n\n/**\n * The per-site artifact collections that render as a console list.\n *\n * `forms` is one of them: it is created through the same resources route,\n * keyed by the same generated uid, and listed by the same table. Its cap is\n * enforced over the WHOLE collection rather than over live documents — a form\n * is deleted outright, and its submissions keep their `formId` either way —\n * so the aggregate below matches what `/api/hosts/resources` counts, with the\n * tombstone subtraction a no-op because nothing writes `deletedAt` on a form.\n */\nexport type HostArtifactCollection =\n | 'screens'\n | 'layouts'\n | 'components'\n | 'templates'\n | 'forms'\n\n/**\n * Every timestamp is greater than this one, and no `null` is.\n *\n * Firestore orders values by TYPE before value, and `null` sorts below every\n * number, string and timestamp — so an inequality against the epoch selects\n * exactly the documents whose `deletedAt` holds a real timestamp. Documents\n * with no `deletedAt` field are excluded from an inequality outright, which\n * is the other half of the same answer.\n *\n * Built where it is used rather than at module scope. This module is reached\n * through the library's barrel, so a `Timestamp` call up here runs for every\n * consumer of every hook in it — including the many that stand a stub in for\n * `@aglyn/shared-util-timestamp` and never touch this one.\n */\nconst beforeEveryTombstone = () => Timestamp.fromMillis(0)\n\n/**\n * How many artifacts of a kind a site actually holds — a server aggregate,\n * not the length of a page (AGL-1716, the AGL-1706 shape).\n *\n * A list that pages holds ten rows. Handing that ten to a quota readout is a\n * confident wrong number in the flattering direction: \"10/10 layouts on your\n * plan\" on a site with sixty of them reads as room to spare, right up until\n * `/api/hosts/resources` refuses the create. The list and the count are\n * different questions and are asked separately — one aggregate per mount,\n * re-asked when `epoch` moves.\n *\n * ## Why two aggregates rather than one\n *\n * Deleting an artifact stamps `deletedAt` and leaves the document in place,\n * so a published tenant page keeps grafting until its next revalidate. The\n * enforcing route counts the LIVE ones — `/api/hosts/resources` reads the\n * collection with a `deletedAt` projection and keeps the entries where it is\n * `== null` — so a console count over the whole collection would quote a cap\n * usage the server does not enforce, and would disagree with the list beside\n * it row for row.\n *\n * Firestore cannot express \"field is absent\" as a predicate, and the two live\n * shapes are not one value: an artifact created through the resources route\n * carries no `deletedAt` at all, while one installed from the marketplace\n * carries an explicit `deletedAt: null` (`install.ts` writes it). So\n * `where('deletedAt', '==', null)` would return the marketplace copies alone.\n * Counting the TOMBSTONES instead is exact, because there is only one shape\n * of tombstone — a `Timestamp` — and subtracting them from the total leaves\n * precisely the documents the server calls live.\n *\n * Both aggregates bill one document read each at these collection sizes.\n *\n * @returns the live count, or `null` while it is pending or if it was\n * refused. A caller must fall back to something that can only UNDERSTATE the\n * truth, never to `0`: an entitlement check answers from whatever it is\n * handed, and zero used is the one wrong answer that opens a gate.\n */\nexport function useLiveArtifactCount(\n hostId: string,\n artifact: HostArtifactCollection,\n epoch = 0,\n): number | null {\n const firestore = useFirestore()\n const [count, setCount] = useState<number | null>(null)\n\n useEffect(() => {\n let active = true\n const artifacts = collection(firestore, 'hosts', hostId, artifact)\n void Promise.all([\n getCountFromServer(artifacts),\n getCountFromServer(\n query(artifacts, where('deletedAt', '>', beforeEveryTombstone())),\n ),\n ])\n .then(([total, tombstones]) => {\n if (!active) return\n // Never below zero: the two aggregates are separate round-trips and a\n // delete landing between them can make the second the larger read.\n setCount(\n Math.max(0, total.data().count - tombstones.data().count),\n )\n })\n .catch(() => {\n // The caller's fallback stands. Deliberately not 0 — see the return\n // contract above.\n })\n return () => {\n active = false\n }\n }, [firestore, hostId, artifact, epoch])\n\n return count\n}\n\nexport default useLiveArtifactCount\n"],"names":["Timestamp","collection","getCountFromServer","query","where","useEffect","useState","useFirestore","beforeEveryTombstone","fromMillis","useLiveArtifactCount","hostId","artifact","epoch","firestore","count","setCount","active","artifacts","Promise","all","then","total","tombstones","Math","max","data","catch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,SAAS,QAAQ,+BAA8B;AACxD,SAASC,UAAU,EAAEC,kBAAkB,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AACjF,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,YAAY,QAAQ,kCAA8B;AAmB3D;;;;;;;;;;;;;CAaC,GACD,MAAMC,uBAAuB,IAAMR,UAAUS,UAAU,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GACD,OAAO,SAASC,qBACdC,MAAc,EACdC,QAAgC,EAChCC,QAAQ,CAAC;IAET,MAAMC,YAAYP;IAClB,MAAM,CAACQ,OAAOC,SAAS,GAAGV,SAAwB;IAElDD,UAAU;QACR,IAAIY,SAAS;QACb,MAAMC,YAAYjB,WAAWa,WAAW,SAASH,QAAQC;QACzD,KAAKO,QAAQC,GAAG,CAAC;YACflB,mBAAmBgB;YACnBhB,mBACEC,MAAMe,WAAWd,MAAM,aAAa,KAAKI;SAE5C,EACEa,IAAI,CAAC,CAAC,CAACC,OAAOC,WAAW;YACxB,IAAI,CAACN,QAAQ;YACb,sEAAsE;YACtE,mEAAmE;YACnED,SACEQ,KAAKC,GAAG,CAAC,GAAGH,MAAMI,IAAI,GAAGX,KAAK,GAAGQ,WAAWG,IAAI,GAAGX,KAAK;QAE5D,GACCY,KAAK,CAAC;QACL,oEAAoE;QACpE,kBAAkB;QACpB;QACF,OAAO;YACLV,SAAS;QACX;IACF,GAAG;QAACH;QAAWH;QAAQC;QAAUC;KAAM;IAEvC,OAAOE;AACT;AAEA,eAAeL,qBAAoB"}
@@ -0,0 +1,57 @@
1
+ import type { AglynOrgMember } from '@aglyn/aglyn';
2
+ /** One team member, as much of them as a picker needs. */
3
+ export interface OrgMemberOption {
4
+ /** The account uid — what a record's `ownerUid` stores. */
5
+ uid: string;
6
+ /** The name a colleague recognizes: display name, else address, else uid. */
7
+ label: string;
8
+ email?: string;
9
+ }
10
+ export interface OrgMemberOptions {
11
+ options: OrgMemberOption[];
12
+ /** The read has answered, one way or the other — false while settling or disabled. */
13
+ ready: boolean;
14
+ /** Why the roster is empty, when it is empty because the route refused. */
15
+ error: string | null;
16
+ }
17
+ /**
18
+ * The roster as picker options, sorted by how they read.
19
+ *
20
+ * Exported on its own so the mapping has one definition: a bulk "set owner"
21
+ * on the contacts list and an audience rule's owner filter must show the
22
+ * same names for the same people, or a merchant assigning contacts to "Ada"
23
+ * on one screen would be filtering for "ada@example.com" on the other.
24
+ */
25
+ export declare function orgMemberOptions(members: ReadonlyArray<Partial<AglynOrgMember> & {
26
+ $id: string;
27
+ }>): OrgMemberOption[];
28
+ /**
29
+ * THE ORG'S TEAM, for an owner picker on some record's page (AGL-2603).
30
+ *
31
+ * ## Through the members route, not the collection
32
+ *
33
+ * `orgs/{orgId}/members` is readable by an org-wide member and by nobody
34
+ * else: the rules evaluate a list against the QUERY, so a collaborator scoped
35
+ * to one site reading the roster gets `permission-denied`, not a shorter
36
+ * roster. `GET /api/orgs/members?orgId=` re-derives membership with the Admin
37
+ * SDK and answers any member of the org, which is who assigns an owner.
38
+ *
39
+ * ## It is OFF unless a caller asks
40
+ *
41
+ * `enabled` defaults to false, the way `useHostCampaigns` does and for the
42
+ * same reason: the picker this feeds sits on surfaces a reader opens for
43
+ * something else, and a roster fetched on every arrival at the contacts list
44
+ * is a request for a control nobody has opened. Callers turn it on when the
45
+ * owner control is actually on screen.
46
+ *
47
+ * ## A refusal is reported, never rendered as an empty team
48
+ *
49
+ * An org with no members is impossible — it has an owner — so an empty list
50
+ * with no error would always be a lie. `authorizedFetch` refuses to send a
51
+ * request it cannot authorize and answers a 401 carrying the reason; that
52
+ * reason, or the route's own, is what `error` holds.
53
+ */
54
+ export declare function useOrgMemberOptions(orgId: string | undefined, options?: {
55
+ enabled?: boolean;
56
+ }): OrgMemberOptions;
57
+ export default useOrgMemberOptions;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
18
+ import { useEffect, useMemo, useState } from "react";
19
+ import { useUser } from "./firebase/firebase-services.js";
20
+ /**
21
+ * The roster as picker options, sorted by how they read.
22
+ *
23
+ * Exported on its own so the mapping has one definition: a bulk "set owner"
24
+ * on the contacts list and an audience rule's owner filter must show the
25
+ * same names for the same people, or a merchant assigning contacts to "Ada"
26
+ * on one screen would be filtering for "ada@example.com" on the other.
27
+ */ export function orgMemberOptions(members) {
28
+ return members.map((member)=>({
29
+ uid: String(member.$id),
30
+ label: String(member.displayName || member.email || member.$id),
31
+ email: member.email ? String(member.email) : undefined
32
+ })).sort((a, b)=>a.label.localeCompare(b.label));
33
+ }
34
+ /**
35
+ * THE ORG'S TEAM, for an owner picker on some record's page (AGL-2603).
36
+ *
37
+ * ## Through the members route, not the collection
38
+ *
39
+ * `orgs/{orgId}/members` is readable by an org-wide member and by nobody
40
+ * else: the rules evaluate a list against the QUERY, so a collaborator scoped
41
+ * to one site reading the roster gets `permission-denied`, not a shorter
42
+ * roster. `GET /api/orgs/members?orgId=` re-derives membership with the Admin
43
+ * SDK and answers any member of the org, which is who assigns an owner.
44
+ *
45
+ * ## It is OFF unless a caller asks
46
+ *
47
+ * `enabled` defaults to false, the way `useHostCampaigns` does and for the
48
+ * same reason: the picker this feeds sits on surfaces a reader opens for
49
+ * something else, and a roster fetched on every arrival at the contacts list
50
+ * is a request for a control nobody has opened. Callers turn it on when the
51
+ * owner control is actually on screen.
52
+ *
53
+ * ## A refusal is reported, never rendered as an empty team
54
+ *
55
+ * An org with no members is impossible — it has an owner — so an empty list
56
+ * with no error would always be a lie. `authorizedFetch` refuses to send a
57
+ * request it cannot authorize and answers a 401 carrying the reason; that
58
+ * reason, or the route's own, is what `error` holds.
59
+ */ export function useOrgMemberOptions(orgId, options) {
60
+ var _ref;
61
+ const enabled = (_ref = options == null ? void 0 : options.enabled) != null ? _ref : false;
62
+ const { data: user } = useUser();
63
+ const [state, setState] = useState({
64
+ orgId: null,
65
+ members: [],
66
+ error: null
67
+ });
68
+ useEffect(()=>{
69
+ if (!enabled || !orgId) return;
70
+ let active = true;
71
+ void (async ()=>{
72
+ let members = [];
73
+ let error = null;
74
+ try {
75
+ const response = await authorizedFetch(user, `/api/orgs/members?orgId=${encodeURIComponent(orgId)}`);
76
+ const payload = await response.json().catch(()=>({}));
77
+ if (!response.ok) {
78
+ var _ref;
79
+ error = String((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The team could not be read.');
80
+ } else {
81
+ members = orgMemberOptions(Array.isArray(payload == null ? void 0 : payload.members) ? payload.members : []);
82
+ }
83
+ } catch (unused) {
84
+ error = 'The team could not be read.';
85
+ }
86
+ if (active) setState({
87
+ orgId,
88
+ members,
89
+ error
90
+ });
91
+ })();
92
+ return ()=>{
93
+ active = false;
94
+ };
95
+ }, [
96
+ enabled,
97
+ orgId,
98
+ user
99
+ ]);
100
+ return useMemo(()=>({
101
+ options: state.orgId === orgId ? state.members : [],
102
+ ready: enabled && Boolean(orgId) && state.orgId === orgId,
103
+ error: state.orgId === orgId ? state.error : null
104
+ }), [
105
+ state,
106
+ orgId,
107
+ enabled
108
+ ]);
109
+ }
110
+ export default useOrgMemberOptions;
111
+
112
+ //# sourceMappingURL=use-org-member-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-org-member-options.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { AglynOrgMember } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useUser } from './firebase/firebase-services'\n\n/** One team member, as much of them as a picker needs. */\nexport interface OrgMemberOption {\n /** The account uid — what a record's `ownerUid` stores. */\n uid: string\n /** The name a colleague recognizes: display name, else address, else uid. */\n label: string\n email?: string\n}\n\nexport interface OrgMemberOptions {\n options: OrgMemberOption[]\n /** The read has answered, one way or the other — false while settling or disabled. */\n ready: boolean\n /** Why the roster is empty, when it is empty because the route refused. */\n error: string | null\n}\n\n/**\n * The roster as picker options, sorted by how they read.\n *\n * Exported on its own so the mapping has one definition: a bulk \"set owner\"\n * on the contacts list and an audience rule's owner filter must show the\n * same names for the same people, or a merchant assigning contacts to \"Ada\"\n * on one screen would be filtering for \"ada@example.com\" on the other.\n */\nexport function orgMemberOptions(\n members: ReadonlyArray<Partial<AglynOrgMember> & { $id: string }>,\n): OrgMemberOption[] {\n return members\n .map((member) => ({\n uid: String(member.$id),\n label: String(member.displayName || member.email || member.$id),\n email: member.email ? String(member.email) : undefined,\n }))\n .sort((a, b) => a.label.localeCompare(b.label))\n}\n\n/**\n * THE ORG'S TEAM, for an owner picker on some record's page (AGL-2603).\n *\n * ## Through the members route, not the collection\n *\n * `orgs/{orgId}/members` is readable by an org-wide member and by nobody\n * else: the rules evaluate a list against the QUERY, so a collaborator scoped\n * to one site reading the roster gets `permission-denied`, not a shorter\n * roster. `GET /api/orgs/members?orgId=` re-derives membership with the Admin\n * SDK and answers any member of the org, which is who assigns an owner.\n *\n * ## It is OFF unless a caller asks\n *\n * `enabled` defaults to false, the way `useHostCampaigns` does and for the\n * same reason: the picker this feeds sits on surfaces a reader opens for\n * something else, and a roster fetched on every arrival at the contacts list\n * is a request for a control nobody has opened. Callers turn it on when the\n * owner control is actually on screen.\n *\n * ## A refusal is reported, never rendered as an empty team\n *\n * An org with no members is impossible — it has an owner — so an empty list\n * with no error would always be a lie. `authorizedFetch` refuses to send a\n * request it cannot authorize and answers a 401 carrying the reason; that\n * reason, or the route's own, is what `error` holds.\n */\nexport function useOrgMemberOptions(\n orgId: string | undefined,\n options?: { enabled?: boolean },\n): OrgMemberOptions {\n const enabled = options?.enabled ?? false\n const { data: user } = useUser()\n const [state, setState] = useState<{\n orgId: string | null\n members: OrgMemberOption[]\n error: string | null\n }>({ orgId: null, members: [], error: null })\n\n useEffect(() => {\n if (!enabled || !orgId) return\n let active = true\n void (async () => {\n let members: OrgMemberOption[] = []\n let error: string | null = null\n try {\n const response = await authorizedFetch(\n user,\n `/api/orgs/members?orgId=${encodeURIComponent(orgId)}`,\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n error = String(payload?.error ?? 'The team could not be read.')\n } else {\n members = orgMemberOptions(\n Array.isArray(payload?.members) ? payload.members : [],\n )\n }\n } catch {\n error = 'The team could not be read.'\n }\n if (active) setState({ orgId, members, error })\n })()\n return () => {\n active = false\n }\n }, [enabled, orgId, user])\n\n return useMemo(\n () => ({\n options: state.orgId === orgId ? state.members : [],\n ready: enabled && Boolean(orgId) && state.orgId === orgId,\n error: state.orgId === orgId ? state.error : null,\n }),\n [state, orgId, enabled],\n )\n}\n\nexport default useOrgMemberOptions\n"],"names":["authorizedFetch","useEffect","useMemo","useState","useUser","orgMemberOptions","members","map","member","uid","String","$id","label","displayName","email","undefined","sort","a","b","localeCompare","useOrgMemberOptions","orgId","options","enabled","data","user","state","setState","error","active","response","encodeURIComponent","payload","json","catch","ok","Array","isArray","ready","Boolean"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAASA,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,OAAO,QAAQ,kCAA8B;AAmBtD;;;;;;;CAOC,GACD,OAAO,SAASC,iBACdC,OAAiE;IAEjE,OAAOA,QACJC,GAAG,CAAC,CAACC,SAAY,CAAA;YAChBC,KAAKC,OAAOF,OAAOG,GAAG;YACtBC,OAAOF,OAAOF,OAAOK,WAAW,IAAIL,OAAOM,KAAK,IAAIN,OAAOG,GAAG;YAC9DG,OAAON,OAAOM,KAAK,GAAGJ,OAAOF,OAAOM,KAAK,IAAIC;QAC/C,CAAA,GACCC,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEL,KAAK,CAACO,aAAa,CAACD,EAAEN,KAAK;AACjD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,SAASQ,oBACdC,KAAyB,EACzBC,OAA+B;;IAE/B,MAAMC,kBAAUD,2BAAAA,QAASC,OAAO,mBAAI;IACpC,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGrB;IACvB,MAAM,CAACsB,OAAOC,SAAS,GAAGxB,SAIvB;QAAEkB,OAAO;QAAMf,SAAS,EAAE;QAAEsB,OAAO;IAAK;IAE3C3B,UAAU;QACR,IAAI,CAACsB,WAAW,CAACF,OAAO;QACxB,IAAIQ,SAAS;QACb,KAAK,AAAC,CAAA;YACJ,IAAIvB,UAA6B,EAAE;YACnC,IAAIsB,QAAuB;YAC3B,IAAI;gBACF,MAAME,WAAW,MAAM9B,gBACrByB,MACA,CAAC,wBAAwB,EAAEM,mBAAmBV,QAAQ;gBAExD,MAAMW,UAAU,MAAMF,SAASG,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;gBACpD,IAAI,CAACJ,SAASK,EAAE,EAAE;;oBAChBP,QAAQlB,eAAOsB,2BAAAA,QAASJ,KAAK,mBAAI;gBACnC,OAAO;oBACLtB,UAAUD,iBACR+B,MAAMC,OAAO,CAACL,2BAAAA,QAAS1B,OAAO,IAAI0B,QAAQ1B,OAAO,GAAG,EAAE;gBAE1D;YACF,EAAE,eAAM;gBACNsB,QAAQ;YACV;YACA,IAAIC,QAAQF,SAAS;gBAAEN;gBAAOf;gBAASsB;YAAM;QAC/C,CAAA;QACA,OAAO;YACLC,SAAS;QACX;IACF,GAAG;QAACN;QAASF;QAAOI;KAAK;IAEzB,OAAOvB,QACL,IAAO,CAAA;YACLoB,SAASI,MAAML,KAAK,KAAKA,QAAQK,MAAMpB,OAAO,GAAG,EAAE;YACnDgC,OAAOf,WAAWgB,QAAQlB,UAAUK,MAAML,KAAK,KAAKA;YACpDO,OAAOF,MAAML,KAAK,KAAKA,QAAQK,MAAME,KAAK,GAAG;QAC/C,CAAA,GACA;QAACF;QAAOL;QAAOE;KAAQ;AAE3B;AAEA,eAAeH,oBAAmB"}
@@ -0,0 +1,46 @@
1
+ export interface OrgPlanState {
2
+ /** The owning org's doc, once it has arrived. */
3
+ org: Record<string, unknown> | undefined;
4
+ /**
5
+ * Whether `org` is trustworthy. FALSE means the answer is still in
6
+ * flight — hold rather than resolve entitlements, because an absent org
7
+ * is indistinguishable from the free tier to every `Aglyn.checkQuota` /
8
+ * `checkEntitlement` call.
9
+ */
10
+ ready: boolean;
11
+ }
12
+ /**
13
+ * A host's owning org doc, for plan quotas and entitlements, with an honest
14
+ * `ready` flag (AGL-1064).
15
+ *
16
+ * This existed as a copy-pasted pair in five commerce console cards:
17
+ *
18
+ * ```ts
19
+ * const orgId = useHostOrgId(hostId)
20
+ * const { data: org } = useFirestoreDoc(
21
+ * () => doc(firestore, 'orgs', orgId ?? '-pending-'), [firestore, orgId])
22
+ * const quota = Aglyn.checkQuota(org, 'posRegisters', registers.length)
23
+ * ```
24
+ *
25
+ * `useHostOrgId` returns null while the `hostIndex` lookup is in flight, so
26
+ * `org` is undefined for the first render or two of every mount — and
27
+ * `checkQuota(undefined, …)` does not mean "unknown", it resolves the FREE
28
+ * tier (`posRegisters: 0`, `productsPerHost: 0`, `inventoryLocations: 1`).
29
+ * A paying customer clicking Add inside that window was told their plan does
30
+ * not include what they bought.
31
+ *
32
+ * Same defect shape as {@link useOrgDataScope} (AGL-1061) and
33
+ * `useScopeTokens` (AGL-1047): a hook handing out a resolved-looking value
34
+ * before it has resolved. Callers gate their controls on `ready` rather than
35
+ * letting an absent org answer a billing question.
36
+ *
37
+ * The `'-pending-'` sentinel goes with it. Addressing `orgs/-pending-` was a
38
+ * guaranteed-denied read on every mount; this holds the ref at null instead,
39
+ * so no query is issued until there is a real org to ask about.
40
+ *
41
+ * Not to be confused with the console's `useOrgPlans` (plural), which reads
42
+ * the plan of every org the user belongs to for the switcher. This one is
43
+ * the single owning org of a HOST.
44
+ */
45
+ export declare function useOrgPlan(hostId: string | undefined): OrgPlanState;
46
+ export default useOrgPlan;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { doc } from "firebase/firestore";
18
+ import { useMemo } from "react";
19
+ import { useFirestore } from "./firebase/firebase-services.js";
20
+ import { useFirestoreDoc } from "./use-firestore-doc.js";
21
+ import { useHostOrgIdState } from "./use-host-org-id.js";
22
+ /**
23
+ * A host's owning org doc, for plan quotas and entitlements, with an honest
24
+ * `ready` flag (AGL-1064).
25
+ *
26
+ * This existed as a copy-pasted pair in five commerce console cards:
27
+ *
28
+ * ```ts
29
+ * const orgId = useHostOrgId(hostId)
30
+ * const { data: org } = useFirestoreDoc(
31
+ * () => doc(firestore, 'orgs', orgId ?? '-pending-'), [firestore, orgId])
32
+ * const quota = Aglyn.checkQuota(org, 'posRegisters', registers.length)
33
+ * ```
34
+ *
35
+ * `useHostOrgId` returns null while the `hostIndex` lookup is in flight, so
36
+ * `org` is undefined for the first render or two of every mount — and
37
+ * `checkQuota(undefined, …)` does not mean "unknown", it resolves the FREE
38
+ * tier (`posRegisters: 0`, `productsPerHost: 0`, `inventoryLocations: 1`).
39
+ * A paying customer clicking Add inside that window was told their plan does
40
+ * not include what they bought.
41
+ *
42
+ * Same defect shape as {@link useOrgDataScope} (AGL-1061) and
43
+ * `useScopeTokens` (AGL-1047): a hook handing out a resolved-looking value
44
+ * before it has resolved. Callers gate their controls on `ready` rather than
45
+ * letting an absent org answer a billing question.
46
+ *
47
+ * The `'-pending-'` sentinel goes with it. Addressing `orgs/-pending-` was a
48
+ * guaranteed-denied read on every mount; this holds the ref at null instead,
49
+ * so no query is issued until there is a real org to ask about.
50
+ *
51
+ * Not to be confused with the console's `useOrgPlans` (plural), which reads
52
+ * the plan of every org the user belongs to for the switcher. This one is
53
+ * the single owning org of a HOST.
54
+ */ export function useOrgPlan(hostId) {
55
+ const firestore = useFirestore();
56
+ const { orgId, loaded } = useHostOrgIdState(hostId);
57
+ const { data: org, status } = useFirestoreDoc(()=>orgId ? doc(firestore, 'orgs', orgId) : null, [
58
+ firestore,
59
+ orgId
60
+ ]);
61
+ return useMemo(()=>({
62
+ org,
63
+ // A host with no owning org is SETTLED, not pending — there is no
64
+ // org doc coming, and holding forever would leave the card
65
+ // permanently disabled. `status` only moves off 'loading' once a
66
+ // ref exists, so it cannot answer that case.
67
+ //
68
+ // `'success'` and not `!== 'loading'` (AGL-1066). A refused listen can
69
+ // now reach `'error'`, and `'error'` satisfies `!== 'loading'` while
70
+ // leaving `org` undefined — so this hook would have handed out
71
+ // `ready: true` with no org, and `checkQuota(undefined, …)` does not
72
+ // mean "unknown", it resolves the FREE tier. That is the exact fault
73
+ // this hook was written to close, arriving through a different door:
74
+ // a paying org rendered as Free for as long as its session stayed
75
+ // stale. Pending is the right answer to a question we cannot read.
76
+ ready: loaded && (!orgId || status === 'success')
77
+ }), [
78
+ org,
79
+ loaded,
80
+ orgId,
81
+ status
82
+ ]);
83
+ }
84
+ export default useOrgPlan;
85
+
86
+ //# sourceMappingURL=use-org-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-org-plan.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { doc } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from './firebase/firebase-services'\nimport { useFirestoreDoc } from './use-firestore-doc'\nimport { useHostOrgIdState } from './use-host-org-id'\n\nexport interface OrgPlanState {\n /** The owning org's doc, once it has arrived. */\n org: Record<string, unknown> | undefined\n /**\n * Whether `org` is trustworthy. FALSE means the answer is still in\n * flight — hold rather than resolve entitlements, because an absent org\n * is indistinguishable from the free tier to every `Aglyn.checkQuota` /\n * `checkEntitlement` call.\n */\n ready: boolean\n}\n\n/**\n * A host's owning org doc, for plan quotas and entitlements, with an honest\n * `ready` flag (AGL-1064).\n *\n * This existed as a copy-pasted pair in five commerce console cards:\n *\n * ```ts\n * const orgId = useHostOrgId(hostId)\n * const { data: org } = useFirestoreDoc(\n * () => doc(firestore, 'orgs', orgId ?? '-pending-'), [firestore, orgId])\n * const quota = Aglyn.checkQuota(org, 'posRegisters', registers.length)\n * ```\n *\n * `useHostOrgId` returns null while the `hostIndex` lookup is in flight, so\n * `org` is undefined for the first render or two of every mount — and\n * `checkQuota(undefined, …)` does not mean \"unknown\", it resolves the FREE\n * tier (`posRegisters: 0`, `productsPerHost: 0`, `inventoryLocations: 1`).\n * A paying customer clicking Add inside that window was told their plan does\n * not include what they bought.\n *\n * Same defect shape as {@link useOrgDataScope} (AGL-1061) and\n * `useScopeTokens` (AGL-1047): a hook handing out a resolved-looking value\n * before it has resolved. Callers gate their controls on `ready` rather than\n * letting an absent org answer a billing question.\n *\n * The `'-pending-'` sentinel goes with it. Addressing `orgs/-pending-` was a\n * guaranteed-denied read on every mount; this holds the ref at null instead,\n * so no query is issued until there is a real org to ask about.\n *\n * Not to be confused with the console's `useOrgPlans` (plural), which reads\n * the plan of every org the user belongs to for the switcher. This one is\n * the single owning org of a HOST.\n */\nexport function useOrgPlan(hostId: string | undefined): OrgPlanState {\n const firestore = useFirestore()\n const { orgId, loaded } = useHostOrgIdState(hostId)\n const { data: org, status } = useFirestoreDoc<Record<string, unknown>>(\n () => (orgId ? doc(firestore, 'orgs', orgId) : null),\n [firestore, orgId],\n )\n return useMemo(\n () => ({\n org,\n // A host with no owning org is SETTLED, not pending — there is no\n // org doc coming, and holding forever would leave the card\n // permanently disabled. `status` only moves off 'loading' once a\n // ref exists, so it cannot answer that case.\n //\n // `'success'` and not `!== 'loading'` (AGL-1066). A refused listen can\n // now reach `'error'`, and `'error'` satisfies `!== 'loading'` while\n // leaving `org` undefined — so this hook would have handed out\n // `ready: true` with no org, and `checkQuota(undefined, …)` does not\n // mean \"unknown\", it resolves the FREE tier. That is the exact fault\n // this hook was written to close, arriving through a different door:\n // a paying org rendered as Free for as long as its session stayed\n // stale. Pending is the right answer to a question we cannot read.\n ready: loaded && (!orgId || status === 'success'),\n }),\n [org, loaded, orgId, status],\n )\n}\n\nexport default useOrgPlan\n"],"names":["doc","useMemo","useFirestore","useFirestoreDoc","useHostOrgIdState","useOrgPlan","hostId","firestore","orgId","loaded","data","org","status","ready"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,GAAG,QAAQ,qBAAoB;AACxC,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,kCAA8B;AAC3D,SAASC,eAAe,QAAQ,yBAAqB;AACrD,SAASC,iBAAiB,QAAQ,uBAAmB;AAcrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,SAASC,WAAWC,MAA0B;IACnD,MAAMC,YAAYL;IAClB,MAAM,EAAEM,KAAK,EAAEC,MAAM,EAAE,GAAGL,kBAAkBE;IAC5C,MAAM,EAAEI,MAAMC,GAAG,EAAEC,MAAM,EAAE,GAAGT,gBAC5B,IAAOK,QAAQR,IAAIO,WAAW,QAAQC,SAAS,MAC/C;QAACD;QAAWC;KAAM;IAEpB,OAAOP,QACL,IAAO,CAAA;YACLU;YACA,kEAAkE;YAClE,2DAA2D;YAC3D,iEAAiE;YACjE,6CAA6C;YAC7C,EAAE;YACF,uEAAuE;YACvE,qEAAqE;YACrE,+DAA+D;YAC/D,qEAAqE;YACrE,qEAAqE;YACrE,qEAAqE;YACrE,kEAAkE;YAClE,mEAAmE;YACnEE,OAAOJ,UAAW,CAAA,CAACD,SAASI,WAAW,SAAQ;QACjD,CAAA,GACA;QAACD;QAAKF;QAAQD;QAAOI;KAAO;AAEhC;AAEA,eAAeP,WAAU"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { DocumentData, Query } from 'firebase/firestore';
18
+ import { type DependencyList } from 'react';
19
+ import { type UseFirestoreCollectionOptions, type UseFirestoreCollectionResult } from './use-firestore-collection';
20
+ export interface UsePagedCollectionOptions extends UseFirestoreCollectionOptions {
21
+ /**
22
+ * Rows per page. Defaults to the console-wide smallest option — a list
23
+ * that picks its own number is how five different ones appear, and on a
24
+ * query bounded by its page size the smallest page is the smallest bill.
25
+ */
26
+ pageSize?: number;
27
+ }
28
+ export interface UsePagedCollectionResult<T> extends UseFirestoreCollectionResult<T> {
29
+ /** The current page's rows. `data` holds the probe row; this never does. */
30
+ rows: T[];
31
+ /** A further page exists. A FACT from the probe row, not a guess. */
32
+ hasMore: boolean;
33
+ /** Zero-based, to match `ListPagination` and MUI. */
34
+ page: number;
35
+ setPage: (page: number) => void;
36
+ pageSize: number;
37
+ setPageSize: (pageSize: number) => void;
38
+ }
39
+ /**
40
+ * A paged window over a live collection, instead of a big read sliced small.
41
+ *
42
+ * ## The shape this replaces
43
+ *
44
+ * A card would listen with `limit(300)` and render `rows.slice(0, 50)`. That
45
+ * is wrong twice over. Two hundred and fifty documents are read, billed and
46
+ * discarded on every mount — and the rows past the slice are not merely
47
+ * wasted, they are UNREACHABLE, because nothing renders them and no control
48
+ * asks for more. A merchant with sixty gift cards sees fifty and no
49
+ * indication that ten more exist; past three hundred, a card issued this
50
+ * morning cannot be reached at all.
51
+ *
52
+ * Here the window IS the query, and it is expressed in PAGES so the same
53
+ * `ListPagination` footer serves this and a cursor feed. The listener is
54
+ * widened to cover every page up to the one being read, so paging BACK costs
55
+ * nothing — the rows are already in the snapshot — and paging forward costs
56
+ * one page more.
57
+ *
58
+ * ## Why the window covers page 0..n rather than page n alone
59
+ *
60
+ * This is a live `onSnapshot`, not a fetch. Firestore cursors need a document
61
+ * snapshot to resume from, which a listener that has never read page n - 1
62
+ * does not have. Widening the limit is what makes forward and backward
63
+ * symmetrical without a second read path, and the cost is bounded by how deep
64
+ * the reader actually goes — with the shared default of ten, page three is
65
+ * thirty-one documents, and most readers never leave page one.
66
+ *
67
+ * ## Why over-fetch by one
68
+ *
69
+ * `hasMore` has to be a fact. Comparing `length === limit` is wrong exactly
70
+ * when the count is an even multiple of the page size — the case that offers
71
+ * a "next page" leading nowhere, or hides one that leads somewhere. The probe
72
+ * row answers it outright and is never rendered.
73
+ *
74
+ * ## Why the window resets on `deps`
75
+ *
76
+ * `deps` identify the SUBJECT being listened to. Without the reset, a card
77
+ * paged three deep on one site opens the next site three pages in — a read
78
+ * the reader never asked for, charged to whoever they switched to.
79
+ *
80
+ * `buildQuery` receives the limit to apply, so the caller keeps ownership of
81
+ * ordering and predicates:
82
+ *
83
+ * ```ts
84
+ * usePagedCollection<Card>(
85
+ * (pageLimit) =>
86
+ * hostId
87
+ * ? query(collection(firestore, 'hosts', hostId, 'giftCards'),
88
+ * orderBy('createdAt', 'desc'), limit(pageLimit))
89
+ * : null,
90
+ * [firestore, hostId],
91
+ * { idField: '$id' },
92
+ * )
93
+ * ```
94
+ */
95
+ export declare function usePagedCollection<T = DocumentData>(buildQuery: (pageLimit: number) => Query<DocumentData> | null | undefined, deps: DependencyList, options?: UsePagedCollectionOptions): UsePagedCollectionResult<T>;