Skip to content

Commit

Permalink
fix sign in issues, fix quick add render loop
Browse files Browse the repository at this point in the history
  • Loading branch information
mshick committed Mar 20, 2024
1 parent 553d631 commit c60b9df
Show file tree
Hide file tree
Showing 17 changed files with 158 additions and 42 deletions.
10 changes: 5 additions & 5 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,12 @@ const nextConfig = {
source: '/api/auth/account/signin',
destination: '/account/signin',
permanent: false
},
{
source: '/api/auth/account/signout',
destination: '/account/signout',
permanent: false
}
// {
// source: '/api/auth/account/signout',
// destination: '/account/signout',
// permanent: false
// }
];
}
};
Expand Down
32 changes: 31 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions playwright/tests/product-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,13 @@ test.describe('Write a product review', () => {
await collectionsPage.selectProduct(PRODUCT_NAME_INSTOCK);
});

// TODO Reviews.io acct is currently inactive
test.fixme('Verify user cannot submit an empty review form', async ({ productPage, page }) => {
test('Verify user cannot submit an empty review form', async ({ productPage, page }) => {
await productPage.clickOnWriteAReviewBtn();
await productPage.submitAReviewBtn().click();
await expect(page.getByText('This field is required')).toHaveCount(2);
});

// TODO Reviews.io acct is currently inactive
// BUG https://app.shortcut.com/takeshape/story/12703/product-review-doesn-t-appear-after-submitting-a-review-form
test.fixme('Submit a review form', async ({ productPage }) => {
test('Submit a review form', async ({ productPage }) => {
const message = getTextMessage();

await productPage.clickOnWriteAReviewBtn();
Expand Down
5 changes: 5 additions & 0 deletions src/app/(shop)/collections/[...collection]/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PropsWithChildren } from 'react';

export default function Template({ children }: PropsWithChildren) {
return <div>{children}</div>;
}
5 changes: 5 additions & 0 deletions src/app/(shop)/products/[...product]/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PropsWithChildren } from 'react';

export default function Template({ children }: PropsWithChildren) {
return <div>{children}</div>;
}
2 changes: 1 addition & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { AuthCustomerQuery } from '@/features/Auth/queries.storefront';
import { getStorefrontClient } from '@/lib/apollo/rsc';
import { NoAccessTokenError, NoEmailError } from '@/lib/auth/errors';
import { getMultipassCustomerAccessToken } from '@/lib/auth/multipass-helper';
import { getMultipassCustomerAccessToken } from '@/lib/auth/multipass';
import ShopifyCredentialsProvider from '@/lib/auth/shopify-credentials-provider';
import logger from '@/logger';
import { AuthCustomerQueryResponse, AuthCustomerQueryVariables } from '@/types/storefront';
Expand Down
2 changes: 2 additions & 0 deletions src/features/AccountNavigation/AccountNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ function useAccountNavigationItems() {

function useLogout() {
const client = useApolloClient();

const handleLogout = useCallback(async () => {
await client.resetStore();
void signOut({ callbackUrl: '/' });
}, [client]);

return {
handleLogout
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const AccountInactiveForm = ({ customer, isOpen, onClose }: AccountInacti

const handleFormSubmit: SubmitHandler<AccountInactiveFormValues> = useCallback(async () => {
await sendInvite({ variables: { customerId: customer.id } });
}, [customer.id, sendInvite]);
}, [customer, sendInvite]);

return (
<ModalForm
Expand Down
2 changes: 1 addition & 1 deletion src/features/Auth/AuthSignIn/AuthSignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const AuthSignIn = ({ callbackUrl, error, useMultipass, email }: AuthSign
}, [callbackUrl]);

const inactiveCustomer = useMemo(() => {
if (error?.code === 'email-in-use') {
if (error?.code === 'disabled') {
return {
email: error.email,
id: error.customerId
Expand Down
32 changes: 18 additions & 14 deletions src/features/QuickAdd/QuickAdd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { Modal } from '@/components/Modal/Modal';
import { getProduct } from '@/features/QuickAdd/transforms';
import { QuickAddQueryResponse, QuickAddQueryVariables } from '@/types/takeshape';
import { QueryReference, useLoadableQuery, useReadQuery } from '@apollo/client';
import { useAtomValue } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { Suspense, useEffect } from 'react';
import { useAtom } from 'jotai';
import { Suspense, useCallback } from 'react';
import { QuickAddItem } from './components/QuickAddItem';
import { QuickAddItemLoading } from './components/QuickAddItemLoading';
import { QuickAddQuery } from './queries';
import { quickAddAtom } from './store';
import { quickAddAtom, useQuickAddAtomListener } from './store';

type ReadQuickAddProps = {
onClose: () => void;
Expand All @@ -29,18 +28,23 @@ function ReadQuickAdd({ onClose, productQueryRef }: ReadQuickAddProps) {
}

export const QuickAdd = () => {
const quickAdd = useAtomValue(quickAddAtom);
const resetQuickAdd = useResetAtom(quickAddAtom);

const [quickAdd, setQuickAdd] = useAtom(quickAddAtom);
const [loadProduct, productQueryRef] = useLoadableQuery<QuickAddQueryResponse, QuickAddQueryVariables>(QuickAddQuery);

useEffect(() => {
if (quickAdd?.productHandle) {
void loadProduct({
handle: quickAdd.productHandle
});
}
}, [loadProduct, quickAdd]);
const resetQuickAdd = useCallback(() => setQuickAdd(null), [setQuickAdd]);

useQuickAddAtomListener(
useCallback(
(get, set, val) => {
if (val?.productHandle) {
void loadProduct({
handle: val.productHandle
});
}
},
[loadProduct]
)
);

return (
<Modal isOpen={Boolean(quickAdd)} onClose={() => resetQuickAdd()} showCloseButton={true}>
Expand Down
4 changes: 2 additions & 2 deletions src/features/QuickAdd/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { atomWithReset } from 'jotai/utils';
import { atomWithListeners } from '@/lib/jotai/atomWithListeners';
import { QuickAdd } from './types';

export const quickAddAtom = atomWithReset<QuickAdd | null>(null);
export const [quickAddAtom, useQuickAddAtomListener] = atomWithListeners<QuickAdd | null>(null);
2 changes: 1 addition & 1 deletion src/lib/apollo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function createApolloClientLinks({
if (networkError) {
// When unauthenticated, redirect to sign in
if ((networkError as ServerError).statusCode === 401 && !isSsr) {
window.location.href = '/account/signin?error=AccessDenied';
window.location.href = '/account/signin?error=SessionRequired';
}

logger.error({
Expand Down
48 changes: 40 additions & 8 deletions src/lib/auth/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export function decodeErrorCode(code: string | undefined) {
}

export type ErrorType =
// Default next-auth signin page errors
| 'Signin'
| 'OAuthSignin'
| 'OAuthCallbackError'
| 'OAuthCreateAccount'
| 'EmailCreateAccount'
| 'Callback'
| 'EmailSignin'
| 'SessionRequired'
// AuthError types
| 'AccessDenied'
| 'CredentialsSignin'
Expand All @@ -48,6 +57,10 @@ export class MissingCredentialsError extends CredentialsSignin {

export class EmailInUseError extends CredentialsSignin {
code = 'email-in-use';
}

export class AccountDisabledError extends CredentialsSignin {
code = 'disabled';
data: Record<string, string>;

constructor(message: string, { cause, data }: ErrorOptions & { data: Record<string, string> }) {
Expand Down Expand Up @@ -101,29 +114,48 @@ export function parseSigninError(searchParams: ServerProps['searchParams']): Sig
};
}

const defaultSigninErrorMessage = 'Unable to sign in.';
const defaultSigninErrorMessage = 'Try signing in with a different account.';
const defaultCredentialsErrorMessage = 'Email address or password are incorrect.';

export const errors: Record<ErrorType, string> = {
/**
* From the default Signin page
* https://github.com/nextauthjs/next-auth/blob/5ea8b7b0f4d285e48f141dd91e518c905c9fb34e/packages/core/src/lib/pages/signin.tsx#L8C7-L22
*/
Signin: defaultSigninErrorMessage,
OAuthSignin: defaultSigninErrorMessage,
OAuthCallbackError: defaultSigninErrorMessage,
OAuthCreateAccount: defaultSigninErrorMessage,
EmailCreateAccount: defaultSigninErrorMessage,
Callback: defaultSigninErrorMessage,
EmailSignin: 'The e-mail could not be sent.',
SessionRequired: 'Please sign in to access this page.',

/**
* Other errors from @auth/core it makes sense to handle (maybe they are in transition?)
*/
AccessDenied: 'Please sign in to access this page.',
OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.',
AccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.',
OAuthSignInError: 'Try signing in with a different account.',
EmailSignInError: 'Try signing in with a different account.',
OAuthSignInError: defaultSigninErrorMessage,
EmailSignInError: defaultSigninErrorMessage,
SignOutError: 'Could not sign out. If this problem persists contact support.',
SessionTokenError: 'Session error. Please sign in again.',
SessionTokenError: 'Session error. Please sign in again to access this page.',

/**
* Custom errors
*/
CheckoutSessionRequired: 'Please sign in to checkout.',
CredentialsSignin: 'Email address or password are incorrect.',
CredentialsSignin: defaultCredentialsErrorMessage,
CannotCreate: 'Email address already in use. Sign in instead.'
};

const credentialSignInErrorCodes: Record<string, string> = {
credentials: 'Email address or password are incorrect.',
credentials: defaultCredentialsErrorMessage,
'missing-credentials': 'Email address or password not provided.',
'no-account': 'Email address or password are incorrect.',
'email-in-use': 'Email address already in use. Sign in instead.',
'no-account': defaultCredentialsErrorMessage,
'email-in-use': 'Email address in use, try another sign in method.',
disabled: 'This account needs to be activated.',
// Not actually CredentialsSignin errors, but that's the only way to throw custom data
'no-email': 'No email address found on the linked account.',
'no-access-token': 'Unable to get an access token from Shopify.',
Expand Down
File renamed without changes.
10 changes: 8 additions & 2 deletions src/lib/auth/shopify-credentials-provider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GetCustomerStateQuery } from '@/features/Auth/queries';
import { AuthCustomerAccessTokenCreateMutation } from '@/features/Auth/queries.storefront';
import { getAnonymousTakeshapeClient, getStorefrontClient } from '@/lib/apollo/rsc';
import { EmailInUseError, MissingCredentialsError, NoAccountError } from '@/lib/auth/errors';
import { AccountDisabledError, EmailInUseError, MissingCredentialsError, NoAccountError } from '@/lib/auth/errors';
import logger from '@/logger';
import {
AuthCustomerAccessTokenCreateMutationResponse,
Expand Down Expand Up @@ -70,7 +70,13 @@ export default function ShopifyCredentialsProvider() {
throw new NoAccountError('No account found matching email');
}

throw new EmailInUseError('This account is already in use', { data: { email, customerId: customer.id } });
if (customer?.state === 'enabled') {
throw new EmailInUseError('This account is already enabled');
}

throw new AccountDisabledError('This account is disabled', {
data: { email, customerId: customer.id }
});
}

throw new CredentialsSignin('Email address or password are incorrect.');
Expand Down
35 changes: 35 additions & 0 deletions src/lib/jotai/atomWithListeners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Getter, SetStateAction, Setter, atom, useSetAtom } from 'jotai';
import { useEffect } from 'react';

type Callback<Value> = (get: Getter, set: Setter, newVal: Value, prevVal: Value) => void;

export function atomWithListeners<Value>(initialValue: Value) {
const baseAtom = atom(initialValue);
const listenersAtom = atom<Callback<Value>[]>([]);

const anAtom = atom(
(get) => get(baseAtom),
(get, set, arg: SetStateAction<Value>) => {
const prevVal = get(baseAtom);
set(baseAtom, arg);
const newVal = get(baseAtom);
get(listenersAtom).forEach((callback) => {
callback(get, set, newVal, prevVal);
});
}
);

const useListener = (callback: Callback<Value>) => {
const setListeners = useSetAtom(listenersAtom);
useEffect(() => {
setListeners((prev) => [...prev, callback]);
return () =>
setListeners((prev) => {
const index = prev.indexOf(callback);
return [...prev.slice(0, index), ...prev.slice(index + 1)];
});
}, [setListeners, callback]);
};

return [anAtom, useListener] as const;
}
2 changes: 1 addition & 1 deletion tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Config } from 'tailwindcss';
import colors from 'tailwindcss/colors';

export default {
content: ['./src/**/*.tsx'],
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
fontFamily: {
Expand Down

0 comments on commit c60b9df

Please sign in to comment.