Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(clerk-react): Fix forceRedirectUrl and fallbackRedirectUrl when passed to button components #3508

Merged
merged 8 commits into from
Jun 4, 2024
5 changes: 5 additions & 0 deletions .changeset/metal-foxes-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-react': patch
---

Update `SignUpButton` and `SignInButton` to respect `forceRedirect` and `fallbackRedirect` props. Previously, these were getting ignored and successful completions of the flows would fallback to the default redirect URL.
5 changes: 5 additions & 0 deletions .changeset/sixty-ears-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fixed a bug where Clerk components rendered in modals were wrapped with `aria-hidden`.
35 changes: 35 additions & 0 deletions integration/templates/next-app-router/src/app/buttons/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SignInButton, SignUpButton } from '@clerk/nextjs';

export default function Home() {
return (
<main>
<SignInButton
mode='modal'
forceRedirectUrl='/protected'
>
Sign in button (force)
</SignInButton>

<SignInButton
mode='modal'
fallbackRedirectUrl='/protected'
>
Sign in button (fallback)
</SignInButton>

<SignUpButton
mode='modal'
forceRedirectUrl='/protected'
>
Sign up button (force)
</SignUpButton>

<SignUpButton
mode='modal'
fallbackRedirectUrl='/protected'
>
Sign up button (fallback)
</SignUpButton>
</main>
);
}
130 changes: 130 additions & 0 deletions integration/tests/redirects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { test } from '@playwright/test';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('redirect props @nextjs', ({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser({
fictionalEmail: true,
withPhoneNumber: true,
withUsername: true,
});
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test.afterEach(async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.signOut();
await u.page.context().clearCookies();
});

test.describe('SignInButton', () => {
test('sign in button respects forceRedirectUrl', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToRelative('/buttons');
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.page.getByText('Sign in button (force)').click();

await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });

await u.page.waitForAppUrl('/protected');

await u.po.expect.toBeSignedIn();
});

test('sign in button respects fallbackRedirectUrl', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToRelative('/buttons');
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.page.getByText('Sign in button (fallback)').click();

await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });

await u.page.waitForAppUrl('/protected');

await u.po.expect.toBeSignedIn();
});
});

test.describe('SignUpButton', () => {
test('sign up button respects forceRedirectUrl', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
const fakeUser = u.services.users.createFakeUser({
fictionalEmail: true,
withPhoneNumber: true,
withUsername: true,
});

await u.page.goToRelative('/buttons');
await u.page.waitForClerkJsLoaded();

await u.page.getByText('Sign up button (force)').click();

// Fill in sign up form
await u.po.signUp.signUpWithEmailAndPassword({
email: fakeUser.email,
password: fakeUser.password,
});

// Verify email
await u.po.signUp.enterTestOtpCode();

await u.page.waitForAppUrl('/protected');

// Check if user is signed in
await u.po.expect.toBeSignedIn();

await fakeUser.deleteIfExists();
});

test('sign up button respects fallbackRedirectUrl', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
const fakeUser = u.services.users.createFakeUser({
fictionalEmail: true,
withPhoneNumber: true,
withUsername: true,
});

await u.page.goToRelative('/buttons');
await u.page.waitForClerkJsLoaded();

await u.page.getByText('Sign up button (fallback)').click();

// Fill in sign up form
await u.po.signUp.signUpWithEmailAndPassword({
email: fakeUser.email,
password: fakeUser.password,
});

// Verify email
await u.po.signUp.enterTestOtpCode();

await u.page.waitForAppUrl('/protected');

// Check if user is signed in
await u.po.expect.toBeSignedIn();

await fakeUser.deleteIfExists();
});
});
});
2 changes: 0 additions & 2 deletions packages/clerk-js/src/ui/elements/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,13 @@ export const Modal = withFloatingTree((props: ModalProps) => {
>
<ModalContext.Provider value={modalCtx}>
<Flex
aria-hidden
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want this, as it removes the entire modal from the accessibility tree.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely not 😅

ref={overlayRef}
elementDescriptor={descriptors.modalBackdrop}
sx={[
t => ({
animation: `${animations.fadeIn} 150ms ${t.transitionTiming.$common}`,
zIndex: t.zIndices.$modal,
backgroundColor: t.colors.$modalBackdrop,
// ...common.centeredFlex(),
alignItems: 'flex-start',
justifyContent: 'center',
overflow: 'auto',
Expand Down
17 changes: 12 additions & 5 deletions packages/react/src/components/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SignInProps } from '@clerk/types';
import React from 'react';

import type { SignInButtonProps, WithClerkProp } from '../types';
Expand All @@ -11,21 +12,27 @@ export const SignInButton = withClerk(({ clerk, children, ...props }: WithClerkP
const child = assertSingleChild(children)('SignInButton');

const clickHandler = () => {
const opts = {
const opts: SignInProps = {
forceRedirectUrl,
fallbackRedirectUrl,
Comment on lines +15 to +17
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal mode relies on the non-prefixed props, adding the type here revealed the issue.

signUpFallbackRedirectUrl,
signUpForceRedirectUrl,
signInForceRedirectUrl: forceRedirectUrl,
signInFallbackRedirectUrl: fallbackRedirectUrl,
};

if (mode === 'modal') {
return clerk.openSignIn(opts);
}
return clerk.redirectToSignIn(opts);
return clerk.redirectToSignIn({
...opts,
signInFallbackRedirectUrl: fallbackRedirectUrl,
signInForceRedirectUrl: forceRedirectUrl,
Comment on lines +27 to +28
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when calling the redirect methods, the prefixed props are necessary. Added here.

});
};

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
if (child && typeof child === 'object' && 'props' in child) {
await safeExecute(child.props.onClick)(e);
}
Comment on lines +33 to +35
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some auto-fix of a lint rule caused a type error, so I tweaked the logic a bit here as a result.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting.

return clickHandler();
};

Expand Down
17 changes: 12 additions & 5 deletions packages/react/src/components/SignUpButton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SignUpProps } from '@clerk/types';
import React from 'react';

import type { SignUpButtonProps, WithClerkProp } from '../types';
Expand All @@ -19,9 +20,9 @@ export const SignUpButton = withClerk(({ clerk, children, ...props }: WithClerkP
const child = assertSingleChild(children)('SignUpButton');

const clickHandler = () => {
const opts = {
signUpFallbackRedirectUrl: fallbackRedirectUrl,
signUpForceRedirectUrl: forceRedirectUrl,
const opts: SignUpProps = {
fallbackRedirectUrl,
forceRedirectUrl,
signInFallbackRedirectUrl,
signInForceRedirectUrl,
unsafeMetadata,
Expand All @@ -31,11 +32,17 @@ export const SignUpButton = withClerk(({ clerk, children, ...props }: WithClerkP
return clerk.openSignUp(opts);
}

return clerk.redirectToSignUp(opts);
return clerk.redirectToSignUp({
...opts,
signUpFallbackRedirectUrl: fallbackRedirectUrl,
signUpForceRedirectUrl: forceRedirectUrl,
});
};

const wrappedChildClickHandler: React.MouseEventHandler = async e => {
await safeExecute((child as any).props.onClick)(e);
if (child && typeof child === 'object' && 'props' in child) {
await safeExecute(child.props.onClick)(e);
}
return clickHandler();
};

Expand Down
Loading