@vertesia/ui 1.6.0-dev.20261004.085740Z → 1.6.0-dev.20261008.064737Z
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/boot/vite.d.ts +3 -0
- package/lib/boot/vite.d.ts.map +1 -1
- package/lib/boot/vite.js +12 -0
- package/lib/boot/vite.js.map +1 -1
- package/lib/core/components/shadcn/tabs.d.ts +2 -1
- package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
- package/lib/core/components/shadcn/tabs.js +6 -4
- package/lib/core/components/shadcn/tabs.js.map +1 -1
- package/lib/env/index.d.ts +8 -0
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.js +16 -10
- package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.d.ts +7 -1
- package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.js +15 -15
- package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
- package/lib/features/media-viewer/ImagePanel.d.ts +1 -1
- package/lib/features/media-viewer/ImagePanel.d.ts.map +1 -1
- package/lib/features/media-viewer/ImagePanel.js +40 -10
- package/lib/features/media-viewer/ImagePanel.js.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.js +1 -1
- package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
- package/lib/i18n/locales/ar.json +14 -0
- package/lib/i18n/locales/de.json +14 -0
- package/lib/i18n/locales/en.json +14 -0
- package/lib/i18n/locales/es.json +14 -0
- package/lib/i18n/locales/fr.json +14 -0
- package/lib/i18n/locales/it.json +14 -0
- package/lib/i18n/locales/ja.json +14 -0
- package/lib/i18n/locales/ko.json +14 -0
- package/lib/i18n/locales/pt.json +14 -0
- package/lib/i18n/locales/ru.json +14 -0
- package/lib/i18n/locales/tr.json +14 -0
- package/lib/i18n/locales/zh-TW.json +14 -0
- package/lib/i18n/locales/zh.json +14 -0
- package/lib/session/auth/oauth.d.ts.map +1 -1
- package/lib/session/auth/oauth.js +5 -3
- package/lib/session/auth/oauth.js.map +1 -1
- package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
- package/lib/shell/AuthScreenPreview.js +1 -0
- package/lib/shell/AuthScreenPreview.js.map +1 -1
- package/lib/shell/login/SignInFlow.d.ts +3 -1
- package/lib/shell/login/SignInFlow.d.ts.map +1 -1
- package/lib/shell/login/SignInFlow.js +13 -4
- package/lib/shell/login/SignInFlow.js.map +1 -1
- 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 +9 -3
- package/lib/shell/login/SigninScreen.js.map +1 -1
- package/lib/shell/login/signInUtils.d.ts +24 -4
- package/lib/shell/login/signInUtils.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.js +53 -1
- package/lib/shell/login/signInUtils.js.map +1 -1
- package/lib/vertesia-ui-boot.js.map +1 -1
- package/lib/vertesia-ui-core.js +42 -42
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-env.js.map +1 -1
- package/lib/vertesia-ui-features.js +339 -339
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.B0orbJb6.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BChwIQVA.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BTEcAVQm.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BeJQKbv7.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BpGr0-5g.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Bs8x-1wI.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BymQoOm0.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CNT_wabV.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Cxh1iVk0.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DLYCM9fV.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.QvAVguDs.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +2 -2
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-i18n.smozeSmI.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.smozeSmI.chunk.js.map +1 -0
- package/lib/vertesia-ui-layout.js +15 -15
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +40 -40
- package/lib/vertesia-ui-rich-text.js.map +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 +54 -47
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +47 -47
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +20 -20
- package/src/boot/vite.ts +15 -0
- package/src/core/components/shadcn/tabs.tsx +9 -3
- package/src/env/index.ts +10 -1
- package/src/features/agent/chat/AgentRequestInputOverlay.tsx +73 -34
- package/src/features/agent/chat/AskUserWidget.tsx +222 -205
- package/src/features/media-viewer/ImagePanel.tsx +51 -10
- package/src/features/store/types/IntakePolicyEditor.tsx +2 -1
- package/src/i18n/locales/ar.json +14 -0
- package/src/i18n/locales/de.json +14 -0
- package/src/i18n/locales/en.json +14 -0
- package/src/i18n/locales/es.json +14 -0
- package/src/i18n/locales/fr.json +14 -0
- package/src/i18n/locales/it.json +14 -0
- package/src/i18n/locales/ja.json +14 -0
- package/src/i18n/locales/ko.json +14 -0
- package/src/i18n/locales/pt.json +14 -0
- package/src/i18n/locales/ru.json +14 -0
- package/src/i18n/locales/tr.json +14 -0
- package/src/i18n/locales/zh-TW.json +14 -0
- package/src/i18n/locales/zh.json +14 -0
- package/src/session/auth/oauth.ts +8 -3
- package/src/shell/AuthScreenPreview.tsx +1 -0
- package/src/shell/login/SignInFlow.tsx +23 -2
- package/src/shell/login/SignInIcons.tsx +2 -1
- 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 +8 -3
- package/src/shell/login/signInUtils.ts +68 -8
- package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.gais--x5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.gais--x5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js.map +0 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { useCallback, useEffect, useState } from 'react';
|
|
14
14
|
import SignInAuthPending from './SignInAuthPending';
|
|
15
15
|
import SignInEmailStep, { type TenantInfo } from './SignInEmailStep';
|
|
16
|
+
import SignInPasswordStep from './SignInPasswordStep';
|
|
16
17
|
import SignInProvidersStep from './SignInProvidersStep';
|
|
17
18
|
import SignInReturningStep from './SignInReturningStep';
|
|
18
19
|
import SignInTenantStep from './SignInTenantStep';
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
} from './signInUtils';
|
|
29
30
|
|
|
30
31
|
/** The modes every host shares. Hosts may add their own; see the `M` parameter below. */
|
|
31
|
-
export type SignInCoreMode = 'email' | 'providers' | 'tenant' | 'returning' | 'pending';
|
|
32
|
+
export type SignInCoreMode = 'email' | 'providers' | 'tenant' | 'password' | 'returning' | 'pending';
|
|
32
33
|
|
|
33
34
|
export interface SignInFlowOptions {
|
|
34
35
|
/**
|
|
@@ -64,6 +65,8 @@ export interface SignInFlowController<M extends string = never> {
|
|
|
64
65
|
pendingProvider: ProviderId | null;
|
|
65
66
|
/** Email accepted: go to the tenant step when one resolved, otherwise the provider list. */
|
|
66
67
|
onProceedFromEmail: (email: string, tenant?: TenantInfo) => void;
|
|
68
|
+
/** Show password entry when tenant discovery or a remembered provider requires it. */
|
|
69
|
+
onPasswordRequired: (email: string) => void;
|
|
67
70
|
/** Back to the email step, dropping the resolved tenant. */
|
|
68
71
|
onBack: () => void;
|
|
69
72
|
/** "Not you?" — forget the remembered identity and sign out. */
|
|
@@ -92,7 +95,12 @@ export function useSignInFlow<M extends string = never>(options: SignInFlowOptio
|
|
|
92
95
|
const onProceedFromEmail = useCallback((e: string, t: TenantInfo | undefined) => {
|
|
93
96
|
setEmail(e);
|
|
94
97
|
setTenant(t);
|
|
95
|
-
setMode(t ? 'tenant' : 'providers');
|
|
98
|
+
setMode(t?.provider === 'password' ? 'password' : t ? 'tenant' : 'providers');
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const onPasswordRequired = useCallback((e: string) => {
|
|
102
|
+
setEmail(e);
|
|
103
|
+
setMode('password');
|
|
96
104
|
}, []);
|
|
97
105
|
|
|
98
106
|
const onBack = useCallback(() => {
|
|
@@ -144,6 +152,7 @@ export function useSignInFlow<M extends string = never>(options: SignInFlowOptio
|
|
|
144
152
|
setStoredSession,
|
|
145
153
|
pendingProvider,
|
|
146
154
|
onProceedFromEmail,
|
|
155
|
+
onPasswordRequired,
|
|
147
156
|
onBack,
|
|
148
157
|
onNotYou,
|
|
149
158
|
onProviderClicked,
|
|
@@ -178,6 +187,15 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
178
187
|
if (mode === 'pending' && pendingProvider) {
|
|
179
188
|
return <SignInAuthPending provider={pendingProvider} />;
|
|
180
189
|
}
|
|
190
|
+
if (mode === 'password' && email) {
|
|
191
|
+
return (
|
|
192
|
+
<SignInPasswordStep
|
|
193
|
+
email={email}
|
|
194
|
+
tenantName={tenant?.label || tenant?.name || storedSession?.tenantName || undefined}
|
|
195
|
+
onBack={flow.onBack}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
181
199
|
if (mode === 'tenant' && tenant) {
|
|
182
200
|
return (
|
|
183
201
|
<SignInTenantStep
|
|
@@ -185,6 +203,7 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
185
203
|
tenant={tenant}
|
|
186
204
|
onBack={flow.onBack}
|
|
187
205
|
onProviderClicked={() => flow.onProviderClicked((tenant.provider ?? 'oidc') as ProviderId)}
|
|
206
|
+
onPasswordRequired={flow.onPasswordRequired}
|
|
188
207
|
redirectTo={redirectTo}
|
|
189
208
|
/>
|
|
190
209
|
);
|
|
@@ -195,6 +214,7 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
195
214
|
email={email}
|
|
196
215
|
onBack={flow.onBack}
|
|
197
216
|
onProviderClicked={flow.onProviderClicked}
|
|
217
|
+
onPasswordRequired={flow.onPasswordRequired}
|
|
198
218
|
redirectTo={redirectTo}
|
|
199
219
|
/>
|
|
200
220
|
);
|
|
@@ -205,6 +225,7 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
205
225
|
session={storedSession}
|
|
206
226
|
onNotYou={flow.onNotYou}
|
|
207
227
|
onProviderClicked={flow.onProviderClicked}
|
|
228
|
+
onPasswordRequired={flow.onPasswordRequired}
|
|
208
229
|
redirectTo={redirectTo}
|
|
209
230
|
/>
|
|
210
231
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LockKeyhole } from 'lucide-react';
|
|
1
|
+
import { KeyRound, LockKeyhole } from 'lucide-react';
|
|
2
2
|
import type { ComponentType, SVGProps } from 'react';
|
|
3
3
|
import type { ProviderId } from './signInUtils';
|
|
4
4
|
|
|
@@ -58,6 +58,7 @@ export const PROVIDER_ICONS: Record<ProviderId, ComponentType<{ className?: stri
|
|
|
58
58
|
github: GithubIcon,
|
|
59
59
|
microsoft: MicrosoftIcon,
|
|
60
60
|
oidc: OidcIcon,
|
|
61
|
+
password: KeyRound,
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
export function providerIcon(provider: ProviderId | string | undefined): ComponentType<{ className?: string }> {
|
|
@@ -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
|
+
}
|
|
@@ -366,6 +366,11 @@ export function SignInOrDivider({ children }: { children: ReactNode }) {
|
|
|
366
366
|
);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
const SIGNIN_INPUT_CLASS =
|
|
370
|
+
'h-[42px] px-3.5 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted ' +
|
|
371
|
+
'outline-none transition focus:border-info focus:ring-4 focus:ring-info/15 ' +
|
|
372
|
+
'aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/15';
|
|
373
|
+
|
|
369
374
|
interface SignInEmailFieldProps {
|
|
370
375
|
inputRef?: Ref<HTMLInputElement>;
|
|
371
376
|
label: ReactNode;
|
|
@@ -396,7 +401,7 @@ export function SignInEmailField({
|
|
|
396
401
|
id="vt-login-email"
|
|
397
402
|
name="vt-login-email"
|
|
398
403
|
type="email"
|
|
399
|
-
className=
|
|
404
|
+
className={SIGNIN_INPUT_CLASS}
|
|
400
405
|
placeholder={placeholder}
|
|
401
406
|
value={value}
|
|
402
407
|
onChange={(e) => onChange(e.target.value)}
|
|
@@ -417,3 +422,50 @@ export function SignInEmailField({
|
|
|
417
422
|
</div>
|
|
418
423
|
);
|
|
419
424
|
}
|
|
425
|
+
|
|
426
|
+
interface SignInPasswordFieldProps {
|
|
427
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
428
|
+
label: ReactNode;
|
|
429
|
+
value: string;
|
|
430
|
+
onChange: (value: string) => void;
|
|
431
|
+
invalid?: boolean;
|
|
432
|
+
error?: ReactNode;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Labeled password input with an inline error. Unlike the email field it leaves password managers
|
|
437
|
+
* enabled: a password account's credential is issued once and is expected to be stored in one.
|
|
438
|
+
*/
|
|
439
|
+
export function SignInPasswordField({
|
|
440
|
+
inputRef,
|
|
441
|
+
label,
|
|
442
|
+
value,
|
|
443
|
+
onChange,
|
|
444
|
+
invalid = false,
|
|
445
|
+
error,
|
|
446
|
+
}: SignInPasswordFieldProps) {
|
|
447
|
+
return (
|
|
448
|
+
<div className="flex flex-col gap-1.5">
|
|
449
|
+
<label htmlFor="vt-login-password" className="text-xs font-medium text-foreground/80">
|
|
450
|
+
{label}
|
|
451
|
+
</label>
|
|
452
|
+
<input
|
|
453
|
+
ref={inputRef}
|
|
454
|
+
id="vt-login-password"
|
|
455
|
+
name="vt-login-password"
|
|
456
|
+
type="password"
|
|
457
|
+
className={SIGNIN_INPUT_CLASS}
|
|
458
|
+
value={value}
|
|
459
|
+
onChange={(e) => onChange(e.target.value)}
|
|
460
|
+
aria-invalid={invalid}
|
|
461
|
+
aria-describedby={error ? 'vt-login-password-error' : undefined}
|
|
462
|
+
autoComplete="current-password"
|
|
463
|
+
/>
|
|
464
|
+
{error && (
|
|
465
|
+
<div id="vt-login-password-error" role="alert" className="text-xs text-destructive">
|
|
466
|
+
{error}
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
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' && !
|
|
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<
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
277
|
+
Env.firebase?.tenantId ? ((Env.firebase.providerType as RedirectProviderId) ?? 'oidc') : provider,
|
|
218
278
|
undefined,
|
|
219
279
|
redirectTo,
|
|
220
280
|
),
|