@vertesia/ui 1.5.0-dev.20260910.052912Z → 1.5.1
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.
- package/lib/core/components/Link.js +1 -1
- package/lib/core/components/Link.js.map +1 -1
- package/lib/core/components/SelectList.d.ts.map +1 -1
- package/lib/core/components/SelectList.js +3 -3
- package/lib/core/components/SelectList.js.map +1 -1
- package/lib/core/components/SidePanel.d.ts.map +1 -1
- package/lib/core/components/SidePanel.js +2 -1
- package/lib/core/components/SidePanel.js.map +1 -1
- package/lib/core/components/Spinner.js +1 -1
- package/lib/core/components/Spinner.js.map +1 -1
- package/lib/core/components/TagsInput.js +7 -7
- package/lib/core/components/TagsInput.js.map +1 -1
- package/lib/core/components/shadcn/badge.js +1 -1
- package/lib/core/components/shadcn/badge.js.map +1 -1
- package/lib/core/components/shadcn/button.d.ts.map +1 -1
- package/lib/core/components/shadcn/button.js +2 -2
- package/lib/core/components/shadcn/button.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js +2 -2
- package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js +1 -1
- package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js.map +1 -1
- package/lib/core/components/shadcn/filters/filter/dateFilter.js +1 -1
- package/lib/core/components/shadcn/filters/filter/dateFilter.js.map +1 -1
- package/lib/core/components/shadcn/filters/filters.js +1 -1
- package/lib/core/components/shadcn/filters/filters.js.map +1 -1
- package/lib/core/components/shadcn/overflowTabs.js +2 -2
- package/lib/core/components/shadcn/overflowTabs.js.map +1 -1
- package/lib/core/components/shadcn/tabs.js +1 -1
- package/lib/core/components/shadcn/tabs.js.map +1 -1
- package/lib/features/activity-doc/ActivityDoc.js +1 -1
- package/lib/features/activity-doc/ActivityDoc.js.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.d.ts +1 -0
- package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.js +15 -4
- package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +51 -17
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +5 -3
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +3 -2
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
- package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
- package/lib/features/layout/GenericPageNavHeader.js +1 -1
- package/lib/features/layout/GenericPageNavHeader.js.map +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.js +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
- package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
- package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
- package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.js +9 -7
- package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js +7 -3
- package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
- package/lib/features/oauth/useMcpConnections.d.ts +4 -2
- package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
- package/lib/features/oauth/useMcpConnections.js +15 -9
- package/lib/features/oauth/useMcpConnections.js.map +1 -1
- package/lib/features/permissions/UserPermissionsProvider.js +1 -1
- package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
- package/lib/features/store/objects/DocumentPreviewPanel.js +5 -5
- package/lib/features/store/objects/DocumentPreviewPanel.js.map +1 -1
- package/lib/features/store/objects/components/DocumentInput.js +1 -1
- package/lib/features/store/objects/components/DocumentInput.js.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
- package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
- package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +6 -3
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts +8 -0
- package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts.map +1 -0
- package/lib/features/store/objects/selection/actions/RunIntakeAction.js +65 -0
- package/lib/features/store/objects/selection/actions/RunIntakeAction.js.map +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
- package/lib/features/store/objects/selection/actions/index.js +1 -0
- package/lib/features/store/objects/selection/actions/index.js.map +1 -1
- package/lib/features/store/objects/upload/DocumentUploadModal.js +1 -1
- package/lib/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.js +11 -3
- package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.js +14 -14
- package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
- package/lib/features/store/types/TypeRegistry.d.ts +275 -150
- package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +275 -150
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
- package/lib/features/user/UserInfo.d.ts +2 -0
- package/lib/features/user/UserInfo.d.ts.map +1 -1
- package/lib/features/user/UserInfo.js +1 -1
- package/lib/features/user/UserInfo.js.map +1 -1
- package/lib/i18n/locales/ar.json +28 -7
- package/lib/i18n/locales/de.json +24 -7
- package/lib/i18n/locales/en.json +24 -7
- package/lib/i18n/locales/es.json +24 -7
- package/lib/i18n/locales/fr.json +25 -7
- package/lib/i18n/locales/it.json +24 -7
- package/lib/i18n/locales/ja.json +24 -7
- package/lib/i18n/locales/ko.json +24 -7
- package/lib/i18n/locales/pt.json +24 -7
- package/lib/i18n/locales/ru.json +26 -7
- package/lib/i18n/locales/tr.json +24 -7
- package/lib/i18n/locales/zh-TW.json +24 -7
- package/lib/i18n/locales/zh.json +24 -7
- package/lib/router/RouteComponent.d.ts.map +1 -1
- package/lib/router/RouteComponent.js +15 -12
- package/lib/router/RouteComponent.js.map +1 -1
- package/lib/session/UserSession.d.ts.map +1 -1
- package/lib/session/UserSession.js +4 -0
- package/lib/session/UserSession.js.map +1 -1
- package/lib/shell/SplashScreen.js +1 -1
- package/lib/shell/SplashScreen.js.map +1 -1
- package/lib/shell/apps/AppInstallationProvider.d.ts +4 -4
- package/lib/shell/login/SignInIcons.d.ts.map +1 -1
- package/lib/shell/login/SignInIcons.js +2 -1
- package/lib/shell/login/SignInIcons.js.map +1 -1
- package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
- package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
- package/lib/shell/login/SignInPasswordStep.js +58 -0
- package/lib/shell/login/SignInPasswordStep.js.map +1 -0
- package/lib/shell/login/SignInPrimitives.d.ts +13 -0
- package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
- package/lib/shell/login/SignInPrimitives.js +11 -1
- package/lib/shell/login/SignInPrimitives.js.map +1 -1
- package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
- package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
- package/lib/shell/login/SignInProvidersStep.js +4 -2
- package/lib/shell/login/SignInProvidersStep.js.map +1 -1
- package/lib/shell/login/SignInReturningStep.d.ts +3 -1
- package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
- package/lib/shell/login/SignInReturningStep.js +18 -8
- package/lib/shell/login/SignInReturningStep.js.map +1 -1
- package/lib/shell/login/SignInTenantStep.d.ts +3 -1
- package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
- package/lib/shell/login/SignInTenantStep.js +2 -2
- package/lib/shell/login/SignInTenantStep.js.map +1 -1
- package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
- package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
- package/lib/shell/login/SignInWithProviderButton.js +8 -7
- package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
- package/lib/shell/login/SigninScreen.d.ts.map +1 -1
- package/lib/shell/login/SigninScreen.js +13 -4
- package/lib/shell/login/SigninScreen.js.map +1 -1
- package/lib/shell/login/signInUtils.d.ts +20 -4
- package/lib/shell/login/signInUtils.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.js +47 -1
- package/lib/shell/login/signInUtils.js.map +1 -1
- package/lib/vertesia-ui-core.js +3 -3
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +33 -33
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +3 -3
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-router.js +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +15 -9
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +2 -2
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/lib/widgets/json-view/JSONCode.js +1 -1
- package/lib/widgets/json-view/JSONCode.js.map +1 -1
- package/lib/widgets/upload/DropZone.d.ts.map +1 -1
- package/lib/widgets/upload/DropZone.js +1 -1
- package/lib/widgets/upload/DropZone.js.map +1 -1
- package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
- package/package.json +7 -7
- package/src/core/components/Link.tsx +1 -1
- package/src/core/components/SelectList.tsx +5 -2
- package/src/core/components/SidePanel.tsx +65 -60
- package/src/core/components/Spinner.tsx +1 -1
- package/src/core/components/TagsInput.tsx +7 -7
- package/src/core/components/shadcn/badge.tsx +1 -1
- package/src/core/components/shadcn/button.tsx +2 -3
- package/src/core/components/shadcn/filters/comboBox/DateCombobox.tsx +2 -2
- package/src/core/components/shadcn/filters/comboBox/SelectCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/comboBox/StringListCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/comboBox/TextCombobox.tsx +1 -1
- package/src/core/components/shadcn/filters/filter/dateFilter.tsx +1 -1
- package/src/core/components/shadcn/filters/filters.tsx +1 -1
- package/src/core/components/shadcn/overflowTabs.tsx +2 -2
- package/src/core/components/shadcn/tabs.tsx +1 -1
- package/src/css/utilities.css +7 -0
- package/src/features/activity-doc/ActivityDoc.tsx +1 -1
- package/src/features/agent/chat/AskUserWidget.test.tsx +24 -2
- package/src/features/agent/chat/AskUserWidget.tsx +19 -4
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +150 -1
- package/src/features/agent/chat/ModernAgentConversation.tsx +59 -21
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +62 -0
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +8 -7
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
- package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +4 -3
- package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
- package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
- package/src/features/layout/GenericPageNavHeader.test.tsx +36 -0
- package/src/features/layout/GenericPageNavHeader.tsx +1 -1
- package/src/features/magic-pdf/AnnotatedImageSlider.tsx +1 -1
- package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
- package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
- package/src/features/oauth/McpConnectionsButton.tsx +9 -6
- package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
- package/src/features/oauth/useMcpConnections.test.ts +10 -0
- package/src/features/oauth/useMcpConnections.ts +18 -10
- package/src/features/permissions/UserPermissionsProvider.tsx +1 -1
- package/src/features/store/objects/DocumentPreviewPanel.tsx +10 -10
- package/src/features/store/objects/components/DocumentInput.tsx +1 -1
- package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
- package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
- package/src/features/store/objects/selection/actions/RunIntakeAction.tsx +69 -0
- package/src/features/store/objects/selection/actions/index.ts +1 -0
- package/src/features/store/objects/upload/DocumentUploadModal.tsx +1 -1
- package/src/features/store/types/ContentObjectTypesTable.tsx +20 -3
- package/src/features/store/types/IntakePolicyEditor.tsx +33 -28
- package/src/features/user/UserInfo.tsx +1 -1
- package/src/i18n/locales/ar.json +28 -7
- package/src/i18n/locales/de.json +24 -7
- package/src/i18n/locales/en.json +24 -7
- package/src/i18n/locales/es.json +24 -7
- package/src/i18n/locales/fr.json +25 -7
- package/src/i18n/locales/it.json +24 -7
- package/src/i18n/locales/ja.json +24 -7
- package/src/i18n/locales/ko.json +24 -7
- package/src/i18n/locales/pt.json +24 -7
- package/src/i18n/locales/ru.json +26 -7
- package/src/i18n/locales/tr.json +24 -7
- package/src/i18n/locales/zh-TW.json +24 -7
- package/src/i18n/locales/zh.json +24 -7
- package/src/router/RouteComponent.test.tsx +65 -1
- package/src/router/RouteComponent.tsx +18 -15
- package/src/session/UserSession.test.ts +65 -0
- package/src/session/UserSession.ts +4 -0
- package/src/shell/SplashScreen.tsx +1 -1
- package/src/shell/login/SignInIcons.tsx +2 -1
- package/src/shell/login/SignInPasswordRequired.test.tsx +82 -0
- package/src/shell/login/SignInPasswordStep.test.tsx +71 -0
- package/src/shell/login/SignInPasswordStep.tsx +103 -0
- package/src/shell/login/SignInPrimitives.tsx +53 -1
- package/src/shell/login/SignInProvidersStep.tsx +8 -4
- package/src/shell/login/SignInReturningStep.tsx +20 -8
- package/src/shell/login/SignInTenantStep.tsx +6 -2
- package/src/shell/login/SignInWithProviderButton.tsx +11 -7
- package/src/shell/login/SigninScreen.tsx +32 -3
- package/src/shell/login/signInPassword.test.ts +133 -0
- package/src/shell/login/signInUtils.ts +60 -7
- package/src/widgets/json-view/JSONCode.test.tsx +2 -2
- package/src/widgets/json-view/JSONCode.tsx +1 -1
- package/src/widgets/upload/DropZone.tsx +2 -4
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
- package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +0 -104
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({ signInWithPassword: vi.fn() }));
|
|
5
|
+
|
|
6
|
+
vi.mock('@vertesia/ui/i18n', () => ({
|
|
7
|
+
useUITranslation: () => ({ t: (key: string) => key }),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('./signInUtils', () => ({ signInWithPassword: mocks.signInWithPassword }));
|
|
11
|
+
|
|
12
|
+
const { default: SignInPasswordStep } = await import('./SignInPasswordStep');
|
|
13
|
+
|
|
14
|
+
const EMAIL = 'claude@reviewers.vertesia.io';
|
|
15
|
+
|
|
16
|
+
function submitWith(password: string) {
|
|
17
|
+
fireEvent.change(screen.getByLabelText('auth.password.label'), { target: { value: password } });
|
|
18
|
+
fireEvent.click(screen.getByRole('button', { name: /auth.password.submit/ }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SignInPasswordStep', () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
mocks.signInWithPassword.mockReset();
|
|
24
|
+
document.body.innerHTML = '';
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('asks for a password before calling sign-in', () => {
|
|
28
|
+
render(<SignInPasswordStep email={EMAIL} onBack={vi.fn()} />);
|
|
29
|
+
|
|
30
|
+
fireEvent.click(screen.getByRole('button', { name: /auth.password.submit/ }));
|
|
31
|
+
|
|
32
|
+
expect(screen.getByRole('alert').textContent).toBe('auth.password.required');
|
|
33
|
+
expect(mocks.signInWithPassword).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('shows the failure and lets the user retry', async () => {
|
|
37
|
+
mocks.signInWithPassword.mockResolvedValue({ ok: false, reason: 'invalid-credentials' });
|
|
38
|
+
render(<SignInPasswordStep email={EMAIL} onBack={vi.fn()} />);
|
|
39
|
+
|
|
40
|
+
submitWith('wrong');
|
|
41
|
+
|
|
42
|
+
await waitFor(() => expect(screen.getByRole('alert').textContent).toBe('auth.password.invalidCredentials'));
|
|
43
|
+
expect(mocks.signInWithPassword).toHaveBeenCalledWith(EMAIL, 'wrong');
|
|
44
|
+
expect((screen.getByRole('button', { name: /auth.password.submit/ }) as HTMLButtonElement).disabled).toBe(
|
|
45
|
+
false,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Success hands off to the session's auth-state listener, which unmounts the screen; until then
|
|
50
|
+
// the button must stay disabled so a second click cannot start a second sign-in.
|
|
51
|
+
it('keeps the submit disabled after a successful sign-in', async () => {
|
|
52
|
+
mocks.signInWithPassword.mockResolvedValue({ ok: true });
|
|
53
|
+
render(<SignInPasswordStep email={EMAIL} onBack={vi.fn()} />);
|
|
54
|
+
|
|
55
|
+
submitWith('secret');
|
|
56
|
+
|
|
57
|
+
await waitFor(() => expect(mocks.signInWithPassword).toHaveBeenCalledOnce());
|
|
58
|
+
const submit = screen.getAllByRole('button').find((b) => (b as HTMLButtonElement).type === 'submit');
|
|
59
|
+
expect((submit as HTMLButtonElement).disabled).toBe(true);
|
|
60
|
+
expect(screen.queryByRole('alert')).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns to the email step from the change link', () => {
|
|
64
|
+
const onBack = vi.fn();
|
|
65
|
+
render(<SignInPasswordStep email={EMAIL} onBack={onBack} />);
|
|
66
|
+
|
|
67
|
+
fireEvent.click(screen.getByRole('button', { name: 'auth.change' }));
|
|
68
|
+
|
|
69
|
+
expect(onBack).toHaveBeenCalledOnce();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Spinner } from '@vertesia/ui/core';
|
|
2
|
+
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
3
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
SignInEmailRow,
|
|
7
|
+
SignInPasswordField,
|
|
8
|
+
SignInStepButton,
|
|
9
|
+
SignInStepHeader,
|
|
10
|
+
SignInStepLayout,
|
|
11
|
+
} from './SignInPrimitives';
|
|
12
|
+
import { type PasswordSignInFailure, signInWithPassword } from './signInUtils';
|
|
13
|
+
|
|
14
|
+
interface SignInPasswordStepProps {
|
|
15
|
+
email: string;
|
|
16
|
+
tenantName?: string;
|
|
17
|
+
onBack: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Password sign-in for an address whose tenant issues passwords instead of federating an IdP. */
|
|
21
|
+
export default function SignInPasswordStep({ email, tenantName, onBack }: SignInPasswordStepProps) {
|
|
22
|
+
const { t } = useUITranslation();
|
|
23
|
+
const [password, setPassword] = useState('');
|
|
24
|
+
const [failure, setFailure] = useState<PasswordSignInFailure | null>(null);
|
|
25
|
+
const [emptyError, setEmptyError] = useState(false);
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
27
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
inputRef.current?.focus();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const submit = async (e: React.FormEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
if (loading) return;
|
|
36
|
+
if (!password) {
|
|
37
|
+
setEmptyError(true);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setEmptyError(false);
|
|
41
|
+
setFailure(null);
|
|
42
|
+
setLoading(true);
|
|
43
|
+
const result = await signInWithPassword(email, password);
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
setFailure(result.reason);
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
// On success the spinner stays up: the session's auth-state listener finishes the login and
|
|
49
|
+
// unmounts this screen.
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Literal keys, one per failure, so i18next-cli extraction sees every message.
|
|
53
|
+
const failureMessage = (reason: PasswordSignInFailure) => {
|
|
54
|
+
switch (reason) {
|
|
55
|
+
case 'invalid-credentials':
|
|
56
|
+
return t('auth.password.invalidCredentials');
|
|
57
|
+
case 'too-many-attempts':
|
|
58
|
+
return t('auth.password.tooManyAttempts');
|
|
59
|
+
case 'not-password-tenant':
|
|
60
|
+
return t('auth.password.notAvailable');
|
|
61
|
+
case 'failed':
|
|
62
|
+
return t('auth.password.failed');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const error = emptyError ? t('auth.password.required') : failure ? failureMessage(failure) : undefined;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<SignInStepLayout>
|
|
69
|
+
<SignInStepHeader
|
|
70
|
+
eyebrow={tenantName ?? t('auth.password.eyebrow')}
|
|
71
|
+
title={t('auth.password.title')}
|
|
72
|
+
body={t('auth.password.body')}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<SignInEmailRow email={email} actionLabel={t('auth.change')} onAction={onBack} />
|
|
76
|
+
|
|
77
|
+
<form onSubmit={submit} noValidate className={`flex flex-col ${error ? 'gap-2' : 'gap-6'}`}>
|
|
78
|
+
<SignInPasswordField
|
|
79
|
+
inputRef={inputRef}
|
|
80
|
+
label={t('auth.password.label')}
|
|
81
|
+
value={password}
|
|
82
|
+
onChange={(value) => {
|
|
83
|
+
setPassword(value);
|
|
84
|
+
if (emptyError) setEmptyError(false);
|
|
85
|
+
}}
|
|
86
|
+
invalid={!!error}
|
|
87
|
+
error={error}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<SignInStepButton type="submit" disabled={loading}>
|
|
91
|
+
{loading ? (
|
|
92
|
+
<Spinner />
|
|
93
|
+
) : (
|
|
94
|
+
<>
|
|
95
|
+
<span>{t('auth.password.submit')}</span>
|
|
96
|
+
<ArrowRight className="!size-3.5" />
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</SignInStepButton>
|
|
100
|
+
</form>
|
|
101
|
+
</SignInStepLayout>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -363,6 +363,11 @@ export function SignInOrDivider({ children }: { children: ReactNode }) {
|
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
const SIGNIN_INPUT_CLASS =
|
|
367
|
+
'h-[42px] px-3.5 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted ' +
|
|
368
|
+
'outline-none transition focus:border-info focus:ring-4 focus:ring-info/15 ' +
|
|
369
|
+
'aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/15';
|
|
370
|
+
|
|
366
371
|
interface SignInEmailFieldProps {
|
|
367
372
|
inputRef?: Ref<HTMLInputElement>;
|
|
368
373
|
label: ReactNode;
|
|
@@ -393,7 +398,7 @@ export function SignInEmailField({
|
|
|
393
398
|
id="vt-login-email"
|
|
394
399
|
name="vt-login-email"
|
|
395
400
|
type="email"
|
|
396
|
-
className=
|
|
401
|
+
className={SIGNIN_INPUT_CLASS}
|
|
397
402
|
placeholder={placeholder}
|
|
398
403
|
value={value}
|
|
399
404
|
onChange={(e) => onChange(e.target.value)}
|
|
@@ -414,3 +419,50 @@ export function SignInEmailField({
|
|
|
414
419
|
</div>
|
|
415
420
|
);
|
|
416
421
|
}
|
|
422
|
+
|
|
423
|
+
interface SignInPasswordFieldProps {
|
|
424
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
425
|
+
label: ReactNode;
|
|
426
|
+
value: string;
|
|
427
|
+
onChange: (value: string) => void;
|
|
428
|
+
invalid?: boolean;
|
|
429
|
+
error?: ReactNode;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Labeled password input with an inline error. Unlike the email field it leaves password managers
|
|
434
|
+
* enabled: a password account's credential is issued once and is expected to be stored in one.
|
|
435
|
+
*/
|
|
436
|
+
export function SignInPasswordField({
|
|
437
|
+
inputRef,
|
|
438
|
+
label,
|
|
439
|
+
value,
|
|
440
|
+
onChange,
|
|
441
|
+
invalid = false,
|
|
442
|
+
error,
|
|
443
|
+
}: SignInPasswordFieldProps) {
|
|
444
|
+
return (
|
|
445
|
+
<div className="flex flex-col gap-1.5">
|
|
446
|
+
<label htmlFor="vt-login-password" className="text-xs font-medium text-foreground/80">
|
|
447
|
+
{label}
|
|
448
|
+
</label>
|
|
449
|
+
<input
|
|
450
|
+
ref={inputRef}
|
|
451
|
+
id="vt-login-password"
|
|
452
|
+
name="vt-login-password"
|
|
453
|
+
type="password"
|
|
454
|
+
className={SIGNIN_INPUT_CLASS}
|
|
455
|
+
value={value}
|
|
456
|
+
onChange={(e) => onChange(e.target.value)}
|
|
457
|
+
aria-invalid={invalid}
|
|
458
|
+
aria-describedby={error ? 'vt-login-password-error' : undefined}
|
|
459
|
+
autoComplete="current-password"
|
|
460
|
+
/>
|
|
461
|
+
{error && (
|
|
462
|
+
<div id="vt-login-password-error" role="alert" className="text-xs text-destructive">
|
|
463
|
+
{error}
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
2
2
|
import { SignInEmailRow, SignInProviderButton, SignInStepHeader, SignInStepLayout } from './SignInPrimitives';
|
|
3
|
-
import { type ProviderId, providerLabel, startSignIn } from './signInUtils';
|
|
3
|
+
import { type ProviderId, providerLabel, type RedirectProviderId, startSignIn } from './signInUtils';
|
|
4
4
|
|
|
5
5
|
interface SignInProvidersStepProps {
|
|
6
6
|
email: string;
|
|
7
7
|
onBack: () => void;
|
|
8
8
|
onProviderClicked: (provider: ProviderId) => void;
|
|
9
|
+
/** The address resolved to a password tenant on retry, so the parent shows the password step. */
|
|
10
|
+
onPasswordRequired: (email: string) => void;
|
|
9
11
|
redirectTo?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
const PROVIDERS:
|
|
14
|
+
const PROVIDERS: RedirectProviderId[] = ['google', 'github', 'microsoft'];
|
|
13
15
|
|
|
14
16
|
export default function SignInProvidersStep({
|
|
15
17
|
email,
|
|
16
18
|
onBack,
|
|
17
19
|
onProviderClicked,
|
|
20
|
+
onPasswordRequired,
|
|
18
21
|
redirectTo,
|
|
19
22
|
}: SignInProvidersStepProps) {
|
|
20
23
|
const { t } = useUITranslation();
|
|
21
24
|
|
|
22
|
-
const pick = async (provider:
|
|
25
|
+
const pick = async (provider: RedirectProviderId) => {
|
|
23
26
|
onProviderClicked(provider);
|
|
24
|
-
await startSignIn(provider, email, redirectTo);
|
|
27
|
+
const result = await startSignIn(provider, email, redirectTo);
|
|
28
|
+
if (!result.ok && result.reason === 'password-required') onPasswordRequired(email);
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
return (
|
|
@@ -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
|
-
|
|
41
|
-
? t('auth.
|
|
42
|
-
:
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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={
|
|
91
|
+
provider={lastProvider}
|
|
80
92
|
label={primaryLabel}
|
|
81
|
-
onClick={
|
|
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
|
|
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:
|
|
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
|
|
3
|
+
import { providerLabel, type RedirectProviderId, startSignIn, startSignInWithoutTenant } from './signInUtils';
|
|
4
4
|
|
|
5
5
|
interface SignInWithProviderButtonProps {
|
|
6
|
-
provider:
|
|
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
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { useCallback, useEffect, useState } from 'react';
|
|
16
16
|
import SignInAuthPending from './SignInAuthPending';
|
|
17
17
|
import SignInEmailStep, { type TenantInfo } from './SignInEmailStep';
|
|
18
|
+
import SignInPasswordStep from './SignInPasswordStep';
|
|
18
19
|
import SignInProvidersStep from './SignInProvidersStep';
|
|
19
20
|
import SignInRecoveryStep, { type SignInRecoveryKind } from './SignInRecoveryStep';
|
|
20
21
|
import SignInRestrictedEnvStep from './SignInRestrictedEnvStep';
|
|
@@ -79,6 +80,7 @@ type Mode =
|
|
|
79
80
|
| 'email'
|
|
80
81
|
| 'providers'
|
|
81
82
|
| 'tenant'
|
|
83
|
+
| 'password'
|
|
82
84
|
| 'blocked'
|
|
83
85
|
| 'returning'
|
|
84
86
|
| 'pending'
|
|
@@ -185,7 +187,13 @@ function SigninScreenImpl({
|
|
|
185
187
|
const onProceedFromEmail = useCallback((e: string, t: TenantInfo | undefined) => {
|
|
186
188
|
setEmail(e);
|
|
187
189
|
setTenant(t);
|
|
188
|
-
|
|
190
|
+
// A password tenant has no identity provider to pick; ask for its password directly.
|
|
191
|
+
setMode(t?.provider === 'password' ? 'password' : t ? 'tenant' : 'providers');
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
const onPasswordRequired = useCallback((e: string) => {
|
|
195
|
+
setEmail(e);
|
|
196
|
+
setMode('password');
|
|
189
197
|
}, []);
|
|
190
198
|
|
|
191
199
|
const onBack = useCallback(() => {
|
|
@@ -336,6 +344,14 @@ function SigninScreenImpl({
|
|
|
336
344
|
);
|
|
337
345
|
} else if (mode === 'signup' && !localStorage.getItem('tenantName')) {
|
|
338
346
|
content = <SignupForm onSignup={onSignup} goBack={startOver} />;
|
|
347
|
+
} else if (mode === 'password' && email) {
|
|
348
|
+
content = (
|
|
349
|
+
<SignInPasswordStep
|
|
350
|
+
email={email}
|
|
351
|
+
tenantName={tenant?.label || tenant?.name || storedSession?.tenantName || undefined}
|
|
352
|
+
onBack={onBack}
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
339
355
|
} else if (mode === 'tenant' && tenant) {
|
|
340
356
|
content = (
|
|
341
357
|
<SignInTenantStep
|
|
@@ -343,13 +359,26 @@ function SigninScreenImpl({
|
|
|
343
359
|
tenant={tenant}
|
|
344
360
|
onBack={onBack}
|
|
345
361
|
onProviderClicked={() => onProviderClicked((tenant.provider ?? 'oidc') as ProviderId)}
|
|
362
|
+
onPasswordRequired={onPasswordRequired}
|
|
346
363
|
/>
|
|
347
364
|
);
|
|
348
365
|
} else if (mode === 'providers') {
|
|
349
|
-
content =
|
|
366
|
+
content = (
|
|
367
|
+
<SignInProvidersStep
|
|
368
|
+
email={email}
|
|
369
|
+
onBack={onBack}
|
|
370
|
+
onProviderClicked={onProviderClicked}
|
|
371
|
+
onPasswordRequired={onPasswordRequired}
|
|
372
|
+
/>
|
|
373
|
+
);
|
|
350
374
|
} else if (mode === 'returning' && storedSession) {
|
|
351
375
|
content = (
|
|
352
|
-
<SignInReturningStep
|
|
376
|
+
<SignInReturningStep
|
|
377
|
+
session={storedSession}
|
|
378
|
+
onNotYou={onNotYou}
|
|
379
|
+
onProviderClicked={onProviderClicked}
|
|
380
|
+
onPasswordRequired={onPasswordRequired}
|
|
381
|
+
/>
|
|
353
382
|
);
|
|
354
383
|
} else {
|
|
355
384
|
content = <SignInEmailStep initialEmail={email} onProceed={onProceedFromEmail} />;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { FirebaseError } from 'firebase/app';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
auth: { tenantId: null as string | null },
|
|
6
|
+
setFirebaseTenant: vi.fn(),
|
|
7
|
+
signInWithEmailAndPassword: vi.fn(),
|
|
8
|
+
signInWithRedirect: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@vertesia/ui/session', () => ({
|
|
12
|
+
getFirebaseAuth: () => mocks.auth,
|
|
13
|
+
setFirebaseTenant: mocks.setFirebaseTenant,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('firebase/auth', async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal<typeof import('firebase/auth')>()),
|
|
18
|
+
signInWithEmailAndPassword: mocks.signInWithEmailAndPassword,
|
|
19
|
+
signInWithRedirect: mocks.signInWithRedirect,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const { passwordFailureReason, readPendingSignin, signInWithPassword, startSignIn } = await import('./signInUtils');
|
|
23
|
+
|
|
24
|
+
const PASSWORD_TENANT = {
|
|
25
|
+
label: 'Vertesia Reviewers',
|
|
26
|
+
name: 'reviewers',
|
|
27
|
+
firebaseTenantId: 'reviewers-abc12',
|
|
28
|
+
provider: 'password',
|
|
29
|
+
domain: ['reviewers.vertesia.io'],
|
|
30
|
+
};
|
|
31
|
+
const OIDC_TENANT = { ...PASSWORD_TENANT, label: 'Acme', name: 'acme', provider: 'oidc', domain: ['acme.com'] };
|
|
32
|
+
const EMAIL = 'claude@reviewers.vertesia.io';
|
|
33
|
+
|
|
34
|
+
describe('passwordFailureReason', () => {
|
|
35
|
+
// One answer for every credential failure, so the form cannot be used to probe which
|
|
36
|
+
// addresses exist or are disabled.
|
|
37
|
+
it.each([
|
|
38
|
+
'auth/invalid-credential',
|
|
39
|
+
'auth/wrong-password',
|
|
40
|
+
'auth/user-not-found',
|
|
41
|
+
'auth/user-disabled',
|
|
42
|
+
'auth/invalid-email',
|
|
43
|
+
])('folds %s into invalid-credentials', (code) => {
|
|
44
|
+
expect(passwordFailureReason(new FirebaseError(code, code))).toBe('invalid-credentials');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('reports throttling separately', () => {
|
|
48
|
+
expect(passwordFailureReason(new FirebaseError('auth/too-many-requests', 'throttled'))).toBe(
|
|
49
|
+
'too-many-attempts',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('treats anything else as a generic failure', () => {
|
|
54
|
+
expect(passwordFailureReason(new FirebaseError('auth/network-request-failed', 'offline'))).toBe('failed');
|
|
55
|
+
expect(passwordFailureReason(new Error('boom'))).toBe('failed');
|
|
56
|
+
expect(passwordFailureReason(undefined)).toBe('failed');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('signInWithPassword', () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
mocks.setFirebaseTenant.mockReset();
|
|
63
|
+
mocks.signInWithEmailAndPassword.mockReset();
|
|
64
|
+
mocks.signInWithRedirect.mockReset();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
sessionStorage.clear();
|
|
69
|
+
localStorage.clear();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('signs a password-tenant user in place and records the pending sign-in', async () => {
|
|
73
|
+
mocks.setFirebaseTenant.mockResolvedValue(PASSWORD_TENANT);
|
|
74
|
+
mocks.signInWithEmailAndPassword.mockResolvedValue({});
|
|
75
|
+
|
|
76
|
+
await expect(signInWithPassword(EMAIL, 'secret')).resolves.toEqual({ ok: true });
|
|
77
|
+
|
|
78
|
+
expect(mocks.setFirebaseTenant).toHaveBeenCalledWith(EMAIL);
|
|
79
|
+
expect(mocks.signInWithEmailAndPassword).toHaveBeenCalledWith(mocks.auth, EMAIL, 'secret');
|
|
80
|
+
expect(readPendingSignin()).toEqual({
|
|
81
|
+
email: EMAIL,
|
|
82
|
+
provider: 'password',
|
|
83
|
+
tenantName: 'Vertesia Reviewers',
|
|
84
|
+
});
|
|
85
|
+
expect(mocks.signInWithRedirect).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The password form is reachable only through tenant resolution, but the helper re-checks: an
|
|
89
|
+
// address that resolves to a federated tenant must never be offered a password.
|
|
90
|
+
it('refuses an address whose tenant is not a password tenant', async () => {
|
|
91
|
+
mocks.setFirebaseTenant.mockResolvedValue(OIDC_TENANT);
|
|
92
|
+
|
|
93
|
+
await expect(signInWithPassword('someone@acme.com', 'secret')).resolves.toEqual({
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: 'not-password-tenant',
|
|
96
|
+
});
|
|
97
|
+
expect(mocks.signInWithEmailAndPassword).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('fails without signing in when the tenant cannot be resolved', async () => {
|
|
101
|
+
mocks.setFirebaseTenant.mockResolvedValue(undefined);
|
|
102
|
+
|
|
103
|
+
await expect(signInWithPassword(EMAIL, 'secret')).resolves.toEqual({ ok: false, reason: 'failed' });
|
|
104
|
+
expect(mocks.signInWithEmailAndPassword).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('clears the pending sign-in when Firebase rejects the credential', async () => {
|
|
108
|
+
mocks.setFirebaseTenant.mockResolvedValue(PASSWORD_TENANT);
|
|
109
|
+
mocks.signInWithEmailAndPassword.mockRejectedValue(new FirebaseError('auth/invalid-credential', 'bad'));
|
|
110
|
+
|
|
111
|
+
await expect(signInWithPassword(EMAIL, 'wrong')).resolves.toEqual({
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: 'invalid-credentials',
|
|
114
|
+
});
|
|
115
|
+
expect(readPendingSignin()).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('startSignIn', () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
mocks.setFirebaseTenant.mockReset();
|
|
122
|
+
mocks.signInWithRedirect.mockReset();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// A returning session's stored provider can go stale; a password tenant has nowhere to redirect.
|
|
126
|
+
it('does not redirect when the address resolves to a password tenant', async () => {
|
|
127
|
+
mocks.setFirebaseTenant.mockResolvedValue(PASSWORD_TENANT);
|
|
128
|
+
|
|
129
|
+
await expect(startSignIn('google', EMAIL)).resolves.toEqual({ ok: false, reason: 'password-required' });
|
|
130
|
+
expect(mocks.signInWithRedirect).not.toHaveBeenCalled();
|
|
131
|
+
expect(readPendingSignin()).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
});
|