bugfix: the flow for trial and accept invitation (org + staff)is now unified. all navigate to dshboard if succesfull.

This commit is contained in:
2026-08-19 00:39:31 +03:30
parent 6d90787c13
commit d6958b2e48
16 changed files with 138 additions and 43 deletions

View File

@@ -119,7 +119,8 @@
"labelCreatePassword": "Create password",
"labelConfirmPassword": "Confirm password",
"activateAccount": "Activate account",
"invitationAcceptedRedirect": "Invitation Accepted. Redirecting to login...",
"invitationAcceptedRedirect": "Invitation accepted. Opening your workspace...",
"invitationAcceptedSignInFailed": "Account activated, but sign-in failed. Please log in with your password.",
"errorAcceptInvitation": "Could not accept invitation",
"alreadyHaveAccess": "Already have access?",
"goToLogin": "Go to login",
@@ -128,7 +129,7 @@
"invitedBy": "Invited by:",
"ownerEmail": "Owner email",
"activateOrganization": "Activate organization",
"organizationAcceptedRedirect": "Invitation accepted. Redirecting to login...",
"organizationAcceptedRedirect": "Invitation accepted. Opening your workspace...",
"stepAccount": "Account",
"stepOrganization": "Organization",
"organizationName": "Organization name",

View File

@@ -119,7 +119,8 @@
"labelCreatePassword": "ایجاد رمز عبور",
"labelConfirmPassword": "تأیید رمز عبور",
"activateAccount": "فعال‌سازی حساب",
"invitationAcceptedRedirect": "دعوتنامه پذیرفته شد. در حال انتقال به صفحه ورود...",
"invitationAcceptedRedirect": "دعوتنامه پذیرفته شد. در حال ورود به فضای کاری...",
"invitationAcceptedSignInFailed": "حساب فعال شد، اما ورود انجام نشد. لطفاً با رمز عبور خود وارد شوید.",
"errorAcceptInvitation": "پذیرش دعوتنامه امکان‌پذیر نبود",
"alreadyHaveAccess": "از قبل دسترسی دارید؟",
"goToLogin": "رفتن به ورود",
@@ -128,7 +129,7 @@
"invitedBy": "دعوت‌کننده:",
"ownerEmail": "ایمیل مالک",
"activateOrganization": "فعال‌سازی سازمان",
"organizationAcceptedRedirect": "دعوتنامه پذیرفته شد. در حال انتقال به صفحه ورود...",
"organizationAcceptedRedirect": "دعوتنامه پذیرفته شد. در حال ورود به فضای کاری...",
"stepAccount": "حساب",
"stepOrganization": "سازمان",
"organizationName": "نام سازمان",

View File

@@ -119,7 +119,8 @@
"labelCreatePassword": "Wachtwoord aanmaken",
"labelConfirmPassword": "Bevestig wachtwoord",
"activateAccount": "Account activeren",
"invitationAcceptedRedirect": "Uitnodiging geaccepteerd. Doorsturen naar inloggen...",
"invitationAcceptedRedirect": "Uitnodiging geaccepteerd. Uw werkruimte wordt geopend...",
"invitationAcceptedSignInFailed": "Account geactiveerd, maar aanmelden is mislukt. Log in met uw wachtwoord.",
"errorAcceptInvitation": "Kon uitnodiging niet accepteren",
"alreadyHaveAccess": "Heeft u al toegang?",
"goToLogin": "Ga naar inloggen",
@@ -128,7 +129,7 @@
"invitedBy": "Uitgenodigd door:",
"ownerEmail": "E-mail eigenaar",
"activateOrganization": "Organisatie activeren",
"organizationAcceptedRedirect": "Uitnodiging geaccepteerd. Doorsturen naar inloggen...",
"organizationAcceptedRedirect": "Uitnodiging geaccepteerd. Uw werkruimte wordt geopend...",
"stepAccount": "Account",
"stepOrganization": "Organisatie",
"organizationName": "Organisatienaam",

View File

@@ -9,12 +9,15 @@ import { Button } from '@/components/ui/shared/Button';
import { Input } from '@/components/ui/shared/Input';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { staffApi } from '@/lib/api/staff';
import { useAuth } from '@/lib/hooks/useAuth';
import { navigateIntoAppIfOrgSelected } from '@/lib/auth/postAuthRedirect';
function AcceptInviteContent() {
const t = useTranslations('auth');
const tErrors = useTranslations('errors');
const params = useSearchParams();
const router = useRouter();
const { login } = useAuth();
const token = useMemo(() => params.get('token') || '', [params]);
const [loading, setLoading] = useState(true);
@@ -76,18 +79,29 @@ function AcceptInviteContent() {
}
setSubmitting(true);
let accepted = false;
try {
await staffApi.acceptInvite({
token,
name: name.trim(),
password,
});
setSuccess(t('invitationAcceptedRedirect'));
setTimeout(() => {
router.replace('/login');
}, 1000);
accepted = true;
const email = inviteInfo?.email?.trim();
if (!email) {
throw new Error('missing-invite-email');
}
const { organizations } = await login(email, password);
navigateIntoAppIfOrgSelected((href) => router.replace(href), organizations.length);
} catch (e: unknown) {
setError(getUserFacingError(e, tErrors, t('errorAcceptInvitation')));
if (accepted) {
setSuccess(t('invitationAcceptedSignInFailed'));
setTimeout(() => {
router.replace('/login');
}, 1500);
} else {
setError(getUserFacingError(e, tErrors, t('errorAcceptInvitation')));
}
} finally {
setSubmitting(false);
}

View File

@@ -16,6 +16,8 @@ import { OrganizationDetailsFields } from '@/components/ui/auth/OrganizationDeta
import { RegistrationProgressSteps } from '@/components/ui/auth/RegistrationProgressSteps';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { organizationApi } from '@/lib/api/organization';
import { useAuth } from '@/lib/hooks/useAuth';
import { navigateIntoAppIfOrgSelected } from '@/lib/auth/postAuthRedirect';
type AcceptOrganizationInviteForm = {
ownerName: string;
@@ -33,6 +35,7 @@ function AcceptOrganizationInviteContent() {
const tValidation = useTranslations('validation');
const params = useSearchParams();
const router = useRouter();
const { login } = useAuth();
const token = useMemo(() => params.get('token') || '', [params]);
const acceptOrganizationInviteSchema = useMemo(
@@ -137,6 +140,7 @@ function AcceptOrganizationInviteContent() {
setError('');
setSuccess('');
setSubmitting(true);
let accepted = false;
try {
await organizationApi.acceptInvite({
token,
@@ -146,10 +150,20 @@ function AcceptOrganizationInviteContent() {
organizationEmail: data.organizationEmail.trim(),
organizationType: data.organizationType,
});
setSuccess(t('organizationAcceptedRedirect'));
setTimeout(() => router.replace('/login'), 1000);
accepted = true;
const email = inviteInfo?.ownerEmail?.trim();
if (!email) {
throw new Error('missing-invite-email');
}
const { organizations } = await login(email, data.password);
navigateIntoAppIfOrgSelected((href) => router.replace(href), organizations.length);
} catch (e: unknown) {
setError(getUserFacingError(e, tErrors, t('errorAcceptInvitation')));
if (accepted) {
setSuccess(t('invitationAcceptedSignInFailed'));
setTimeout(() => router.replace('/login'), 1500);
} else {
setError(getUserFacingError(e, tErrors, t('errorAcceptInvitation')));
}
} finally {
setSubmitting(false);
}

View File

@@ -2,7 +2,6 @@
import { Suspense, useState, useEffect, useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { useRouter } from '@/i18n/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
@@ -16,10 +15,8 @@ import { AuthPageShell } from '@/components/ui/auth/AuthPageShell';
import { Button } from '@/components/ui/shared/Button';
import { Checkbox } from '@/components/ui/shared/Checkbox';
import { Input } from '@/components/ui/shared/Input';
import {
consumeAuthRedirect,
storeAuthRedirectFromPath,
} from '@/lib/auth/postAuthRedirect';
import { storeAuthRedirectFromPath } from '@/lib/auth/postAuthRedirect';
import { useEnterAppWhenAuthenticated } from '@/lib/hooks/useEnterAppWhenAuthenticated';
type LoginForm = {
email: string;
@@ -32,8 +29,7 @@ function LoginPageContent() {
const tCommon = useTranslations('common');
const tValidation = useTranslations('validation');
const tErrors = useTranslations('errors');
const { login, isLoading, user, isAuthReady, organizations, currentOrganization } = useAuth();
const router = useRouter();
const { login, isLoading, isAuthReady } = useAuth();
const searchParams = useSearchParams();
const [error, setError] = useState<string | null>(null);
const [savedEmail] = useState(() => getRememberedEmail());
@@ -55,12 +51,7 @@ function LoginPageContent() {
}
}, [searchParams]);
useEffect(() => {
if (!isAuthReady || !user || isLoading) return;
const orgReady = organizations.length <= 1 || currentOrganization;
if (!orgReady) return;
router.push(consumeAuthRedirect() ?? '/today');
}, [isAuthReady, user, isLoading, organizations, currentOrganization, router]);
useEnterAppWhenAuthenticated();
const {
register,

View File

@@ -9,6 +9,7 @@ import { Link } from '@/i18n/navigation';
import { Mail, Lock, User, Phone } from 'lucide-react';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { useAuth } from '@/lib/hooks/useAuth';
import { useEnterAppWhenAuthenticated } from '@/lib/hooks/useEnterAppWhenAuthenticated';
import { AuthPageShell } from '@/components/ui/auth/AuthPageShell';
import { OrganizationDetailsFields } from '@/components/ui/auth/OrganizationDetailsFields';
import { RegistrationProgressSteps } from '@/components/ui/auth/RegistrationProgressSteps';
@@ -38,9 +39,10 @@ export default function RegisterPage() {
const tCommon = useTranslations('common');
const tValidation = useTranslations('validation');
const tErrors = useTranslations('errors');
const { registerTrial, isLoading } = useAuth();
const { registerTrial, isLoading, isAuthReady } = useAuth();
const [step, setStep] = useState(1);
const [error, setError] = useState<string | null>(null);
useEnterAppWhenAuthenticated();
const registerSchema = useMemo(
() =>
@@ -121,6 +123,14 @@ export default function RegisterPage() {
hide: t('hidePassword'),
};
if (!isAuthReady) {
return (
<div className="min-h-[100dvh] app-web-bg flex items-center justify-center px-4">
<p className="text-text-secondary">{tCommon('loading')}</p>
</div>
);
}
return (
<AuthPageShell
header={

View File

@@ -15,3 +15,20 @@ export function consumeAuthRedirect(): string | null {
if (path) sessionStorage.removeItem(AUTH_REDIRECT_KEY);
return path;
}
/** Dashboard (or a stored share-link / post-auth path). Consume once — never inside useAuth.login() or registerTrial. */
export function appPathAfterAuth(): string {
return consumeAuthRedirect() ?? '/today';
}
/**
* After login() on invite pages only. Multi-org already went to /select-organization.
* Do not use useEnterAppWhenAuthenticated on invite pages.
*/
export function navigateIntoAppIfOrgSelected(
replace: (href: string) => void,
organizationCount: number,
) {
if (organizationCount > 1) return;
replace(appPathAfterAuth());
}

View File

@@ -18,7 +18,7 @@ import {
} from '@/lib/auth/proactiveRefresh';
import { notifyAccessTokenRefreshed } from '@/lib/auth/accessTokenEvents';
import { asApiError, legacyStatusCode, type ApiError } from '@/types/api';
import { consumeAuthRedirect } from '@/lib/auth/postAuthRedirect';
import { appPathAfterAuth } from '@/lib/auth/postAuthRedirect';
function toApiError(err: unknown): ApiError {
return (
@@ -50,7 +50,11 @@ interface AuthContextType {
organizationEmail: string,
organizationType: 'CLINIC' | 'LAB'
) => Promise<void>;
login: (email: string, password: string, rememberMe?: boolean) => Promise<void>;
login: (
email: string,
password: string,
rememberMe?: boolean,
) => Promise<{ organizations: Organization[] }>;
logout: () => Promise<void>;
selectOrganization: (orgId: string) => Promise<void>;
createOrganization: (
@@ -337,6 +341,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
router.push('/select-organization');
}
return { organizations: orgs };
} catch (err: any) {
setApiError(toApiError(err));
throw err;
@@ -388,12 +393,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
plan: (organization as { plan?: Organization['plan'] }).plan,
});
const redirectPath = consumeAuthRedirect();
if (redirectPath) {
router.push(redirectPath);
} else {
router.push('/today');
}
router.push(appPathAfterAuth());
} catch (err: any) {
setApiError(toApiError(err));

View File

@@ -0,0 +1,23 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from '@/i18n/navigation';
import { appPathAfterAuth } from '@/lib/auth/postAuthRedirect';
import { useAuth } from '@/lib/hooks/useAuth';
/**
* When a session already exists (or was just created on this page), enter the app.
* Use on /login and /register only — not on invite pages, where a logged-in visitor
* must still be able to finish accept before navigating.
*/
export function useEnterAppWhenAuthenticated() {
const { user, isAuthReady, isLoading, organizations, currentOrganization } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isAuthReady || !user || isLoading) return;
const orgReady = organizations.length <= 1 || currentOrganization;
if (!orgReady) return;
router.push(appPathAfterAuth());
}, [isAuthReady, user, isLoading, organizations, currentOrganization, router]);
}