Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/shared/src/components/auth/AuthDefault.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React, { useEffect, useRef, useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { checkKratosEmail } from '../../lib/kratos';
import { checkBetterAuthEmail } from '../../lib/betterAuth';
import { useIsBetterAuth } from '../../hooks/useIsBetterAuth';
import type { AuthFormProps, Provider } from './common';
import { getFormEmail } from './common';
import EmailSignupForm from './EmailSignupForm';
Expand Down Expand Up @@ -62,13 +64,17 @@ const AuthDefault = ({
simplified,
}: AuthDefaultProps): ReactElement => {
const { logEvent } = useLogContext();
const isBetterAuth = useIsBetterAuth();
const [shouldLogin, setShouldLogin] = useState(isLoginFlow);
const title = shouldLogin ? logInTitle : signUpTitle;
const { displayToast } = useToastNotification();
const [registerEmail, setRegisterEmail] = useState<string>(null);
const socialLoginListRef = useRef<HTMLDivElement>(null);
const { mutateAsync: checkEmail } = useMutation({
mutationFn: (emailParam: string) => checkKratosEmail(emailParam),
mutationFn: (emailParam: string) =>
isBetterAuth
? checkBetterAuthEmail(emailParam)
: checkKratosEmail(emailParam),
});

const focusFirstSocialLink = () => {
Expand Down
58 changes: 52 additions & 6 deletions packages/shared/src/components/auth/AuthOptionsInner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import classNames from 'classnames';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
Expand All @@ -13,6 +13,13 @@ import {
AuthTriggers,
getNodeValue,
} from '../../lib/auth';
import {
getBetterAuthSocialUrl,
betterAuthSendVerificationOTP,
betterAuthVerifyEmailOTP,
} from '../../lib/betterAuth';
import { useIsBetterAuth } from '../../hooks/useIsBetterAuth';
import { webappUrl, broadcastChannel, isTesting } from '../../lib/constants';
import { generateNameFromEmail } from '../../lib/strings';
import { generateUsername, claimClaimableItem } from '../../graphql/users';
import useRegistration from '../../hooks/useRegistration';
Expand All @@ -36,7 +43,6 @@ import {
useEventListener,
usePersistentState,
} from '../../hooks';
import { broadcastChannel, isTesting } from '../../lib/constants';
import type { SignBackProvider } from '../../hooks/auth/useSignBack';
import { SIGNIN_METHOD_KEY, useSignBack } from '../../hooks/auth/useSignBack';
import type { LoggedUser } from '../../lib/user';
Expand Down Expand Up @@ -129,6 +135,7 @@ function AuthOptionsInner({
const { syncSettings } = useSettingsContext();
const { trackSignup } = usePixelsContext();
const { logEvent } = useLogContext();
const isBetterAuth = useIsBetterAuth();
const [isConnected, setIsConnected] = useState(false);
const [registrationHints, setRegistrationHints] = useState<RegistrationError>(
{},
Expand All @@ -144,7 +151,7 @@ function AuthOptionsInner({
: defaultDisplay,
);

const { setEmail } = useAuthData();
const { email, setEmail } = useAuthData();

const onSetActiveDisplay = (display: AuthDisplay) => {
onDisplayChange?.(display);
Expand Down Expand Up @@ -255,12 +262,13 @@ function AuthOptionsInner({
} = useProfileForm({ onSuccess: onProfileSuccess });

const autoCompleteProfileForRecruiter = async (
email: string,
recruiterEmail: string,
name?: string,
) => {
try {
// Generate name from email if not provided by OAuth
const displayName = name || generateNameFromEmail(email, 'Recruiter');
const displayName =
name || generateNameFromEmail(recruiterEmail, 'Recruiter');

// Generate username from the display name
const username = await generateUsername(displayName);
Expand Down Expand Up @@ -354,6 +362,25 @@ function AuthOptionsInner({
target_id: provider,
extra: JSON.stringify({ trigger }),
});

if (isBetterAuth) {
const callbackURL = login
? `${webappUrl}callback?login=true`
: `${webappUrl}callback`;
const socialUrl = getBetterAuthSocialUrl(
provider.toLowerCase(),
callbackURL,
);
if (!isNativeAuthSupported(provider)) {
windowPopup.current = window.open(socialUrl);
} else {
windowPopup.current.location.href = socialUrl;
}
await setChosenProvider(provider);
onAuthStateUpdate?.({ isLoading: true });
return;
}

// Only web auth requires a popup
if (!isNativeAuthSupported(provider)) {
windowPopup.current = window.open();
Expand Down Expand Up @@ -481,7 +508,9 @@ function AuthOptionsInner({
...params,
method: 'password',
});
await onProfileSuccess({ setSignBack: false });
if (!isBetterAuth) {
await onProfileSuccess({ setSignBack: false });
}
};

const onForgotPassword = (withEmail?: string) => {
Expand Down Expand Up @@ -675,6 +704,23 @@ function AuthOptionsInner({
<EmailCodeVerification
flowId={verificationFlowId}
onSubmit={onProfileSuccess}
onVerifyCode={
isBetterAuth
? async (code) => {
const res = await betterAuthVerifyEmailOTP(email, code);
if (res.error) {
throw new Error(res.error);
}
}
: undefined
}
onResendCode={
isBetterAuth
? async () => {
await betterAuthSendVerificationOTP(email);
}
: undefined
}
/>
</Tab>
</TabContainer>
Expand Down
95 changes: 72 additions & 23 deletions packages/shared/src/components/auth/EmailCodeVerification.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { Button, ButtonVariant } from '../buttons/Button';
import type { AuthFormProps } from './common';
Expand All @@ -23,38 +23,82 @@ interface EmailCodeVerificationProps extends AuthFormProps {
flowId: string;
onSubmit?: () => void;
className?: string;
onVerifyCode?: (code: string) => Promise<void>;
onResendCode?: () => Promise<void>;
}

function EmailCodeVerification({
code: codeProp,
flowId,
onSubmit,
className,
onVerifyCode,
onResendCode,
}: EmailCodeVerificationProps): ReactElement {
const { email } = useAuthData();
const { logEvent } = useLogContext();
const [hint, setHint] = useState('');
const [alert, setAlert] = useState({ firstAlert: true, alert: false });
const [code, setCode] = useState(codeProp);
const { sendEmail, verifyCode, resendTimer, autoResend, isVerifyingCode } =
useAccountEmailFlow({
flow: AuthFlow.Verification,
flowId,
timerOnLoad: 60,
onError: setHint,
onVerifyCodeSuccess: () => {
const [isCustomVerifying, setIsCustomVerifying] = useState(false);
const verifyingRef = useRef(false);

const {
sendEmail,
verifyCode,
resendTimer,
resetResendTimer,
autoResend,
isVerifyingCode,
} = useAccountEmailFlow({
flow: AuthFlow.Verification,
flowId: onVerifyCode ? 'skip' : flowId,
timerOnLoad: 60,
onError: setHint,
onVerifyCodeSuccess: () => {
logEvent({
event_name: AuthEventNames.VerifiedSuccessfully,
});
onSubmit();
},
});

const isVerifying = onVerifyCode ? isCustomVerifying : isVerifyingCode;

useEffect(() => {
if (
!onVerifyCode &&
autoResend &&
!alert.alert &&
alert.firstAlert === true
) {
setAlert({ firstAlert: false, alert: true });
}
}, [autoResend, alert, onVerifyCode]);

const handleVerify = async (verifyCodeValue: string) => {
if (onVerifyCode) {
if (verifyingRef.current) {
return;
}
verifyingRef.current = true;
setIsCustomVerifying(true);
try {
await onVerifyCode(verifyCodeValue);
logEvent({
event_name: AuthEventNames.VerifiedSuccessfully,
});
onSubmit();
},
});

useEffect(() => {
if (autoResend && !alert.alert && alert.firstAlert === true) {
setAlert({ firstAlert: false, alert: true });
} catch (err) {
verifyingRef.current = false;
setHint(err instanceof Error ? err.message : 'Verification failed');
} finally {
setIsCustomVerifying(false);
}
} else {
await verifyCode({ code: verifyCodeValue });
}
}, [autoResend, alert]);
};

const onCodeVerification = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand All @@ -64,22 +108,27 @@ function EmailCodeVerification({
});
setHint('');
setAlert({ firstAlert: false, alert: false });
await verifyCode({ code });
await handleVerify(code);
};

const onSendCode = () => {
const onSendCode = async () => {
logEvent({
event_name: LogEvent.Click,
target_type: TargetType.ResendVerificationCode,
});
setAlert({ firstAlert: false, alert: false });
sendEmail(email);
if (onResendCode) {
await onResendCode();
resetResendTimer();
} else {
sendEmail(email);
}
};

const onCodeSubmit = async (newCode: string) => {
if (newCode.length === 6) {
setCode(newCode);
await verifyCode({ code: newCode });
await handleVerify(newCode);
}
};

Expand Down Expand Up @@ -120,7 +169,7 @@ function EmailCodeVerification({
<CodeField
onSubmit={onCodeSubmit}
onChange={onCodeChange}
disabled={isVerifyingCode}
disabled={isVerifying}
/>
{hint && (
<Typography
Expand Down Expand Up @@ -150,12 +199,12 @@ function EmailCodeVerification({
className="w-full"
type="submit"
variant={ButtonVariant.Primary}
loading={isVerifyingCode}
disabled={autoResend}
loading={isVerifying}
disabled={onVerifyCode ? false : autoResend}
>
Verify
</Button>
{alert.alert && (
{!onVerifyCode && alert.alert && (
<Alert className="mt-6" type={AlertType.Error} flexDirection="flex-row">
<AlertParagraph className="!mt-0 flex-1">
Your session expired, please click the resend button above to get a
Expand Down
Loading
Loading