Skip to content
Closed
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
43 changes: 42 additions & 1 deletion packages/shared/src/components/auth/AuthOptionsInner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import {
AuthTriggers,
getNodeValue,
} from '../../lib/auth';
import {
getBetterAuthSocialUrl,
betterAuthVerifySignupEmail,
betterAuthSendSignupVerification,
} 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 Down Expand Up @@ -354,6 +361,23 @@ function AuthOptionsInner({
target_id: provider,
extra: JSON.stringify({ trigger }),
});

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

// Only web auth requires a popup
if (!isNativeAuthSupported(provider)) {
windowPopup.current = window.open();
Expand Down Expand Up @@ -675,6 +699,23 @@ function AuthOptionsInner({
<EmailCodeVerification
flowId={verificationFlowId}
onSubmit={onProfileSuccess}
onVerifyCode={
isBetterAuth
? async (code) => {
const res = await betterAuthVerifySignupEmail(code);
if (res.error) {
throw new Error(res.error);
}
}
: undefined
}
onResendCode={
isBetterAuth
? async () => {
await betterAuthSendSignupVerification();
}
: undefined
}
/>
</Tab>
</TabContainer>
Expand Down
76 changes: 62 additions & 14 deletions packages/shared/src/components/auth/EmailCodeVerification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,40 @@ import {
} from '../typography/Typography';
import { CodeField } from '../fields/CodeField';
import { useAuthData } from '../../contexts/AuthDataContext';
import useTimer from '../../hooks/useTimer';

interface EmailCodeVerificationProps extends AuthFormProps {
code?: string;
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 [isVerifyingCustom, setIsVerifyingCustom] = useState(false);
const {
timer: customResendTimer,
setTimer: setCustomResendTimer,
runTimer: runCustomTimer,
} = useTimer(() => {}, 60);
const { sendEmail, verifyCode, resendTimer, autoResend, isVerifyingCode } =
useAccountEmailFlow({
flow: AuthFlow.Verification,
flowId,
flowId: onVerifyCode ? 'skip' : flowId,
timerOnLoad: 60,
onError: setHint,
onVerifyCodeSuccess: () => {
Expand All @@ -50,11 +61,34 @@ function EmailCodeVerification({
},
});

const activeResendTimer = onVerifyCode ? customResendTimer : resendTimer;
const activeIsVerifying = onVerifyCode ? isVerifyingCustom : isVerifyingCode;

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

const handleCustomVerify = async (verifyCodeValue: string) => {
setIsVerifyingCustom(true);
try {
await onVerifyCode(verifyCodeValue);
logEvent({
event_name: AuthEventNames.VerifiedSuccessfully,
});
onSubmit();
} catch (err) {
setHint(err instanceof Error ? err.message : 'Verification failed');
} finally {
setIsVerifyingCustom(false);
}
};

const onCodeVerification = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand All @@ -64,22 +98,36 @@ function EmailCodeVerification({
});
setHint('');
setAlert({ firstAlert: false, alert: false });
await verifyCode({ code });
if (onVerifyCode) {
await handleCustomVerify(code);
} else {
await verifyCode({ 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();
setCustomResendTimer(60);
runCustomTimer();
} else {
sendEmail(email);
}
};

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

Expand Down Expand Up @@ -120,7 +168,7 @@ function EmailCodeVerification({
<CodeField
onSubmit={onCodeSubmit}
onChange={onCodeChange}
disabled={isVerifyingCode}
disabled={activeIsVerifying}
/>
{hint && (
<Typography
Expand All @@ -135,27 +183,27 @@ function EmailCodeVerification({
Didn&#39;t get a verification code?{' '}
<button
type="button"
disabled={resendTimer > 0}
disabled={activeResendTimer > 0}
onClick={onSendCode}
className={
resendTimer === 0 ? 'text-text-link' : 'text-text-disabled'
activeResendTimer === 0 ? 'text-text-link' : 'text-text-disabled'
}
>
Resend code
{resendTimer > 0 && ` ${resendTimer}s`}
{activeResendTimer > 0 && ` ${activeResendTimer}s`}
</button>
</span>
</div>
<Button
className="w-full"
type="submit"
variant={ButtonVariant.Primary}
loading={isVerifyingCode}
disabled={autoResend}
loading={activeIsVerifying}
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
84 changes: 84 additions & 0 deletions packages/shared/src/components/auth/RegistrationForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SettingsContext from '../../contexts/SettingsContext';
import { mockGraphQL } from '../../../__tests__/helpers/graphql';
import { GET_USERNAME_SUGGESTION } from '../../graphql/users';
import { AuthTriggers } from '../../lib/auth';
import * as betterAuthHook from '../../hooks/useIsBetterAuth';
import type { AuthOptionsProps } from './common';

const user = null;
Expand All @@ -24,6 +25,7 @@ beforeEach(() => {
jest.clearAllMocks();
nock.cleanAll();
jest.clearAllMocks();
jest.spyOn(betterAuthHook, 'useIsBetterAuth').mockReturnValue(false);
});

const onSuccessfulLogin = jest.fn();
Expand Down Expand Up @@ -129,6 +131,55 @@ const renderLogin = async (email: string) => {
});
};

const renderBetterAuthRegistration = async (
email = 'sshanzel@yahoo.com',
name = 'Lee Solevilla',
username = 'leesolevilla',
) => {
jest.spyOn(betterAuthHook, 'useIsBetterAuth').mockReturnValue(true);
renderComponent();
await waitForNock();

nock(process.env.NEXT_PUBLIC_API_URL as string)
.get('/a/auth/check-email')
.query({ email })
.reply(200, { result: false });

fireEvent.input(screen.getByPlaceholderText('Email'), {
target: { value: email },
});
fireEvent.click(await screen.findByTestId('email_signup_submit'));
await waitForNock();

let queryCalled = false;
mockGraphQL({
request: {
query: GET_USERNAME_SUGGESTION,
variables: { name },
},
result: () => {
queryCalled = true;
return { data: { generateUniqueUsername: username } };
},
});

await screen.findByTestId('registration_form');
const nameInput = screen.getByPlaceholderText('Name');
fireEvent.input(screen.getByPlaceholderText('Enter a username'), {
target: { value: username },
});
fireEvent.input(screen.getByPlaceholderText('Name'), {
target: { value: name },
});
simulateTextboxInput(nameInput as HTMLTextAreaElement, name);
fireEvent.input(screen.getByPlaceholderText('Create a password'), {
target: { value: '#123xAbc' },
});

await waitForNock();
await waitFor(() => expect(queryCalled).toBeTruthy());
};

// NOTE: Chris turned this off needs a good re-look at
// it('should post registration', async () => {
// const email = 'sshanzel@yahoo.com';
Expand Down Expand Up @@ -171,6 +222,39 @@ it('should show login if email exists', async () => {
expect(text).toBeInTheDocument();
});

it('should show a generic sign up error when Better Auth sign up is privacy-protected', async () => {
const email = 'sshanzel@yahoo.com';
await renderBetterAuthRegistration(email);

nock(process.env.NEXT_PUBLIC_API_URL as string)
.post('/a/auth/sign-up/email', {
name: 'Lee Solevilla',
email,
password: '#123xAbc',
})
.reply(200, { status: true });
nock(process.env.NEXT_PUBLIC_API_URL as string)
.post('/a/auth/sign-in/email', {
email,
password: '#123xAbc',
})
.reply(401, {
code: 'INVALID_CREDENTIALS',
message: 'Invalid credentials',
});

fireEvent.submit(await screen.findByTestId('registration_form'));
await waitForNock();

await waitFor(() => {
expect(
screen.getByText(
"We couldn't complete sign up. If you already have an account, try signing in instead.",
),
).toBeInTheDocument();
});
});

describe('testing username auto generation', () => {
it('should suggest a valid option', async () => {
const email = 'sshanzel@yahoo.com';
Expand Down
Loading