@graph8/sdk 0.370.0 → 0.374.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,230 @@
1
+ "use client";
2
+
3
+ /**
4
+ * A2pRegistration: US carriers filter text messages from numbers whose business is not registered (A2P 10DLC). This shows
5
+ * where the workspace's registration stands, saves the details as a draft, submits them, and re-runs numbers that got stuck.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getPhoneA2pStatus GET /phone-numbers/a2p/status (read; brand and campaign status, missing inputs, carrier failures)
9
+ * 2. saveA2pInputs POST /phone-numbers/a2p/inputs (write; saves a DRAFT, sends nothing; it REPLACES what was saved,
10
+ * so the whole object is sent, merged over what the status returned)
11
+ * 3. startA2pRegistration POST /phone-numbers/a2p/register (external: reaches the carrier registry and carries
12
+ * carrier fees, takes days and can be rejected. Idempotent per workspace. Confirmed
13
+ * with ConfirmSpendDialog.)
14
+ * 4. reconcileA2pRegistration POST /phone-numbers/a2p/reconcile (write; re-attaches pending or failed numbers; safe to repeat)
15
+ *
16
+ * The form is shown only once the status has loaded and is seeded from it, so a save never overwrites saved details with blanks.
17
+ * Brand and campaign objects are open: keys this form does not edit (an address, say) are carried through untouched.
18
+ */
19
+
20
+ import { useEffect, useState } from "react";
21
+ import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
22
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
28
+ import { SectionCard } from "@/g8/components/screen/section-card";
29
+ import { StatusPill } from "@/g8/components/screen/status-pill";
30
+ import { Skeleton } from "@graph8/sdk/ui";
31
+ import { a2pTone, asObject, dataOf } from "../lib/phone-numbers-utils";
32
+
33
+ export interface A2pRegistrationProps {
34
+ /** Called when the status was reloaded, so a stat can follow it. */
35
+ onStatus?: (status: A2pStatus) => void;
36
+ className?: string;
37
+ }
38
+
39
+ export interface A2pStatus {
40
+ brandStatus: string | null;
41
+ campaignStatus: string | null;
42
+ submitted: boolean;
43
+ inputsComplete: boolean;
44
+ missing: string[];
45
+ failures: Array<{ scope?: string | null; code?: number | null; reason?: string | null; fields?: string[] }>;
46
+ brand: Record<string, unknown>;
47
+ campaign: Record<string, unknown>;
48
+ }
49
+
50
+ export function a2pStatusOf(envelope: unknown): A2pStatus {
51
+ const d = dataOf(envelope);
52
+ const text = (v: unknown) => (typeof v === "string" && v ? v : null);
53
+ return {
54
+ brandStatus: text(d.brand_status),
55
+ campaignStatus: text(d.campaign_status),
56
+ submitted: Boolean(d.workflow_id),
57
+ inputsComplete: d.inputs_complete === true,
58
+ missing: Array.isArray(d.missing_inputs) ? d.missing_inputs.filter((m): m is string => typeof m === "string") : [],
59
+ failures: (Array.isArray(d.failures) ? d.failures : []).map((f) => asObject(f)) as A2pStatus["failures"],
60
+ brand: asObject(d.brand_inputs),
61
+ campaign: asObject(d.campaign_inputs),
62
+ };
63
+ }
64
+
65
+ const NAMES: Record<string, string> = {
66
+ legal_business_name: "Legal business name",
67
+ use_case: "Use case",
68
+ message_samples: "Sample messages",
69
+ description: "Campaign description",
70
+ message_flow: "How people opt in",
71
+ privacy_policy_url: "Privacy policy URL",
72
+ terms_and_conditions_url: "Terms of service URL",
73
+ };
74
+
75
+ const BRAND_FIELDS: Array<[string, string, string]> = [
76
+ ["legal_business_name", "Legal business name", "Acme Inc."],
77
+ ["ein", "EIN (tax id)", "12-3456789"],
78
+ ["website", "Website", "https://acme.example"],
79
+ ["business_type", "Business type", "Corporation"],
80
+ ["vertical", "Industry", "Technology"],
81
+ ];
82
+ const CAMPAIGN_LINES: Array<[string, string, string]> = [
83
+ ["use_case", "Use case", "MIXED, MARKETING or ACCOUNT_NOTIFICATION"],
84
+ ["privacy_policy_url", "Privacy policy URL", "https://acme.example/privacy"],
85
+ ["terms_and_conditions_url", "Terms of service URL", "https://acme.example/terms"],
86
+ ];
87
+
88
+ const asText = (v: unknown): string => (typeof v === "string" ? v : "");
89
+ const samplesOf = (v: unknown): string => (Array.isArray(v) ? v.filter((x) => typeof x === "string").join("\n") : asText(v));
90
+
91
+ export function A2pRegistration({ onStatus, className }: A2pRegistrationProps) {
92
+ const q = useG8Query(async (signal) => a2pStatusOf(await g8Get("/phone-numbers/a2p/status", undefined, { signal })), []);
93
+ const status = q.data;
94
+ useEffect(() => {
95
+ if (status) onStatus?.(status);
96
+ }, [status]); // eslint-disable-line react-hooks/exhaustive-deps
97
+ if (q.error && !status) return <ApiError error={q.error} onRetry={() => void q.refetch()} className={className} />;
98
+ if (!status) return <Skeleton className={`h-40 w-full ${className ?? ""}`} />;
99
+ // The form is mounted only once the status has loaded, with its state seeded at the FIRST render from it. (Seeding in an effect
100
+ // let an edit typed right after the form appeared be overwritten by the late seed, and a save then dropped that field.)
101
+ // A reload after a save keeps what is typed; the save merges over the fresh server copy (`status`).
102
+ return <A2pForm status={status} reload={() => q.refetch()} className={className} />;
103
+ }
104
+
105
+ function A2pForm({ status, reload, className }: { status: A2pStatus; reload: () => Promise<unknown>; className?: string }) {
106
+ const [brand, setBrand] = useState<Record<string, string>>(() => Object.fromEntries(BRAND_FIELDS.map(([k]) => [k, asText(status.brand[k])])));
107
+ const [campaign, setCampaign] = useState<Record<string, string>>(() =>
108
+ Object.fromEntries([...CAMPAIGN_LINES.map(([k]) => k), "description", "message_flow", "opt_in_method"].map((k) => [k, asText(status.campaign[k])])),
109
+ );
110
+ const [samples, setSamples] = useState(() => samplesOf(status.campaign.message_samples));
111
+ const [confirming, setConfirming] = useState(false);
112
+ const [note, setNote] = useState<string | null>(null);
113
+
114
+ const body = () => {
115
+ const clean = (o: Record<string, string>) => Object.fromEntries(Object.entries(o).filter(([, v]) => v.trim() !== "").map(([k, v]) => [k, v.trim()]));
116
+ const lines = samples.split("\n").map((l) => l.trim()).filter(Boolean);
117
+ return {
118
+ brand_inputs: { ...status.brand, ...clean(brand) },
119
+ campaign_inputs: { ...status.campaign, ...clean(campaign), ...(lines.length ? { message_samples: lines } : {}) },
120
+ };
121
+ };
122
+
123
+ const save = useG8Mutation(async () => {
124
+ await g8Post("/phone-numbers/a2p/inputs", body());
125
+ setNote("Draft saved. Nothing was sent to the carriers.");
126
+ await reload();
127
+ });
128
+ const submit = useG8Mutation(async () => {
129
+ await g8Post("/phone-numbers/a2p/register", body());
130
+ setNote("Registration submitted. The carriers take days to decide; this page shows the result.");
131
+ setConfirming(false);
132
+ await reload();
133
+ });
134
+ const reconcile = useG8Mutation(async () => {
135
+ await g8Post("/phone-numbers/a2p/reconcile", {});
136
+ setNote("Pending and failed numbers were re-attached to the messaging service.");
137
+ await reload();
138
+ });
139
+
140
+ const failed = status.failures.filter((f) => f.reason);
141
+ const busy = save.loading || submit.loading || reconcile.loading;
142
+ const field = (id: string, label: string, value: string, set: (v: string) => void, placeholder?: string) => (
143
+ <div className="space-y-1" key={id}>
144
+ <Label htmlFor={`a2p-${id}`}>{label}</Label>
145
+ <Input id={`a2p-${id}`} value={value} placeholder={placeholder} onChange={(e) => set(e.target.value)} />
146
+ </div>
147
+ );
148
+
149
+ return (
150
+ <div className={`space-y-4 ${className ?? ""}`}>
151
+ <SectionCard
152
+ title="Text message registration (A2P 10DLC)"
153
+ description="US carriers filter business text messages from numbers that are not registered."
154
+ actions={
155
+ <StatusPill tone={a2pTone(status.campaignStatus ?? status.brandStatus)} dot>
156
+ {status.campaignStatus ?? status.brandStatus ?? "Not started"}
157
+ </StatusPill>
158
+ }
159
+ >
160
+ <KeyValueList
161
+ columns={2}
162
+ items={[
163
+ { label: "Brand", value: status.brandStatus ?? "-" },
164
+ { label: "Campaign", value: status.campaignStatus ?? "-" },
165
+ { label: "Submitted", value: status.submitted ? "Yes" : "No" },
166
+ { label: "Details complete", value: status.inputsComplete ? "Yes" : "No" },
167
+ ]}
168
+ />
169
+ {status.missing.length > 0 ? (
170
+ <p className="mt-3 text-sm text-muted-foreground">Still needed: {status.missing.map((m) => NAMES[m] ?? m).join(", ")}.</p>
171
+ ) : null}
172
+ {failed.length > 0 ? (
173
+ <ul aria-label="Carrier feedback" className="mt-3 space-y-1 text-sm">
174
+ {failed.map((f, i) => (
175
+ <li key={i} className="rounded-md border border-border bg-muted px-3 py-2">
176
+ {f.scope ? <span className="font-medium capitalize">{f.scope}: </span> : null}
177
+ {f.reason}
178
+ {f.code ? <span className="text-xs text-muted-foreground"> (code {f.code})</span> : null}
179
+ </li>
180
+ ))}
181
+ </ul>
182
+ ) : null}
183
+ </SectionCard>
184
+
185
+ <SectionCard title="Business and campaign details" description="Saving a draft sends nothing. Submitting registers with the carriers.">
186
+ <form className="space-y-4" onSubmit={(e) => { e.preventDefault(); void save.mutate(undefined); }}>
187
+ <div className="grid gap-3 sm:grid-cols-2">
188
+ {BRAND_FIELDS.map(([k, label, ph]) => field(`b-${k}`, label, brand[k] ?? "", (v) => setBrand((b) => ({ ...b, [k]: v })), ph))}
189
+ {CAMPAIGN_LINES.map(([k, label, ph]) => field(`c-${k}`, label, campaign[k] ?? "", (v) => setCampaign((c) => ({ ...c, [k]: v })), ph))}
190
+ </div>
191
+ <div className="space-y-1">
192
+ <Label htmlFor="a2p-description">What the messages are (40 characters or more)</Label>
193
+ <Textarea id="a2p-description" rows={2} value={campaign.description ?? ""} onChange={(e) => setCampaign((c) => ({ ...c, description: e.target.value }))} />
194
+ </div>
195
+ <div className="space-y-1">
196
+ <Label htmlFor="a2p-flow">How people opt in (40 characters or more)</Label>
197
+ <Textarea id="a2p-flow" rows={2} value={campaign.message_flow ?? ""} onChange={(e) => setCampaign((c) => ({ ...c, message_flow: e.target.value }))} />
198
+ </div>
199
+ <div className="space-y-1">
200
+ <Label htmlFor="a2p-samples">Sample messages (one per line)</Label>
201
+ <Textarea id="a2p-samples" rows={3} value={samples} onChange={(e) => setSamples(e.target.value)} />
202
+ </div>
203
+ <div className="flex flex-wrap gap-2">
204
+ <Button type="submit" variant="outline" loading={save.loading} disabled={busy}>Save draft</Button>
205
+ <Button type="button" disabled={busy || !status.inputsComplete} onClick={() => setConfirming(true)}>Submit registration</Button>
206
+ <Button type="button" variant="ghost" loading={reconcile.loading} disabled={busy} onClick={() => void reconcile.mutate(undefined)}>Retry stuck numbers</Button>
207
+ </div>
208
+ {!status.inputsComplete ? <p className="text-xs text-muted-foreground">Submitting unlocks once the saved details are complete. Save the draft to check.</p> : null}
209
+ </form>
210
+ {save.error ? <ApiError error={save.error} title="The draft was not saved" onRetry={save.reset} className="mt-3" /> : null}
211
+ {submit.error ? <ApiError error={submit.error} title="The registration was not submitted" onRetry={submit.reset} className="mt-3" /> : null}
212
+ {reconcile.error ? <ApiError error={reconcile.error} title="The numbers were not re-attached" onRetry={reconcile.reset} className="mt-3" /> : null}
213
+ {note ? <p role="status" className="mt-3 text-sm">{note}</p> : null}
214
+ </SectionCard>
215
+
216
+ {confirming ? (
217
+ <ConfirmSpendDialog
218
+ open
219
+ tier="external"
220
+ title="Submit the registration to the carriers"
221
+ description="This sends your business details to the US carrier registry and carries carrier fees. It takes days, the carriers can reject it, and a second submit while one is running does nothing."
222
+ confirmLabel="Submit registration"
223
+ confirming={submit.loading}
224
+ onCancel={() => setConfirming(false)}
225
+ onConfirm={() => void submit.mutate(undefined).then(() => setConfirming(false))}
226
+ />
227
+ ) : null}
228
+ </div>
229
+ );
230
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AssignNumber: who a number belongs to. The assignee is not a label: it decides who the number rings, who is told about
5
+ * a missed call, and whose outbound caller-ID pool it joins.
6
+ *
7
+ * SDK operations:
8
+ * updatePhoneAssignee PATCH /phone-numbers/update-assignee (write; moves the number to a person, in the CRM record and in voice)
9
+ * updateVoiceAssignee PATCH /voice/numbers/assignee (write; the VOICE half alone. `assigned_to: ""` clears the
10
+ * assignment, and sending the current assignee again re-syncs voice)
11
+ *
12
+ * The person comes from MemberPicker (no one types an email); "Assign to me" uses the signed-in member. Reassigning is
13
+ * admin-only through the API: a 403 says so instead of failing silently.
14
+ * Unassign is VOICE-ONLY: update-assignee requires an assignee, so there is no operation that clears the CRM record's owner.
15
+ * The UI says so next to the button.
16
+ */
17
+
18
+ import { useState } from "react";
19
+ import { Button } from "@graph8/sdk/ui";
20
+ import { g8Patch } from "@/g8/lib/g8-client";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { MemberPicker } from "@/g8/pickers/components/member-picker";
24
+ import type { MemberOption } from "@/g8/pickers/components/member-picker";
25
+ import { useCurrentMember } from "@/g8/pickers/hooks/use-current-member";
26
+ import { formatNumber } from "../lib/phone-numbers-utils";
27
+ import type { NumberRow } from "../lib/phone-numbers-utils";
28
+
29
+ export interface AssignNumberProps {
30
+ row: NumberRow;
31
+ /** Called after the assignment changed, so the list can refresh. */
32
+ onChanged?: () => void;
33
+ className?: string;
34
+ }
35
+
36
+ export function AssignNumber({ row, onChanged, className }: AssignNumberProps) {
37
+ const me = useCurrentMember();
38
+ const [picked, setPicked] = useState<MemberOption | null>(null);
39
+ const [note, setNote] = useState<string | null>(null);
40
+
41
+ const assign = useG8Mutation(async (member: MemberOption) => {
42
+ if (!member.email) throw new Error(`${member.name} has no email on file, so the number cannot be assigned to them.`);
43
+ await g8Patch("/phone-numbers/update-assignee", { phone_number: row.number, assignee: member.email });
44
+ setNote(`${formatNumber(row.number)} now rings for ${member.name}.`);
45
+ setPicked(null);
46
+ onChanged?.();
47
+ });
48
+ const voice = useG8Mutation(async (assignedTo: string) => {
49
+ await g8Patch("/voice/numbers/assignee", { number: row.number, assigned_to: assignedTo });
50
+ setNote(assignedTo ? "Voice assignment re-synced." : `${formatNumber(row.number)} is unassigned.`);
51
+ onChanged?.();
52
+ });
53
+
54
+ const error = assign.error ?? voice.error;
55
+ const forbidden = (error as { status?: number } | undefined)?.status === 403;
56
+ const busy = assign.loading || voice.loading;
57
+ const clearError = () => {
58
+ assign.reset();
59
+ voice.reset();
60
+ };
61
+
62
+ return (
63
+ <div className={`space-y-3 ${className ?? ""}`}>
64
+ <p className="text-sm">
65
+ {row.assignedTo ? (
66
+ <>Assigned to <span className="font-medium">{row.assignedTo}</span>.</>
67
+ ) : (
68
+ "Not assigned to anyone."
69
+ )}
70
+ {row.agentName ? <> Held by the voice agent <span className="font-medium">{row.agentName}</span>.</> : null}
71
+ </p>
72
+ <div className="flex flex-wrap items-end gap-2">
73
+ <MemberPicker label="Assign to" value={picked?.id ?? null} selectedName={picked?.name} onChange={setPicked} disabled={busy} />
74
+ <Button disabled={!picked || busy} loading={assign.loading} onClick={() => picked && void assign.mutate(picked)}>
75
+ Assign
76
+ </Button>
77
+ {me.member ? (
78
+ <Button variant="outline" disabled={busy} onClick={() => me.member && void assign.mutate(me.member)}>
79
+ Assign to me
80
+ </Button>
81
+ ) : null}
82
+ </div>
83
+ {row.assignedTo ? (
84
+ <div className="flex flex-wrap gap-2">
85
+ <Button size="sm" variant="outline" disabled={busy} onClick={() => void voice.mutate("")}>
86
+ Unassign (voice)
87
+ </Button>
88
+ <Button size="sm" variant="ghost" disabled={busy} onClick={() => void voice.mutate(row.assignedTo ?? "")}>
89
+ Re-sync voice record
90
+ </Button>
91
+ </div>
92
+ ) : null}
93
+ <p className="text-xs text-muted-foreground">
94
+ Unassign clears the voice assignment only; the API has no way to clear the owner on the CRM record, so assign someone else to change that.
95
+ </p>
96
+ <p className="text-xs text-muted-foreground">
97
+ The assignee decides who the number rings, who hears about a missed call, and whose outbound caller IDs include it.
98
+ Only workspace admins can reassign numbers through the API.
99
+ </p>
100
+ {note ? <p role="status" className="text-sm">{note}</p> : null}
101
+ {error ? <ApiError error={error} title={forbidden ? "Only workspace admins can reassign numbers" : "The assignment did not change"} onRetry={clearError} /> : null}
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,239 @@
1
+ "use client";
2
+
3
+ /**
4
+ * NumberSearch: find a number a carrier has available, then buy it.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. searchAvailablePhoneNumbers POST /phone-numbers/search (read; nothing is held or charged)
8
+ * 2. purchasePhoneNumber POST /phone-numbers/purchase (billable: SPENDS CREDITS AND REAL MONEY, so it opens
9
+ * ConfirmSpendDialog with the price first; sent with an Idempotency-Key)
10
+ *
11
+ * Ids passed between steps: the `phone_number` of a search result (E.164) is the body of step 2. The price is an
12
+ * ESTIMATE (250 credits held on purchase, then about 250 credits each month): the API has no price read. A 402 means
13
+ * not enough credits; the number was not bought and nothing was charged. A 409 means the number is already taken.
14
+ */
15
+
16
+ import { useRef, useState } from "react";
17
+ import { Phone, Search } from "lucide-react";
18
+ import { Button, Input, Label } from "@graph8/sdk/ui";
19
+ import { g8Post } from "@/g8/lib/g8-client";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
23
+ import { EmptyState } from "@/g8/components/empty-state";
24
+ import { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { plural } from "@/g8/lib/plural";
26
+ import { MemberPicker } from "@/g8/pickers/components/member-picker";
27
+ import type { MemberOption } from "@/g8/pickers/components/member-picker";
28
+ import { MONTHLY_RENTAL_CREDITS, PROVIDERS, PURCHASE_CREDITS, asObject, formatNumber, newIdempotencyKey } from "../lib/phone-numbers-utils";
29
+ import type { AvailableNumber, Provider } from "../lib/phone-numbers-utils";
30
+
31
+ export interface NumberSearchProps {
32
+ /** Called after a purchase went through, so the list can refresh. */
33
+ onPurchased?: (phoneNumber: string) => void;
34
+ className?: string;
35
+ }
36
+
37
+ type Use = "dialer" | "ai" | "work";
38
+
39
+ const USE_LABEL: Record<Use, string> = { dialer: "Outbound dialer", ai: "Voice AI agent", work: "Personal work phone" };
40
+ const SELECT = "h-9 w-full rounded-md border border-border bg-background px-2 text-sm";
41
+
42
+ function resultsOf(envelope: unknown): AvailableNumber[] {
43
+ const data = asObject(envelope).data;
44
+ const rows = Array.isArray(data) ? data : asObject(data).items;
45
+ return (Array.isArray(rows) ? rows : []).filter((r): r is AvailableNumber => typeof asObject(r).phone_number === "string");
46
+ }
47
+
48
+ function Field({ id, label, children }: { id: string; label: string; children: React.ReactNode }) {
49
+ return (
50
+ <div className="space-y-1">
51
+ <Label htmlFor={id}>{label}</Label>
52
+ {children}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function NumberSearch({ onPurchased, className }: NumberSearchProps) {
58
+ const [provider, setProvider] = useState<Provider>("twilio");
59
+ const [country, setCountry] = useState("US");
60
+ const [numberType, setNumberType] = useState<"local" | "toll_free">("local");
61
+ const [areaCode, setAreaCode] = useState("");
62
+ const [by, setBy] = useState<"locality" | "pattern">("pattern");
63
+ const [text, setText] = useState("");
64
+ const [match, setMatch] = useState<"first" | "anywhere" | "last">("first");
65
+ // The results remember the carrier they were searched on: a number is bought from THAT carrier, even if the select moved since.
66
+ const [found, setFound] = useState<{ provider: Provider; rows: AvailableNumber[] } | null>(null);
67
+ const results = found?.rows ?? null;
68
+ const boughtFrom = found?.provider ?? provider;
69
+ const [picked, setPicked] = useState<AvailableNumber | null>(null);
70
+ const [use, setUse] = useState<Use>("dialer");
71
+ const [assignee, setAssignee] = useState<MemberOption | null>(null);
72
+ const [confirming, setConfirming] = useState(false);
73
+ const [done, setDone] = useState<string | null>(null);
74
+ const idem = useRef<{ sig: string; key: string } | null>(null);
75
+
76
+ const search = useG8Mutation(async () => {
77
+ const body = {
78
+ provider,
79
+ country: country.trim().toUpperCase() || "US",
80
+ number_type: numberType,
81
+ ...(areaCode.trim() ? { area_code: areaCode.trim() } : {}),
82
+ ...(text.trim() ? (by === "pattern" ? { pattern: text.trim(), match } : { locality: text.trim() }) : {}),
83
+ };
84
+ setPicked(null);
85
+ setDone(null);
86
+ setFound({ provider, rows: resultsOf(await g8Post("/phone-numbers/search", body)) });
87
+ });
88
+
89
+ const purchaseBody = picked
90
+ ? {
91
+ phone_number: picked.phone_number,
92
+ provider: boughtFrom,
93
+ dialer: use !== "ai",
94
+ work: use === "work",
95
+ ...(assignee?.email ? { assignee: assignee.email } : {}),
96
+ }
97
+ : null;
98
+
99
+ const buy = useG8Mutation(async () => {
100
+ if (!purchaseBody) return;
101
+ const sig = JSON.stringify(purchaseBody);
102
+ if (idem.current?.sig !== sig) idem.current = { sig, key: newIdempotencyKey() };
103
+ await g8Post("/phone-numbers/purchase", purchaseBody, { idempotencyKey: idem.current.key });
104
+ setDone(`${formatNumber(purchaseBody.phone_number)} is yours. It can take a minute to appear in your numbers.`);
105
+ setFound((f) => (f ? { ...f, rows: f.rows.filter((x) => x.phone_number !== purchaseBody.phone_number) } : f));
106
+ setPicked(null);
107
+ setConfirming(false);
108
+ onPurchased?.(purchaseBody.phone_number);
109
+ });
110
+
111
+ const status = (buy.error as { status?: number } | undefined)?.status;
112
+
113
+ return (
114
+ <div className={`space-y-5 ${className ?? ""}`}>
115
+ <form
116
+ className="space-y-3"
117
+ onSubmit={(e) => {
118
+ e.preventDefault();
119
+ void search.mutate(undefined);
120
+ }}
121
+ >
122
+ <div className="grid gap-3 sm:grid-cols-2">
123
+ <Field id="ns-provider" label="Carrier">
124
+ <select id="ns-provider" className={SELECT} value={provider} onChange={(e) => setProvider(e.target.value as Provider)}>
125
+ {PROVIDERS.map((p) => (
126
+ <option key={p} value={p}>{p === "twilio" ? "Twilio" : "Telnyx"}</option>
127
+ ))}
128
+ </select>
129
+ </Field>
130
+ <Field id="ns-type" label="Number type">
131
+ <select id="ns-type" className={SELECT} value={numberType} onChange={(e) => setNumberType(e.target.value as "local" | "toll_free")}>
132
+ <option value="local">Local</option>
133
+ <option value="toll_free">Toll free</option>
134
+ </select>
135
+ </Field>
136
+ <Field id="ns-country" label="Country (ISO code)">
137
+ <Input id="ns-country" value={country} maxLength={2} onChange={(e) => setCountry(e.target.value)} />
138
+ </Field>
139
+ <Field id="ns-area" label="Area code">
140
+ <Input id="ns-area" inputMode="numeric" placeholder="415" value={areaCode} onChange={(e) => setAreaCode(e.target.value)} />
141
+ </Field>
142
+ </div>
143
+ <div className="grid gap-3 sm:grid-cols-[10rem_1fr_9rem]">
144
+ <Field id="ns-by" label="Narrow by">
145
+ <select id="ns-by" className={SELECT} value={by} onChange={(e) => setBy(e.target.value as "locality" | "pattern")}>
146
+ <option value="pattern">Digits or phrase</option>
147
+ <option value="locality">City</option>
148
+ </select>
149
+ </Field>
150
+ <Field id="ns-text" label={by === "pattern" ? "Digits or phrase" : "City"}>
151
+ <Input id="ns-text" placeholder={by === "pattern" ? "555 or 0100" : "San Francisco"} value={text} onChange={(e) => setText(e.target.value)} />
152
+ </Field>
153
+ {by === "pattern" ? (
154
+ <Field id="ns-match" label="Where it appears">
155
+ <select id="ns-match" className={SELECT} value={match} onChange={(e) => setMatch(e.target.value as "first" | "anywhere" | "last")}>
156
+ <option value="first">At the start</option>
157
+ <option value="anywhere">Anywhere</option>
158
+ <option value="last">At the end</option>
159
+ </select>
160
+ </Field>
161
+ ) : null}
162
+ </div>
163
+ <Button type="submit" loading={search.loading}>
164
+ <Search className="mr-1 h-4 w-4" aria-hidden /> Search numbers
165
+ </Button>
166
+ <p className="text-xs text-muted-foreground">Searching is free. Nothing is held or charged until you confirm a purchase.</p>
167
+ </form>
168
+
169
+ {search.error ? <ApiError error={search.error} onRetry={search.reset} /> : null}
170
+ {done ? <p role="status" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">{done}</p> : null}
171
+
172
+ {results && results.length === 0 ? (
173
+ <EmptyState compact title="No numbers available" description="Try another area code, a shorter pattern, or the other carrier." />
174
+ ) : null}
175
+ {results && results.length > 0 ? (
176
+ <section aria-label="Available numbers" className="space-y-2">
177
+ <h3 className="text-sm font-medium">{plural(results.length, "number")} available</h3>
178
+ <ul className="divide-y rounded-md border border-border">
179
+ {results.map((r) => (
180
+ <li key={r.phone_number} className={`flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm ${picked?.phone_number === r.phone_number ? "bg-muted" : ""}`}>
181
+ <span className="flex flex-wrap items-center gap-2">
182
+ <Phone className="h-4 w-4 text-muted-foreground" aria-hidden />
183
+ <span className="font-medium tabular-nums">{formatNumber(r.phone_number)}</span>
184
+ <span className="text-xs text-muted-foreground">{[r.locality, r.region].filter(Boolean).join(", ")}</span>
185
+ {r.capabilities?.voice ? <StatusPill tone="neutral">Voice</StatusPill> : null}
186
+ {r.capabilities?.sms ? <StatusPill tone="neutral">SMS</StatusPill> : null}
187
+ </span>
188
+ <Button size="sm" variant="outline" aria-label={`Choose ${r.phone_number}`} onClick={() => { setPicked(r); setDone(null); buy.reset(); }}>
189
+ {picked?.phone_number === r.phone_number ? "Chosen" : "Choose"}
190
+ </Button>
191
+ </li>
192
+ ))}
193
+ </ul>
194
+ </section>
195
+ ) : null}
196
+
197
+ {picked ? (
198
+ <section aria-label="Buy this number" className="space-y-3 rounded-md border border-border p-3">
199
+ <h3 className="text-sm font-medium">Buy {formatNumber(picked.phone_number)}</h3>
200
+ <div className="grid gap-3 sm:grid-cols-2">
201
+ <Field id="ns-use" label="What it is for">
202
+ <select id="ns-use" className={SELECT} value={use} onChange={(e) => setUse(e.target.value as Use)}>
203
+ {(Object.keys(USE_LABEL) as Use[]).map((u) => (
204
+ <option key={u} value={u}>{USE_LABEL[u]}</option>
205
+ ))}
206
+ </select>
207
+ </Field>
208
+ <Field id="ns-assignee" label="Assign to (optional)">
209
+ <MemberPicker id="ns-assignee" label="Assign to" value={assignee?.id ?? null} selectedName={assignee?.name} onChange={setAssignee} />
210
+ </Field>
211
+ </div>
212
+ {use === "work" && !assignee ? <p className="text-xs text-muted-foreground">A personal work phone rings for one person: pick who, or it is yours.</p> : null}
213
+ {buy.error ? (
214
+ <ApiError
215
+ error={buy.error}
216
+ title={status === 402 ? "Not enough credits. Nothing was bought and nothing was charged." : status === 409 ? "That number is no longer available" : "The purchase did not go through"}
217
+ onRetry={buy.reset}
218
+ />
219
+ ) : null}
220
+ <Button onClick={() => setConfirming(true)}>Buy for about {PURCHASE_CREDITS} credits</Button>
221
+ </section>
222
+ ) : null}
223
+
224
+ {picked && confirming ? (
225
+ <ConfirmSpendDialog
226
+ open
227
+ tier="billable"
228
+ title={`Buy ${formatNumber(picked.phone_number)} from ${boughtFrom === "twilio" ? "Twilio" : "Telnyx"}`}
229
+ description={`This spends credits now, whether or not the number is ever used. It is then rented for about ${MONTHLY_RENTAL_CREDITS} credits a month until you release it. These are estimates: the exact rate comes from your plan.`}
230
+ estimatedCredits={PURCHASE_CREDITS}
231
+ confirmLabel="Buy number"
232
+ confirming={buy.loading}
233
+ onCancel={() => setConfirming(false)}
234
+ onConfirm={() => void buy.mutate(undefined).then(() => setConfirming(false))}
235
+ />
236
+ ) : null}
237
+ </div>
238
+ );
239
+ }