@vertesia/ui 1.6.0-dev.20261004.085740Z → 1.6.0-dev.20261009.095813Z

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 (235) hide show
  1. package/README.md +24 -0
  2. package/lib/boot/vite.d.ts +3 -0
  3. package/lib/boot/vite.d.ts.map +1 -1
  4. package/lib/boot/vite.js +12 -0
  5. package/lib/boot/vite.js.map +1 -1
  6. package/lib/core/components/shadcn/button.d.ts +1 -0
  7. package/lib/core/components/shadcn/button.d.ts.map +1 -1
  8. package/lib/core/components/shadcn/button.js +2 -2
  9. package/lib/core/components/shadcn/button.js.map +1 -1
  10. package/lib/core/components/shadcn/selectBox.d.ts.map +1 -1
  11. package/lib/core/components/shadcn/selectBox.js +2 -2
  12. package/lib/core/components/shadcn/selectBox.js.map +1 -1
  13. package/lib/core/components/shadcn/tabs.d.ts +2 -1
  14. package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
  15. package/lib/core/components/shadcn/tabs.js +6 -4
  16. package/lib/core/components/shadcn/tabs.js.map +1 -1
  17. package/lib/env/index.d.ts +9 -1
  18. package/lib/env/index.d.ts.map +1 -1
  19. package/lib/env/index.js +1 -1
  20. package/lib/env/index.js.map +1 -1
  21. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts +1 -1
  22. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
  23. package/lib/features/agent/chat/AgentRequestInputOverlay.js +16 -10
  24. package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
  25. package/lib/features/agent/chat/AskUserWidget.d.ts +7 -1
  26. package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
  27. package/lib/features/agent/chat/AskUserWidget.js +17 -16
  28. package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
  29. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  30. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +2 -253
  31. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  32. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts +1 -3
  33. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts.map +1 -1
  34. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js +2 -8
  35. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js.map +1 -1
  36. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts +4 -0
  37. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts.map +1 -0
  38. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js +248 -0
  39. package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js.map +1 -0
  40. package/lib/features/media-viewer/ImagePanel.d.ts +1 -1
  41. package/lib/features/media-viewer/ImagePanel.d.ts.map +1 -1
  42. package/lib/features/media-viewer/ImagePanel.js +40 -10
  43. package/lib/features/media-viewer/ImagePanel.js.map +1 -1
  44. package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
  45. package/lib/features/store/types/IntakePolicyEditor.js +1 -1
  46. package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
  47. package/lib/i18n/locales/ar.json +15 -0
  48. package/lib/i18n/locales/de.json +15 -0
  49. package/lib/i18n/locales/en.json +15 -0
  50. package/lib/i18n/locales/es.json +15 -0
  51. package/lib/i18n/locales/fr.json +15 -0
  52. package/lib/i18n/locales/it.json +15 -0
  53. package/lib/i18n/locales/ja.json +15 -0
  54. package/lib/i18n/locales/ko.json +15 -0
  55. package/lib/i18n/locales/pt.json +15 -0
  56. package/lib/i18n/locales/ru.json +15 -0
  57. package/lib/i18n/locales/tr.json +15 -0
  58. package/lib/i18n/locales/zh-TW.json +15 -0
  59. package/lib/i18n/locales/zh.json +15 -0
  60. package/lib/session/auth/composable.d.ts +1 -5
  61. package/lib/session/auth/composable.d.ts.map +1 -1
  62. package/lib/session/auth/composable.js +1 -10
  63. package/lib/session/auth/composable.js.map +1 -1
  64. package/lib/session/auth/firebase.d.ts.map +1 -1
  65. package/lib/session/auth/firebase.js +8 -5
  66. package/lib/session/auth/firebase.js.map +1 -1
  67. package/lib/session/auth/oauth.d.ts.map +1 -1
  68. package/lib/session/auth/oauth.js +43 -17
  69. package/lib/session/auth/oauth.js.map +1 -1
  70. package/lib/session/auth/vertesiaHosts.d.ts +0 -40
  71. package/lib/session/auth/vertesiaHosts.d.ts.map +1 -1
  72. package/lib/session/auth/vertesiaHosts.js +2 -45
  73. package/lib/session/auth/vertesiaHosts.js.map +1 -1
  74. package/lib/session/index.d.ts +1 -1
  75. package/lib/session/index.d.ts.map +1 -1
  76. package/lib/session/index.js +1 -1
  77. package/lib/session/index.js.map +1 -1
  78. package/lib/session/scopeSelection.d.ts +4 -0
  79. package/lib/session/scopeSelection.d.ts.map +1 -1
  80. package/lib/session/scopeSelection.js +11 -0
  81. package/lib/session/scopeSelection.js.map +1 -1
  82. package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
  83. package/lib/shell/AuthScreenPreview.js +1 -0
  84. package/lib/shell/AuthScreenPreview.js.map +1 -1
  85. package/lib/shell/login/SignInFlow.d.ts +3 -1
  86. package/lib/shell/login/SignInFlow.d.ts.map +1 -1
  87. package/lib/shell/login/SignInFlow.js +13 -4
  88. package/lib/shell/login/SignInFlow.js.map +1 -1
  89. package/lib/shell/login/SignInIcons.d.ts.map +1 -1
  90. package/lib/shell/login/SignInIcons.js +2 -1
  91. package/lib/shell/login/SignInIcons.js.map +1 -1
  92. package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
  93. package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
  94. package/lib/shell/login/SignInPasswordStep.js +58 -0
  95. package/lib/shell/login/SignInPasswordStep.js.map +1 -0
  96. package/lib/shell/login/SignInPrimitives.d.ts +13 -0
  97. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  98. package/lib/shell/login/SignInPrimitives.js +11 -1
  99. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  100. package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
  101. package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
  102. package/lib/shell/login/SignInProvidersStep.js +4 -2
  103. package/lib/shell/login/SignInProvidersStep.js.map +1 -1
  104. package/lib/shell/login/SignInReturningStep.d.ts +3 -1
  105. package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
  106. package/lib/shell/login/SignInReturningStep.js +18 -8
  107. package/lib/shell/login/SignInReturningStep.js.map +1 -1
  108. package/lib/shell/login/SignInTenantStep.d.ts +3 -1
  109. package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
  110. package/lib/shell/login/SignInTenantStep.js +2 -2
  111. package/lib/shell/login/SignInTenantStep.js.map +1 -1
  112. package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
  113. package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
  114. package/lib/shell/login/SignInWithProviderButton.js +8 -7
  115. package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
  116. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  117. package/lib/shell/login/SigninScreen.js +9 -3
  118. package/lib/shell/login/SigninScreen.js.map +1 -1
  119. package/lib/shell/login/signInUtils.d.ts +24 -4
  120. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  121. package/lib/shell/login/signInUtils.js +53 -1
  122. package/lib/shell/login/signInUtils.js.map +1 -1
  123. package/lib/vertesia-ui-boot.js.map +1 -1
  124. package/lib/vertesia-ui-core.js +42 -42
  125. package/lib/vertesia-ui-core.js.map +1 -1
  126. package/lib/vertesia-ui-env.js +1 -1
  127. package/lib/vertesia-ui-env.js.map +1 -1
  128. package/lib/vertesia-ui-features.js +574 -589
  129. package/lib/vertesia-ui-features.js.map +1 -1
  130. package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js +5 -0
  131. package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js.map +1 -0
  132. package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js +5 -0
  133. package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js.map +1 -0
  134. package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js +5 -0
  135. package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js.map +1 -0
  136. package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js +5 -0
  137. package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js.map +1 -0
  138. package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js +5 -0
  139. package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js.map +1 -0
  140. package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js +5 -0
  141. package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js.map +1 -0
  142. package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js +5 -0
  143. package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js.map +1 -0
  144. package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js +5 -0
  145. package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js.map +1 -0
  146. package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js +5 -0
  147. package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js.map +1 -0
  148. package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js +5 -0
  149. package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js.map +1 -0
  150. package/lib/vertesia-ui-i18n.js +2 -2
  151. package/lib/vertesia-ui-i18n.js.map +1 -1
  152. package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js +5 -0
  153. package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js.map +1 -0
  154. package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js +5 -0
  155. package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js.map +1 -0
  156. package/lib/vertesia-ui-layout.js +15 -15
  157. package/lib/vertesia-ui-layout.js.map +1 -1
  158. package/lib/vertesia-ui-rich-text.js +40 -40
  159. package/lib/vertesia-ui-rich-text.js.map +1 -1
  160. package/lib/vertesia-ui-router.js.map +1 -1
  161. package/lib/vertesia-ui-session.js +1 -1
  162. package/lib/vertesia-ui-session.js.map +1 -1
  163. package/lib/vertesia-ui-shell.js +54 -47
  164. package/lib/vertesia-ui-shell.js.map +1 -1
  165. package/lib/vertesia-ui-widgets.js +47 -47
  166. package/lib/vertesia-ui-widgets.js.map +1 -1
  167. package/package.json +20 -20
  168. package/src/boot/vite.ts +15 -0
  169. package/src/core/components/shadcn/button.tsx +3 -2
  170. package/src/core/components/shadcn/selectBox.tsx +12 -4
  171. package/src/core/components/shadcn/tabs.tsx +9 -3
  172. package/src/css/index.css +1 -1
  173. package/src/css/typography.css +111 -88
  174. package/src/env/index.ts +10 -3
  175. package/src/features/agent/chat/AgentRequestInputOverlay.tsx +73 -34
  176. package/src/features/agent/chat/AskUserWidget.tsx +225 -206
  177. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +2 -253
  178. package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.tsx +2 -8
  179. package/src/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.ts +248 -0
  180. package/src/features/media-viewer/ImagePanel.tsx +51 -10
  181. package/src/features/store/types/IntakePolicyEditor.tsx +2 -1
  182. package/src/i18n/locales/ar.json +15 -0
  183. package/src/i18n/locales/de.json +15 -0
  184. package/src/i18n/locales/en.json +15 -0
  185. package/src/i18n/locales/es.json +15 -0
  186. package/src/i18n/locales/fr.json +15 -0
  187. package/src/i18n/locales/it.json +15 -0
  188. package/src/i18n/locales/ja.json +15 -0
  189. package/src/i18n/locales/ko.json +15 -0
  190. package/src/i18n/locales/pt.json +15 -0
  191. package/src/i18n/locales/ru.json +15 -0
  192. package/src/i18n/locales/tr.json +15 -0
  193. package/src/i18n/locales/zh-TW.json +15 -0
  194. package/src/i18n/locales/zh.json +15 -0
  195. package/src/session/auth/composable.ts +1 -11
  196. package/src/session/auth/firebase.ts +9 -6
  197. package/src/session/auth/oauth.ts +47 -16
  198. package/src/session/auth/vertesiaHosts.ts +2 -47
  199. package/src/session/index.ts +1 -1
  200. package/src/session/scopeSelection.ts +13 -0
  201. package/src/shell/AuthScreenPreview.tsx +1 -0
  202. package/src/shell/login/SignInFlow.tsx +23 -2
  203. package/src/shell/login/SignInIcons.tsx +2 -1
  204. package/src/shell/login/SignInPasswordStep.tsx +103 -0
  205. package/src/shell/login/SignInPrimitives.tsx +53 -1
  206. package/src/shell/login/SignInProvidersStep.tsx +8 -4
  207. package/src/shell/login/SignInReturningStep.tsx +20 -8
  208. package/src/shell/login/SignInTenantStep.tsx +6 -2
  209. package/src/shell/login/SignInWithProviderButton.tsx +11 -7
  210. package/src/shell/login/SigninScreen.tsx +8 -3
  211. package/src/shell/login/signInUtils.ts +68 -8
  212. package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js +0 -5
  213. package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js.map +0 -1
  214. package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js +0 -5
  215. package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js.map +0 -1
  216. package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js +0 -5
  217. package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js.map +0 -1
  218. package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js +0 -5
  219. package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js.map +0 -1
  220. package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js +0 -5
  221. package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js.map +0 -1
  222. package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js +0 -5
  223. package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js.map +0 -1
  224. package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js +0 -5
  225. package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js.map +0 -1
  226. package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js +0 -5
  227. package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js.map +0 -1
  228. package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js +0 -5
  229. package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js.map +0 -1
  230. package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js +0 -5
  231. package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js.map +0 -1
  232. package/lib/vertesia-ui-i18n.gais--x5.chunk.js +0 -5
  233. package/lib/vertesia-ui-i18n.gais--x5.chunk.js.map +0 -1
  234. package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js +0 -5
  235. package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js.map +0 -1
@@ -21,6 +21,8 @@ interface SignInReturningStepProps {
21
21
  session: LastSuccessfulLogin;
22
22
  onNotYou: () => void;
23
23
  onProviderClicked: (provider: ProviderId) => void;
24
+ /** The address signs in with a password, so the parent shows the password step instead. */
25
+ onPasswordRequired: (email: string) => void;
24
26
  redirectTo?: string;
25
27
  }
26
28
 
@@ -28,6 +30,7 @@ export default function SignInReturningStep({
28
30
  session,
29
31
  onNotYou,
30
32
  onProviderClicked,
33
+ onPasswordRequired,
31
34
  redirectTo,
32
35
  }: SignInReturningStepProps) {
33
36
  const { t } = useUITranslation();
@@ -36,14 +39,23 @@ export default function SignInReturningStep({
36
39
  const avatar = <SignInInitialsBadge initials={emailInitial(session.email)} />;
37
40
  // A stored tenantName means we resolved the user's organization.
38
41
  const hasTenant = !!session.tenantName;
42
+ const lastProvider = session.lastProvider;
39
43
  const primaryLabel =
40
- session.lastProvider === 'oidc'
41
- ? t('auth.continueWithSignIn')
42
- : t('auth.continueWithProvider', { provider: providerLabel(session.lastProvider) });
44
+ lastProvider === 'password'
45
+ ? t('auth.password.continue')
46
+ : lastProvider === 'oidc'
47
+ ? t('auth.continueWithSignIn')
48
+ : t('auth.continueWithProvider', { provider: providerLabel(lastProvider) });
43
49
 
44
- const continueWith = async (provider: ProviderId) => {
45
- onProviderClicked(provider);
46
- await startSignIn(provider, session.email, redirectTo);
50
+ const continueWithLastProvider = async () => {
51
+ if (lastProvider === 'password') {
52
+ onPasswordRequired(session.email);
53
+ return;
54
+ }
55
+ onProviderClicked(lastProvider);
56
+ // The stored provider can be stale: the address may now resolve to a password tenant.
57
+ const result = await startSignIn(lastProvider, session.email, redirectTo);
58
+ if (!result.ok && result.reason === 'password-required') onPasswordRequired(session.email);
47
59
  };
48
60
 
49
61
  return (
@@ -76,9 +88,9 @@ export default function SignInReturningStep({
76
88
 
77
89
  <div className="flex flex-col gap-2">
78
90
  <SignInProviderButton
79
- provider={session.lastProvider}
91
+ provider={lastProvider}
80
92
  label={primaryLabel}
81
- onClick={() => continueWith(session.lastProvider)}
93
+ onClick={continueWithLastProvider}
82
94
  variant="filled"
83
95
  />
84
96
  <SignInStepButton variant="ghost" onClick={onNotYou}>
@@ -8,13 +8,15 @@ import {
8
8
  SignInStepLayout,
9
9
  } from './SignInPrimitives';
10
10
  import SignInWithProviderButton from './SignInWithProviderButton';
11
- import { type ProviderId, providerLabel } from './signInUtils';
11
+ import { providerLabel, type RedirectProviderId } from './signInUtils';
12
12
 
13
13
  interface SignInTenantStepProps {
14
14
  email: string;
15
15
  tenant: TenantInfo;
16
16
  onBack: () => void;
17
17
  onProviderClicked: () => void;
18
+ /** The tenant re-resolved as a password tenant, so the parent shows the password step. */
19
+ onPasswordRequired: (email: string) => void;
18
20
  redirectTo?: string;
19
21
  }
20
22
 
@@ -32,12 +34,13 @@ export default function SignInTenantStep({
32
34
  tenant,
33
35
  onBack,
34
36
  onProviderClicked,
37
+ onPasswordRequired,
35
38
  redirectTo,
36
39
  }: SignInTenantStepProps) {
37
40
  const { t } = useUITranslation();
38
41
  const tenantName = tenant.label || tenant.name || t('auth.blocked.tenantFallback');
39
42
  // Brands keep their identity; anything else routes through OIDC.
40
- const provider: ProviderId =
43
+ const provider: RedirectProviderId =
41
44
  tenant.provider === 'google' || tenant.provider === 'github' || tenant.provider === 'microsoft'
42
45
  ? tenant.provider
43
46
  : 'oidc';
@@ -68,6 +71,7 @@ export default function SignInTenantStep({
68
71
  redirectTo={redirectTo}
69
72
  variant="filled"
70
73
  onClick={onProviderClicked}
74
+ onPasswordRequired={() => onPasswordRequired(email)}
71
75
  />
72
76
  <SignInStepButton variant="ghost" onClick={onBack}>
73
77
  {t('auth.tenant.notPartOf', { name: tenantName })}
@@ -1,15 +1,17 @@
1
1
  import { useUITranslation } from '@vertesia/ui/i18n';
2
2
  import { SignInProviderButton } from './SignInPrimitives';
3
- import { type ProviderId, providerLabel, startSignIn, startSignInWithoutTenant } from './signInUtils';
3
+ import { providerLabel, type RedirectProviderId, startSignIn, startSignInWithoutTenant } from './signInUtils';
4
4
 
5
5
  interface SignInWithProviderButtonProps {
6
- provider: ProviderId;
6
+ provider: RedirectProviderId;
7
7
  /** Email for the tenant-aware flow. Omit for a no-tenant sign-in (e.g. SignInModal). */
8
8
  email?: string;
9
9
  redirectTo?: string;
10
10
  variant?: 'outline' | 'filled';
11
11
  /** Fired on click, before the redirect. */
12
12
  onClick?: () => void;
13
+ /** Fired instead of a redirect when the email resolves to a password tenant. */
14
+ onPasswordRequired?: () => void;
13
15
  }
14
16
 
15
17
  // "Continue with <provider>" button that owns its sign-in redirect.
@@ -19,6 +21,7 @@ export default function SignInWithProviderButton({
19
21
  redirectTo,
20
22
  variant = 'outline',
21
23
  onClick,
24
+ onPasswordRequired,
22
25
  }: SignInWithProviderButtonProps) {
23
26
  const { t } = useUITranslation();
24
27
  const label =
@@ -26,15 +29,16 @@ export default function SignInWithProviderButton({
26
29
  ? t('auth.continueWithSignIn')
27
30
  : t('auth.continueWithProvider', { provider: providerLabel(provider) });
28
31
 
29
- const signIn = () => {
32
+ const signIn = async () => {
30
33
  onClick?.();
31
- if (email) {
32
- void startSignIn(provider, email, redirectTo);
33
- } else {
34
+ if (!email) {
34
35
  // OIDC needs a resolved tenant; the no-email path is only hit by no-tenant buttons.
35
36
  startSignInWithoutTenant(provider, redirectTo);
37
+ return;
36
38
  }
39
+ const result = await startSignIn(provider, email, redirectTo);
40
+ if (!result.ok && result.reason === 'password-required') onPasswordRequired?.();
37
41
  };
38
42
 
39
- return <SignInProviderButton provider={provider} label={label} onClick={signIn} variant={variant} />;
43
+ return <SignInProviderButton provider={provider} label={label} onClick={() => void signIn()} variant={variant} />;
40
44
  }
@@ -18,7 +18,7 @@ import SignInRecoveryStep, { type SignInRecoveryKind } from './SignInRecoverySte
18
18
  import SignInRestrictedEnvStep from './SignInRestrictedEnvStep';
19
19
  import SignInTenantBlockedStep from './SignInTenantBlockedStep';
20
20
  import SignupForm from './SignupForm';
21
- import { isInviteRequiredError, readPendingSignin, resetSignInState } from './signInUtils';
21
+ import { isInviteRequiredError, isTenantSignIn, readPendingSignin, resetSignInState } from './signInUtils';
22
22
 
23
23
  /** Presentation only: the shared screen still owns session gating and recovery transitions. */
24
24
  export interface SignInScreenViewProps {
@@ -152,7 +152,12 @@ function SigninScreenImpl({
152
152
  useEffect(() => {
153
153
  if (!authError) return;
154
154
  if (authError instanceof UserNotFoundError) {
155
- setMode('signup');
155
+ if (isTenantSignIn()) {
156
+ setEmail(authError.email);
157
+ setMode('blocked');
158
+ } else {
159
+ setMode('signup');
160
+ }
156
161
  } else if (authError instanceof RestrictedEnvironmentError) {
157
162
  setMode('restricted');
158
163
  } else if (authError instanceof RequestedScopeUnavailableError) {
@@ -280,7 +285,7 @@ function SigninScreenImpl({
280
285
  onUseDifferentAccount={useDifferentAccount}
281
286
  />
282
287
  );
283
- } else if (mode === 'signup' && !localStorage.getItem('tenantName')) {
288
+ } else if (mode === 'signup' && !isTenantSignIn()) {
284
289
  content = <SignupForm onSignup={onSignup} goBack={startOver} />;
285
290
  } else {
286
291
  // Every remaining mode is a core one; an unmatched recovery mode falls through to the
@@ -1,17 +1,22 @@
1
1
  import { Env } from '@vertesia/ui/env';
2
2
  import { getFirebaseAuth, setFirebaseTenant } from '@vertesia/ui/session';
3
+ import { FirebaseError } from 'firebase/app';
3
4
  import {
4
5
  type AuthProvider,
5
6
  signOut as firebaseSignOut,
6
7
  GithubAuthProvider,
7
8
  GoogleAuthProvider,
8
9
  OAuthProvider,
10
+ signInWithEmailAndPassword,
9
11
  signInWithRedirect,
10
12
  } from 'firebase/auth';
11
13
 
12
14
  // Matches auth-tenants.json `provider` values. Tenant context is derived from
13
15
  // email resolution at sign-in time, not encoded here.
14
- export type ProviderId = 'google' | 'github' | 'microsoft' | 'oidc';
16
+ export type ProviderId = 'google' | 'github' | 'microsoft' | 'oidc' | 'password';
17
+
18
+ /** Providers that sign in by redirecting to an identity provider. `password` signs in in place. */
19
+ export type RedirectProviderId = Exclude<ProviderId, 'password'>;
15
20
 
16
21
  export interface LastSuccessfulLogin {
17
22
  email: string;
@@ -82,6 +87,13 @@ export function clearPendingSignin(): void {
82
87
  }
83
88
  }
84
89
 
90
+ /**
91
+ * Detects a tenant sign-in by the key's presence, including tenants with an empty label or name.
92
+ */
93
+ export function isTenantSignIn(): boolean {
94
+ return localStorage.getItem('tenantName') !== null;
95
+ }
96
+
85
97
  /**
86
98
  * Promotes the pending sign-in record to the last-successful-login entry, stamping the
87
99
  * authenticated user's display name.
@@ -166,28 +178,28 @@ const buildOidcProvider: ProviderBuilder = (email) => {
166
178
  return p;
167
179
  };
168
180
 
169
- const PROVIDER_BUILDERS: Record<ProviderId, ProviderBuilder> = {
181
+ const PROVIDER_BUILDERS: Record<RedirectProviderId, ProviderBuilder> = {
170
182
  google: buildGoogleProvider,
171
183
  github: buildGithubProvider,
172
184
  microsoft: buildMicrosoftProvider,
173
185
  oidc: buildOidcProvider,
174
186
  };
175
187
 
176
- function buildFirebaseProvider(idp: ProviderId, email?: string, redirectTo?: string): AuthProvider {
188
+ function buildFirebaseProvider(idp: RedirectProviderId, email?: string, redirectTo?: string): AuthProvider {
177
189
  return PROVIDER_BUILDERS[idp](email, redirectTo);
178
190
  }
179
191
 
180
192
  export async function startSignIn(
181
- provider: ProviderId,
193
+ provider: RedirectProviderId,
182
194
  email: string,
183
195
  redirectTo?: string,
184
- ): Promise<{ ok: true } | { ok: false; reason: 'no-email' }> {
196
+ ): Promise<{ ok: true } | { ok: false; reason: 'no-email' | 'password-required' }> {
185
197
  if (!email) return { ok: false, reason: 'no-email' };
186
198
 
187
199
  // A tenant-mapped email uses the tenant's provider (overriding `provider`); otherwise use `provider`.
188
200
  const tenant = await setFirebaseTenant(email);
189
201
  const auth = getFirebaseAuth();
190
- let effectiveIdp = provider;
202
+ let effectiveIdp: ProviderId = provider;
191
203
  let tenantName: string | undefined;
192
204
 
193
205
  if (tenant) {
@@ -201,20 +213,68 @@ export async function startSignIn(
201
213
  auth.tenantId = Env.firebase?.tenantId ?? null;
202
214
  }
203
215
 
216
+ // A password tenant has no identity provider to redirect to; its step collects the password.
217
+ if (effectiveIdp === 'password') return { ok: false, reason: 'password-required' };
218
+
204
219
  writePendingSignin({ email, provider: effectiveIdp, tenantName });
205
220
  void signInWithRedirect(auth, buildFirebaseProvider(effectiveIdp, email, redirectTo));
206
221
  return { ok: true };
207
222
  }
208
223
 
224
+ export type PasswordSignInFailure = 'not-password-tenant' | 'invalid-credentials' | 'too-many-attempts' | 'failed';
225
+
226
+ // Firebase reports an unknown user, a wrong password and a disabled user with distinct codes on
227
+ // some SDK versions; they are folded into one answer so the form does not reveal which it was.
228
+ const INVALID_CREDENTIAL_CODES = new Set([
229
+ 'auth/invalid-credential',
230
+ 'auth/invalid-email',
231
+ 'auth/user-disabled',
232
+ 'auth/user-not-found',
233
+ 'auth/wrong-password',
234
+ ]);
235
+
236
+ export function passwordFailureReason(err: unknown): Exclude<PasswordSignInFailure, 'not-password-tenant'> {
237
+ if (!(err instanceof FirebaseError)) return 'failed';
238
+ if (INVALID_CREDENTIAL_CODES.has(err.code)) return 'invalid-credentials';
239
+ if (err.code === 'auth/too-many-requests') return 'too-many-attempts';
240
+ return 'failed';
241
+ }
242
+
243
+ /**
244
+ * Signs in a user of a password tenant, in place — there is no redirect. Only an address whose
245
+ * domain resolves to a tenant configured for password sign-in may use it; the tenant is resolved
246
+ * again here rather than trusted from the caller, so a returning session cannot carry a stale one.
247
+ * On success the session's auth-state listener completes the login.
248
+ */
249
+ export async function signInWithPassword(
250
+ email: string,
251
+ password: string,
252
+ ): Promise<{ ok: true } | { ok: false; reason: PasswordSignInFailure }> {
253
+ const tenant = await setFirebaseTenant(email);
254
+ if (!tenant) return { ok: false, reason: 'failed' };
255
+ if (tenant.provider !== 'password') return { ok: false, reason: 'not-password-tenant' };
256
+
257
+ const tenantName = tenant.label || tenant.name || undefined;
258
+ localStorage.setItem('tenantName', tenantName ?? '');
259
+ writePendingSignin({ email, provider: 'password', tenantName });
260
+ try {
261
+ await signInWithEmailAndPassword(getFirebaseAuth(), email, password);
262
+ return { ok: true };
263
+ } catch (err: unknown) {
264
+ clearPendingSignin();
265
+ return { ok: false, reason: passwordFailureReason(err) };
266
+ }
267
+ }
268
+
209
269
  /** Starts sign-in without email discovery, preserving an explicitly configured tenant. */
210
- export function startSignInWithoutTenant(provider: ProviderId, redirectTo?: string): void {
270
+ export function startSignInWithoutTenant(provider: RedirectProviderId, redirectTo?: string): void {
211
271
  const auth = getFirebaseAuth();
212
272
  localStorage.removeItem('tenantName');
213
273
  auth.tenantId = Env.firebase?.tenantId ?? null;
214
274
  void signInWithRedirect(
215
275
  auth,
216
276
  buildFirebaseProvider(
217
- Env.firebase?.tenantId ? ((Env.firebase.providerType as ProviderId) ?? 'oidc') : provider,
277
+ Env.firebase?.tenantId ? ((Env.firebase.providerType as RedirectProviderId) ?? 'oidc') : provider,
218
278
  undefined,
219
279
  redirectTo,
220
280
  ),