@wildo-ai/saas-website 1.1.4 → 1.1.6

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 (265) hide show
  1. package/dist/esm/astro/blog-post-bridge.d.ts +23 -20
  2. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
  3. package/dist/esm/astro/blog-post-bridge.js +13 -6
  4. package/dist/esm/astro/blog-post-bridge.js.map +1 -1
  5. package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -1
  6. package/dist/esm/astro/bridge-runtime.d.ts +55 -26
  7. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
  8. package/dist/esm/astro/bridge-runtime.js +21 -7
  9. package/dist/esm/astro/bridge-runtime.js.map +1 -1
  10. package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -1
  11. package/dist/esm/astro/headers-renderer.d.ts.map +1 -1
  12. package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -1
  13. package/dist/esm/astro/label-pack-loader.d.ts +4 -4
  14. package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
  15. package/dist/esm/astro/label-pack-loader.js +14 -4
  16. package/dist/esm/astro/label-pack-loader.js.map +1 -1
  17. package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
  18. package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
  19. package/dist/esm/astro/robots-renderer.d.ts +9 -8
  20. package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
  21. package/dist/esm/astro/robots-renderer.js +15 -15
  22. package/dist/esm/astro/robots-renderer.js.map +1 -1
  23. package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
  24. package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
  25. package/dist/esm/astro/website-page-head.renderer.d.ts +1 -1
  26. package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
  27. package/dist/esm/astro/website-page-head.renderer.js +2 -1
  28. package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
  29. package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
  30. package/dist/esm/astro/website-site-context.d.ts +5 -2
  31. package/dist/esm/astro/website-site-context.d.ts.map +1 -1
  32. package/dist/esm/astro/website-site-context.js.map +1 -1
  33. package/dist/esm/astro-island.d.ts.map +1 -1
  34. package/dist/esm/astro.d.ts.map +1 -1
  35. package/dist/esm/companion-exports.d.ts +8 -15
  36. package/dist/esm/companion-exports.d.ts.map +1 -1
  37. package/dist/esm/companion-exports.js +8 -15
  38. package/dist/esm/companion-exports.js.map +1 -1
  39. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
  40. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
  41. package/dist/esm/config/define-website-config.d.ts +3 -2
  42. package/dist/esm/config/define-website-config.d.ts.map +1 -1
  43. package/dist/esm/config/define-website-config.js +10 -10
  44. package/dist/esm/config/define-website-config.js.map +1 -1
  45. package/dist/esm/config/index.d.ts.map +1 -1
  46. package/dist/esm/config/load-website-config.d.ts +6 -0
  47. package/dist/esm/config/load-website-config.d.ts.map +1 -1
  48. package/dist/esm/config/load-website-config.js +20 -10
  49. package/dist/esm/config/load-website-config.js.map +1 -1
  50. package/dist/esm/config/load-website-pricing-catalog.d.ts +13 -0
  51. package/dist/esm/config/load-website-pricing-catalog.d.ts.map +1 -0
  52. package/dist/esm/config/load-website-pricing-catalog.js +34 -0
  53. package/dist/esm/config/load-website-pricing-catalog.js.map +1 -0
  54. package/dist/esm/config/load-website-site-context.d.ts +53 -0
  55. package/dist/esm/config/load-website-site-context.d.ts.map +1 -0
  56. package/dist/esm/config/load-website-site-context.js +98 -0
  57. package/dist/esm/config/load-website-site-context.js.map +1 -0
  58. package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
  59. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
  60. package/dist/esm/config/wildo-website-config.schemas.js +10 -5
  61. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
  62. package/dist/esm/config-loader.d.ts +2 -0
  63. package/dist/esm/config-loader.d.ts.map +1 -1
  64. package/dist/esm/config-loader.js +2 -0
  65. package/dist/esm/config-loader.js.map +1 -1
  66. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
  67. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
  68. package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
  69. package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
  70. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +14 -3
  71. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
  72. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +28 -13
  73. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
  74. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +24 -1
  75. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
  76. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +46 -3
  77. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
  78. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
  79. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
  80. package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
  81. package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
  82. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
  83. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
  84. package/dist/esm/core/consent/WebsiteConsentBanner.js +129 -0
  85. package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
  86. package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
  87. package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
  88. package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
  89. package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
  90. package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
  91. package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
  92. package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
  93. package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
  94. package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
  95. package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
  96. package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
  97. package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
  98. package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
  99. package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
  100. package/dist/esm/core/consent/website-consent-storage.js +102 -0
  101. package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
  102. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
  103. package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
  104. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +33 -6
  105. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
  106. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
  107. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
  108. package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
  109. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
  110. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
  111. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
  112. package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
  113. package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
  114. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
  115. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
  116. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
  117. package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
  118. package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
  119. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
  120. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
  121. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
  122. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
  123. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
  124. package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
  125. package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
  126. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
  127. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
  128. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
  129. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
  130. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +43 -10
  131. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
  132. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +92 -29
  133. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
  134. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
  135. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
  136. package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
  137. package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
  138. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
  139. package/dist/esm/core/factories/define-website-section.d.ts +3 -3
  140. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
  141. package/dist/esm/core/factories/define-website-section.js +3 -3
  142. package/dist/esm/core/factories/define-website-section.js.map +1 -1
  143. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
  144. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
  145. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
  146. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
  147. package/dist/esm/core/layouts/WebsitePageLayout.js +39 -8
  148. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
  149. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
  150. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
  151. package/dist/esm/index.d.ts +36 -65
  152. package/dist/esm/index.d.ts.map +1 -1
  153. package/dist/esm/index.js +36 -65
  154. package/dist/esm/index.js.map +1 -1
  155. package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
  156. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
  157. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
  158. package/dist/esm/mdx/index.d.ts.map +1 -1
  159. package/dist/esm/mdx.d.ts.map +1 -1
  160. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
  161. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +35 -6
  162. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
  163. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
  164. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
  165. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
  166. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
  167. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
  168. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
  169. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
  170. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
  171. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
  172. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
  173. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
  174. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
  175. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +6 -5
  176. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
  177. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
  178. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -1
  179. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
  180. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
  181. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
  182. package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
  183. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +14 -7
  184. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
  185. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
  186. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
  187. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
  188. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
  189. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
  190. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
  191. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
  192. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
  193. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
  194. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
  195. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
  196. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
  197. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -1
  198. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
  199. package/dist/tsconfig.build.tsbuildinfo +1 -1
  200. package/package.json +6 -29
  201. package/src/__tests__/bundle-isolation.test.ts +19 -22
  202. package/src/astro/__tests__/blog-post-bridge.test.tsx +58 -1
  203. package/src/astro/__tests__/bridge-runtime.test.tsx +44 -1
  204. package/src/astro/__tests__/label-pack-loader.test.ts +29 -1
  205. package/src/astro/blog-post-bridge.tsx +36 -26
  206. package/src/astro/bridge-runtime.tsx +79 -33
  207. package/src/astro/label-pack-loader.ts +12 -4
  208. package/src/astro/robots-renderer.ts +15 -15
  209. package/src/astro/website-page-head.renderer.ts +2 -2
  210. package/src/astro/website-site-context.ts +5 -2
  211. package/src/companion-exports.ts +8 -15
  212. package/src/config/__tests__/website-build-origins.test.ts +52 -0
  213. package/src/config/define-website-config.ts +10 -10
  214. package/src/config/load-website-config.ts +27 -15
  215. package/src/config/load-website-pricing-catalog.ts +52 -0
  216. package/src/config/load-website-site-context.ts +145 -0
  217. package/src/config/wildo-website-config.schemas.ts +10 -5
  218. package/src/config-loader.ts +2 -0
  219. package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
  220. package/src/core/__tests__/contexts.test.tsx +20 -1
  221. package/src/core/__tests__/website-consent.test.tsx +312 -0
  222. package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
  223. package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
  224. package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
  225. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
  226. package/src/core/anonymous-session/inbound-contact-form.schema.ts +30 -13
  227. package/src/core/anonymous-session/website-anonymous-session-client.ts +52 -3
  228. package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
  229. package/src/core/consent/WebsiteConsentBanner.tsx +223 -0
  230. package/src/core/consent/WebsiteConsentContext.tsx +164 -0
  231. package/src/core/consent/WebsiteConsentGate.tsx +34 -0
  232. package/src/core/consent/useWebsiteConsent.ts +18 -0
  233. package/src/core/consent/website-consent-storage.ts +116 -0
  234. package/src/core/contexts/WebsiteRuntimeContext.tsx +33 -6
  235. package/src/core/contexts/useWebsiteRuntime.ts +33 -2
  236. package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
  237. package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
  238. package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
  239. package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
  240. package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
  241. package/src/core/external-providers/provider-component-registry.website.ts +115 -0
  242. package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
  243. package/src/core/external-providers/useWebsiteProviderScripts.ts +114 -40
  244. package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
  245. package/src/core/factories/define-website-section.ts +3 -3
  246. package/src/core/layouts/WebsitePageLayout.tsx +75 -7
  247. package/src/index.ts +38 -65
  248. package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
  249. package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
  250. package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
  251. package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
  252. package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +83 -0
  253. package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
  254. package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
  255. package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
  256. package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
  257. package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
  258. package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
  259. package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
  260. package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
  261. package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
  262. package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
  263. package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
  264. package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
  265. package/src/core/external-providers/provider-scripts.website.ts +0 -126
@@ -0,0 +1,238 @@
1
+ /**
2
+ * A provider component on the website (#1703), through the real page bridge.
3
+ *
4
+ * What this pins, in order of how badly each would fail:
5
+ *
6
+ * 1. The server render ships NOTHING of the vendor — the loader is never called, and the HTML holds
7
+ * only the reserved box. Astro renders every island on the server; a component that ran there
8
+ * would put vendor markup into HTML a visitor downloads before consenting to anything.
9
+ * 2. One declaration serves the website: the provider module's `components[capability].load`, the
10
+ * same field the application reads, is what renders here.
11
+ * 3. A consent-based provider downloads nothing until the visitor agrees, shows an announced,
12
+ * labelled refusal with a real button, and loads in place once the visitor grants the purpose.
13
+ * 4. A failed load says so, in the site's words.
14
+ */
15
+ import React from 'react';
16
+ import { renderToString } from 'react-dom/server';
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
18
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
19
+ import '@testing-library/jest-dom';
20
+ import {
21
+ AvailableLanguage,
22
+ ConsentPurpose,
23
+ ENGINE_PROVIDER_ORIGIN,
24
+ ProviderAddressLookupPlaceIdRetention,
25
+ ProviderTier,
26
+ defineFrontendPublicConfigSchema,
27
+ defineProviderMetadata,
28
+ type FrontendProvidersBlock,
29
+ type ProviderCapability,
30
+ } from '@wildo-ai/saas-models/public-runtime';
31
+ import { ProviderComponentStatus, type GeneratedWebsiteProviderModules } from '@wildo-ai/external-connectors-public/website';
32
+
33
+ import { createWebsitePageBridgeIsland } from '../../../astro/bridge-runtime';
34
+ import { WebsiteLabelKeySchema } from '../../../schemas/label-keys/website-label-key.schemas';
35
+ import {
36
+ WEBSITE_CONSENT_LABEL_KEYS,
37
+ WebsiteConsentLabelSlot,
38
+ websiteConsentLabelKey,
39
+ websiteConsentPurposeLabelKey,
40
+ } from '../../../schemas/label-keys/website-consent-label-keys.schemas';
41
+ import {
42
+ WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS,
43
+ WebsiteProviderComponentLabelSlot,
44
+ websiteProviderComponentLabelKey,
45
+ } from '../../../schemas/label-keys/website-provider-component-label-keys.schemas';
46
+ import type { WebsitePageRef } from '../../../schemas/refs/page-ref.schemas';
47
+ import { writeWebsiteVisitorConsent } from '../../consent/website-consent-storage';
48
+ import { WebsiteProviderComponent } from '../WebsiteProviderComponent';
49
+
50
+ const LOOKUP = 'FRONTEND_ADDRESS_LOOKUP' as ProviderCapability;
51
+ const SKIP_KEY = WebsiteLabelKeySchema.parse('website.chrome.layout.skip-to-main-content');
52
+ // Every framework-owned key resolves to itself, so the rendered states are found by the key they show.
53
+ const LABEL_PACK = Object.fromEntries(
54
+ [SKIP_KEY, ...WEBSITE_CONSENT_LABEL_KEYS, ...WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS].map((key) => [key as unknown as string, key as unknown as string]),
55
+ );
56
+ const text = (key: unknown): string => key as string;
57
+
58
+ const DESIGN_TOKENS = {
59
+ colors: {
60
+ background: '#fff', foreground: '#000', primary: '#000', primaryForeground: '#fff', accent: '#000', accentForeground: '#fff',
61
+ surface: '#fff', surfaceForeground: '#000', border: '#ccc', muted: '#eee', mutedForeground: '#666',
62
+ },
63
+ radii: { none: '0', sm: '2px', md: '4px', lg: '8px', xl: '12px', full: '9999px' },
64
+ };
65
+
66
+ /** The vendor's component: renders the prop the page passed, so the test can see it arrived. */
67
+ function PlacesLookup({ fieldLabel }: { fieldLabel: string }): React.ReactNode {
68
+ return <input aria-label={fieldLabel} data-testid="places-lookup" />;
69
+ }
70
+
71
+ const metadata = defineProviderMetadata({ ref: 'places', packageName: '@wildo-ai/provider-places', tier: ProviderTier.CATALOGUE, origin: ENGINE_PROVIDER_ORIGIN });
72
+
73
+ /** The DATA and CODE channels for one provider declaring ONE address-lookup component. */
74
+ function site(input: { load: () => Promise<unknown>; consentPurposes?: ConsentPurpose[]; scripts?: Array<{ src: string }>; withComponent?: boolean }): {
75
+ entries: FrontendProvidersBlock;
76
+ modules: GeneratedWebsiteProviderModules;
77
+ } {
78
+ const descriptor = { placeIdRetention: ProviderAddressLookupPlaceIdRetention.NONE };
79
+ const common = {
80
+ metadata,
81
+ providerCapabilities: [LOOKUP],
82
+ protocols: ['FRONTEND_SDK'],
83
+ ...(input.consentPurposes !== undefined ? { consentPurposes: input.consentPurposes } : {}),
84
+ ...(input.scripts !== undefined ? { scripts: input.scripts } : {}),
85
+ };
86
+ const withComponent = input.withComponent ?? true;
87
+ return {
88
+ entries: { providers: { places: { ...common, ...(withComponent ? { components: { [LOOKUP]: descriptor } } : {}) } } } as unknown as FrontendProvidersBlock,
89
+ modules: {
90
+ places: {
91
+ ...common,
92
+ ...(withComponent ? { components: { [LOOKUP]: { ...descriptor, load: input.load } } } : {}),
93
+ publicConfigSchema: defineFrontendPublicConfigSchema({}),
94
+ },
95
+ } as unknown as GeneratedWebsiteProviderModules,
96
+ };
97
+ }
98
+
99
+ function ContactPage(): React.ReactNode {
100
+ return (
101
+ <form>
102
+ <WebsiteProviderComponent capability={LOOKUP} providerRef="places" componentProps={{ fieldLabel: 'Address' }} reservedMinHeight="3rem" />
103
+ </form>
104
+ );
105
+ }
106
+
107
+ function EmptyPage(): React.ReactNode {
108
+ return <p>No form on this page</p>;
109
+ }
110
+
111
+ function island(channels: ReturnType<typeof site>, PageComponent: () => React.ReactNode = ContactPage): React.ReactElement {
112
+ const Island = createWebsitePageBridgeIsland({
113
+ PageComponent,
114
+ pageRef: 'contact' as unknown as WebsitePageRef,
115
+ navigation: { headerComponent: () => null, footerComponent: () => null },
116
+ frontendProviderModules: channels.modules,
117
+ });
118
+ return (
119
+ <Island
120
+ locale={AvailableLanguage.EN}
121
+ defaultLocale={AvailableLanguage.EN}
122
+ labelPack={LABEL_PACK}
123
+ designTokens={DESIGN_TOKENS as never}
124
+ skipToMainContentLabelKey={SKIP_KEY}
125
+ frontendProviders={channels.entries}
126
+ />
127
+ );
128
+ }
129
+
130
+ const SCRIPT_SRC = 'https://maps.vendor.example/places.js';
131
+ const injectedScripts = (): string[] => [...document.querySelectorAll('script')].map((script) => script.getAttribute('src') ?? '').filter((src) => src === SCRIPT_SRC);
132
+
133
+ beforeEach(() => {
134
+ window.localStorage.clear();
135
+ for (const script of document.querySelectorAll('script')) script.remove();
136
+ });
137
+ afterEach(cleanup);
138
+
139
+ describe('a provider component on the website (#1703)', () => {
140
+ it('ships NOTHING of the vendor in the server render — only the reserved box', async () => {
141
+ const load = vi.fn(async () => PlacesLookup);
142
+
143
+ const html = renderToString(island(site({ load })));
144
+ // The registry starts a loader on a microtask; wait one macrotask so a load begun by the render is seen.
145
+ await new Promise((resolve) => setTimeout(resolve, 0));
146
+
147
+ expect(load).not.toHaveBeenCalled();
148
+ expect(html).toContain(`data-website-provider-component="${LOOKUP}"`);
149
+ expect(html).toContain(`data-website-provider-component-state="${ProviderComponentStatus.NONE}"`);
150
+ expect(html).toContain('min-height:3rem');
151
+ expect(html).not.toContain('places-lookup');
152
+ });
153
+
154
+ it('has its live region in the FIRST render, before anything is announced', () => {
155
+ // A screen reader announces changes to a live region it already knows about; a region inserted together with its
156
+ // text is often read late or not at all. So the empty role="status" must exist in the server render, in the NONE
157
+ // state, before any refusal can be written into it (#1703 review F3).
158
+ const html = renderToString(island(site({ load: vi.fn(async () => PlacesLookup) })));
159
+
160
+ expect(html).toContain(`data-website-provider-component-state="${ProviderComponentStatus.NONE}"`);
161
+ expect(html).toMatch(/<div role="status"><\/div>/);
162
+ });
163
+
164
+ it('renders the component the provider DECLARED, with the props the page passed, in the browser', async () => {
165
+ const load = vi.fn(async () => PlacesLookup);
166
+
167
+ render(island(site({ load })));
168
+
169
+ expect(await screen.findByLabelText('Address')).toBeInTheDocument();
170
+ expect(load).toHaveBeenCalledTimes(1);
171
+ });
172
+
173
+ it('downloads nothing until the visitor agrees, says so accessibly, and loads in place once they do', async () => {
174
+ const load = vi.fn(async () => PlacesLookup);
175
+
176
+ render(island(site({ load, consentPurposes: [ConsentPurpose.MARKETING] })));
177
+ // The visitor declines everything in the panel the layout shows.
178
+ fireEvent.click(await screen.findByText(text(websiteConsentLabelKey(WebsiteConsentLabelSlot.DECLINE_ALL))));
179
+
180
+ const refusal = await screen.findByText(text(websiteProviderComponentLabelKey(WebsiteProviderComponentLabelSlot.CONSENT_REQUIRED)));
181
+ expect(refusal.closest('[role="status"]')).not.toBeNull();
182
+ expect(screen.getByText(text(websiteConsentPurposeLabelKey(ConsentPurpose.MARKETING)))).toBeInTheDocument();
183
+ expect(load).not.toHaveBeenCalled();
184
+
185
+ // The refusal's own button opens the panel; granting there loads the component without a re-mount.
186
+ const manage = screen.getAllByRole('button', { name: text(websiteConsentLabelKey(WebsiteConsentLabelSlot.MANAGE)) });
187
+ fireEvent.click(manage[0]!);
188
+ fireEvent.click(await screen.findByText(text(websiteConsentLabelKey(WebsiteConsentLabelSlot.ACCEPT_ALL))));
189
+
190
+ expect(await screen.findByLabelText('Address')).toBeInTheDocument();
191
+ expect(load).toHaveBeenCalledTimes(1);
192
+ });
193
+
194
+ it('loads at once for a visitor who agreed on an earlier page', async () => {
195
+ writeWebsiteVisitorConsent([ConsentPurpose.MARKETING]);
196
+ const load = vi.fn(async () => PlacesLookup);
197
+
198
+ render(island(site({ load, consentPurposes: [ConsentPurpose.MARKETING] })));
199
+
200
+ expect(await screen.findByLabelText('Address')).toBeInTheDocument();
201
+ });
202
+
203
+ it('says the feature is unavailable when the component cannot load — never a blank', async () => {
204
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
205
+ render(island(site({ load: async () => { throw new Error('vendor CDN unreachable'); } })));
206
+
207
+ const statement = await screen.findByText(text(websiteProviderComponentLabelKey(WebsiteProviderComponentLabelSlot.UNAVAILABLE)));
208
+ expect(statement.closest('[role="status"]')).not.toBeNull();
209
+ await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.stringContaining('LOAD_FAILED')));
210
+ warn.mockRestore();
211
+ });
212
+
213
+ it('says nothing, rather than "try again later", for a provider that offers no component here', async () => {
214
+ render(island(site({ load: async () => PlacesLookup, withComponent: false })));
215
+ await new Promise((resolve) => setTimeout(resolve, 0));
216
+ expect(screen.queryByText(text(websiteProviderComponentLabelKey(WebsiteProviderComponentLabelSlot.UNAVAILABLE)))).toBeNull();
217
+ expect(document.querySelector(`[data-website-provider-component-state="${ProviderComponentStatus.NONE}"]`)).not.toBeNull();
218
+ });
219
+ });
220
+
221
+ describe('a component-bearing provider\'s scripts load WITH its component, not on every page (#1703)', () => {
222
+ it('injects nothing at startup on a page without the component', async () => {
223
+ render(island(site({ load: async () => PlacesLookup, scripts: [{ src: SCRIPT_SRC }] }), EmptyPage));
224
+ await screen.findByText('No form on this page');
225
+ await new Promise((resolve) => setTimeout(resolve, 0));
226
+ expect(injectedScripts()).toEqual([]);
227
+ });
228
+
229
+ it('loads the script when the page shows the component', async () => {
230
+ render(island(site({ load: async () => PlacesLookup, scripts: [{ src: SCRIPT_SRC }] })));
231
+ await waitFor(() => expect(injectedScripts()).toEqual([SCRIPT_SRC]));
232
+ });
233
+
234
+ it('still injects at startup the scripts of a provider that contributes NO component', async () => {
235
+ render(island(site({ load: async () => PlacesLookup, scripts: [{ src: SCRIPT_SRC }], withComponent: false }), EmptyPage));
236
+ await waitFor(() => expect(injectedScripts()).toEqual([SCRIPT_SRC]));
237
+ });
238
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * #1704 — the website's consent-based providers start on the visitor's grant, stop on withdrawal, and a start never
3
+ * overlaps the previous stop of the same page-global vendor.
4
+ *
5
+ * The runner and the lane are the real ones; only the vendor is a double.
6
+ */
7
+ import React from 'react';
8
+ import { afterEach, describe, expect, it, vi } from 'vitest';
9
+ import { cleanup, render, waitFor } from '@testing-library/react';
10
+ import { BUILTIN_PROVIDER_CAPABILITY, ConsentPurpose } from '@wildo-ai/saas-models/public-runtime';
11
+ import type { FrontendWebsiteProviderRegistry } from '@wildo-ai/external-connectors-public/website';
12
+
13
+ import { useWebsiteProviderSdks } from '../useWebsiteProviderScripts';
14
+
15
+ afterEach(() => { cleanup(); });
16
+
17
+ function Harness({ registry, granted }: { registry: FrontendWebsiteProviderRegistry; granted: ConsentPurpose[] }) {
18
+ useWebsiteProviderSdks(registry, granted);
19
+ return null;
20
+ }
21
+
22
+ function registryOf(activate: () => Promise<unknown>): FrontendWebsiteProviderRegistry {
23
+ const module = {
24
+ metadata: { ref: 'posthog' },
25
+ providerCapabilities: [BUILTIN_PROVIDER_CAPABILITY.FRONTEND_PRODUCT_ANALYTICS],
26
+ protocols: ['FRONTEND_SDK'],
27
+ publicConfig: {},
28
+ sdk: { descriptor: { packageName: '@vendor/posthog' }, stoppable: true, activate },
29
+ };
30
+ return { modules: new Map([['posthog', module]]) } as unknown as FrontendWebsiteProviderRegistry;
31
+ }
32
+
33
+ describe('useWebsiteProviderSdks — the consent-based group (#1704)', () => {
34
+ it('starts nothing before the grant, starts on it and stops on withdrawal', async () => {
35
+ const deactivate = vi.fn();
36
+ const activate = vi.fn(async () => ({ handle: {}, deactivate }));
37
+ const registry = registryOf(activate);
38
+ const view = render(<Harness registry={registry} granted={[]} />);
39
+ await new Promise((resolve) => setTimeout(resolve, 20));
40
+ expect(activate).not.toHaveBeenCalled();
41
+
42
+ view.rerender(<Harness registry={registry} granted={[ConsentPurpose.ANALYTICS]} />);
43
+ await waitFor(() => expect(activate).toHaveBeenCalledTimes(1));
44
+ view.rerender(<Harness registry={registry} granted={[]} />);
45
+ await waitFor(() => expect(deactivate).toHaveBeenCalledTimes(1));
46
+ });
47
+
48
+ it('does not start again until the previous start has fully stopped (grant, withdraw, grant while the vendor loads)', async () => {
49
+ const order: string[] = [];
50
+ let releaseFirst!: () => void;
51
+ const firstLoaded = new Promise<void>((resolve) => { releaseFirst = resolve; });
52
+ let starts = 0;
53
+ const activate = vi.fn(async () => {
54
+ starts += 1;
55
+ const n = starts;
56
+ order.push(`start:${n}`);
57
+ if (n === 1) await firstLoaded;
58
+ return { handle: {}, deactivate: () => { order.push(`stop:${n}`); } };
59
+ });
60
+ const registry = registryOf(activate);
61
+ const view = render(<Harness registry={registry} granted={[ConsentPurpose.ANALYTICS]} />);
62
+ await waitFor(() => expect(order).toEqual(['start:1']));
63
+
64
+ view.rerender(<Harness registry={registry} granted={[]} />);
65
+ view.rerender(<Harness registry={registry} granted={[ConsentPurpose.ANALYTICS]} />);
66
+ await new Promise((resolve) => setTimeout(resolve, 20));
67
+ expect(order).toEqual(['start:1']);
68
+
69
+ releaseFirst();
70
+ await waitFor(() => expect(order).toEqual(['start:1', 'stop:1', 'start:2']));
71
+ });
72
+ });
@@ -40,8 +40,7 @@ export interface CreateFrontendWebsiteProviderRegistryInput {
40
40
  readonly entries?: FrontendProvidersBlock;
41
41
  /**
42
42
  * The CODE channel. Absent is the ordinary case for a website that declares
43
- * no package-shipped provider — `wildo config sync` writes no generated file,
44
- * so there is nothing to import and nothing to pass.
43
+ * no providers. Config sync still writes an empty map so startup imports remain stable.
45
44
  */
46
45
  readonly modules?: GeneratedWebsiteProviderModules;
47
46
  readonly onDiagnostic?: (diagnostic: FrontendProviderHydrationDiagnostic) => void;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The website's provider-component registry (#1703): the SAME registry the application builds, from
3
+ * the SAME `components` declaration, over the website's live provider modules.
4
+ *
5
+ * A provider declares its components once, on its frontend module. Before this, the website facet
6
+ * type accepted `components` and nothing on the website read it, so a contact form could not host an
7
+ * address lookup (#561) and a marketing page could not host any provider component at all. The
8
+ * registry is `createProviderComponentRegistry` from `@wildo-ai/external-connectors-public/website`
9
+ * — keyed `capability:providerRef`, consent-gated, script-before-component and browser-only — and
10
+ * this file only feeds it the website's two inputs:
11
+ *
12
+ * - the HYDRATED website provider registry (both channels joined, so a provider not live for this
13
+ * website service contributes no component);
14
+ * - the loaders the site's facet library contributes for components an engine provider module
15
+ * declares but cannot import (the module lives in a renderer-free package), handed in through the
16
+ * page bridge's `providerComponentLoaders` rather than through module-level state, because the
17
+ * bridge is the website's one construction site and can carry it explicitly.
18
+ *
19
+ * Nothing is loaded here. A component loads when a `WebsiteProviderComponent` slot asks for it, from
20
+ * an effect — never during the Astro server render.
21
+ */
22
+
23
+ import { useMemo, type ComponentType } from 'react';
24
+ import {
25
+ collectProviderComponentRegistrations,
26
+ createProviderComponentRegistry,
27
+ indexProviderComponentLoaderContributions,
28
+ type ProviderComponentDiagnostic,
29
+ type ProviderComponentLoaderContribution,
30
+ type ProviderComponentRegistry,
31
+ } from '@wildo-ai/external-connectors-public/website';
32
+
33
+ import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
34
+
35
+ import {
36
+ createFrontendWebsiteProviderRegistry,
37
+ type FrontendWebsiteProviderRegistry,
38
+ type GeneratedWebsiteProviderModules,
39
+ } from './frontend-provider-registry.website';
40
+
41
+ /** A component type the website's registry hands back once loaded. */
42
+ export type WebsiteProviderComponentType = ComponentType<never>;
43
+
44
+ /** The website's provider-component registry: the shared registry, holding React components. */
45
+ export type WebsiteProviderComponentRegistry = ProviderComponentRegistry<WebsiteProviderComponentType>;
46
+
47
+ export interface CreateWebsiteProviderComponentRegistryInput {
48
+ /** The hydrated website provider registry; absent on a site with no providers. */
49
+ readonly providerRegistry: FrontendWebsiteProviderRegistry | undefined;
50
+ /** Loaders the site's facet library contributes for engine-declared components. */
51
+ readonly providerComponentLoaders?: Iterable<ProviderComponentLoaderContribution>;
52
+ /**
53
+ * Where a registration or load problem is reported. Defaults to `console.warn`: the website runtime
54
+ * has no logger service, and a provider component that cannot be offered is a developer-facing fact
55
+ * that must not be silent.
56
+ */
57
+ readonly onDiagnostic?: (diagnostic: ProviderComponentDiagnostic) => void;
58
+ }
59
+
60
+ function warnProviderComponentDiagnostic(diagnostic: ProviderComponentDiagnostic): void {
61
+ // Reported in the BROWSER only. The registry is built during the server render or prerender too, once per
62
+ // page, and a declaration nothing can load would otherwise fill the build log with one warning per page
63
+ // for a fact the first page already stated. The browser is where somebody can see and act on it.
64
+ if (typeof document === 'undefined') return;
65
+ // eslint-disable-next-line no-console
66
+ console.warn(`[wildo-provider-component] ${diagnostic.kind}: ${diagnostic.message}`);
67
+ }
68
+
69
+ /** Builds the website's provider-component registry from its live provider modules. */
70
+ export function createWebsiteProviderComponentRegistry(
71
+ input: CreateWebsiteProviderComponentRegistryInput,
72
+ ): WebsiteProviderComponentRegistry {
73
+ const onDiagnostic = input.onDiagnostic ?? warnProviderComponentDiagnostic;
74
+ const registrations = collectProviderComponentRegistrations(input.providerRegistry?.modules.values() ?? [], {
75
+ contributedLoaders: indexProviderComponentLoaderContributions(input.providerComponentLoaders),
76
+ onDiagnostic,
77
+ });
78
+ return createProviderComponentRegistry<WebsiteProviderComponentType>({ registrations, onDiagnostic });
79
+ }
80
+
81
+ /** What a page bridge builds once per island: the hydrated provider registry and its component registry. */
82
+ export interface WebsiteProviderRegistries {
83
+ readonly frontendProviderRegistry: FrontendWebsiteProviderRegistry;
84
+ readonly providerComponentRegistry: WebsiteProviderComponentRegistry;
85
+ }
86
+
87
+ /**
88
+ * Builds both registries for a page bridge, MEMOISED on their inputs.
89
+ *
90
+ * Memoised because the component registry caches each loaded component: a registry rebuilt on every
91
+ * render would forget what it loaded, hand every slot a new registry identity, and make each slot's
92
+ * load effect run again — a download loop. The inputs are an island prop (`frontendProviders`, fixed
93
+ * for the island's life) and two module-load-time bridge dependencies, so in practice this builds
94
+ * once per island.
95
+ */
96
+ export function useWebsiteProviderRegistries(input: {
97
+ readonly frontendProviders: FrontendProvidersBlock | undefined;
98
+ readonly frontendProviderModules: GeneratedWebsiteProviderModules | undefined;
99
+ readonly providerComponentLoaders: Iterable<ProviderComponentLoaderContribution> | undefined;
100
+ }): WebsiteProviderRegistries {
101
+ const { frontendProviders, frontendProviderModules, providerComponentLoaders } = input;
102
+ return useMemo(() => {
103
+ const frontendProviderRegistry = createFrontendWebsiteProviderRegistry({
104
+ entries: frontendProviders,
105
+ modules: frontendProviderModules,
106
+ });
107
+ return {
108
+ frontendProviderRegistry,
109
+ providerComponentRegistry: createWebsiteProviderComponentRegistry({
110
+ providerRegistry: frontendProviderRegistry,
111
+ ...(providerComponentLoaders !== undefined ? { providerComponentLoaders } : {}),
112
+ }),
113
+ };
114
+ }, [frontendProviders, frontendProviderModules, providerComponentLoaders]);
115
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Reading provider-contributed components on the website (#1703) — the website's counterpart of the
3
+ * application's `useProviderComponentChoices` / `useProviderComponent`, over the same registry and
4
+ * the same status vocabulary (`ProviderComponentStatus`).
5
+ *
6
+ * Two differences from the application, both about WHO answers:
7
+ *
8
+ * - the consent grant is the VISITOR's decision from `WebsiteConsentContext`, not a signed-in
9
+ * person's stored preference;
10
+ * - nothing starts before that decision has been read from storage (`isResolved`). The server render
11
+ * and the first client render read no storage, and asking the registry then would report a
12
+ * consent-required state to a visitor who already agreed, flashing the refusal on every page.
13
+ *
14
+ * Neither hook picks a provider: when several providers offer a component for one capability,
15
+ * choosing is the page's job.
16
+ */
17
+
18
+ import { useEffect, useState } from 'react';
19
+ import {
20
+ idleProviderComponentLoadState,
21
+ startProviderComponentLoad,
22
+ type ProviderComponentLoadState,
23
+ } from '@wildo-ai/external-connectors-public/website';
24
+ import type { ProviderCapability } from '@wildo-ai/saas-models/public-runtime';
25
+
26
+ import { useWebsiteConsent } from '../consent/useWebsiteConsent';
27
+ import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
28
+ import type { WebsiteProviderComponentType } from './provider-component-registry.website';
29
+
30
+ /** What `useWebsiteProviderComponent` returns: the shared load state, holding a React component. */
31
+ export type WebsiteProviderComponentState = ProviderComponentLoadState<WebsiteProviderComponentType>;
32
+
33
+ /**
34
+ * The providers that are BOTH live for this website service and carry a component for this
35
+ * capability, in the order the service declared them. Synchronous, so a page can render its choice
36
+ * on the first frame.
37
+ */
38
+ export function useWebsiteProviderComponentChoices(capability: ProviderCapability): ReadonlyArray<string> {
39
+ const { providerComponentRegistry } = useWebsiteRuntime();
40
+ return providerComponentRegistry?.listProviders(capability) ?? [];
41
+ }
42
+
43
+ /**
44
+ * Loads one provider's component for a capability, once the visitor's consent decision is known.
45
+ *
46
+ * Pass `undefined` for the ref while nothing is chosen. The load runs in an effect, so it never runs
47
+ * during the Astro server render; `CONSENT_REQUIRED` means nothing of the provider was downloaded,
48
+ * and granting the missing purposes through the consent panel loads it without a re-mount.
49
+ */
50
+ export function useWebsiteProviderComponent(
51
+ capability: ProviderCapability,
52
+ providerRef: string | undefined,
53
+ ): WebsiteProviderComponentState {
54
+ const { providerComponentRegistry } = useWebsiteRuntime();
55
+ const consent = useWebsiteConsent();
56
+ const grantedKey = [...consent.grantedPurposes].sort().join(',');
57
+ const [state, setState] = useState<WebsiteProviderComponentState>(() => idleProviderComponentLoadState<WebsiteProviderComponentType>());
58
+
59
+ useEffect(() => {
60
+ if (!consent.isResolved) {
61
+ setState(idleProviderComponentLoadState<WebsiteProviderComponentType>());
62
+ return undefined;
63
+ }
64
+ return startProviderComponentLoad({
65
+ registry: providerComponentRegistry,
66
+ capability,
67
+ providerRef,
68
+ grantedConsentPurposes: consent.grantedPurposes,
69
+ onState: setState,
70
+ });
71
+ // `grantedPurposes` is read through `grantedKey`: keyed by the granted SET, not the array's identity.
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [capability, providerRef, providerComponentRegistry, consent.isResolved, grantedKey]);
74
+
75
+ return state;
76
+ }