diff --git a/components/auth/ChangePassword.tsx b/components/auth/ChangePassword.tsx new file mode 100644 index 000000000..02748fd76 --- /dev/null +++ b/components/auth/ChangePassword.tsx @@ -0,0 +1,123 @@ +import { FC, useEffect, useState, useCallback } from 'react' +import { Logo, Button, Input } from '@components/ui' +import { useUI } from '@components/ui/context' +import useCustomer from '@framework/customer/use-customer' +import useChangePassword from '@framework/auth/use-change-password' +import changePassword from '@framework/api/endpoints/change-password/change-password' + +// import { validate } from 'email-validator' + +interface Props { +} + +const ChangePassword: FC = () => { + const changePassword = useChangePassword() + // return (
FOo
) + // + const { data: customer } = useCustomer() + // + if (customer) { + console.log(customer) + } else { + return <>; + } + + + // Form State + const [email, _setEmail] = useState(customer.email as string) + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [dirty, setDirty] = useState(false) + const [disabled, setDisabled] = useState(false) + const { setModalView, closeModal } = useUI() + + // // const login = useLogin() + // + const handleChangePassword = async (e: React.SyntheticEvent) => { + console.log('handleChangePassword'); + e.preventDefault() + + if (!dirty && !disabled) { + setDirty(true) + handleValidation() + } + + try { + setLoading(true) + setMessage('') + await changePassword({ + email, + currentPassword, + newPassword + }) + setLoading(false) + closeModal() + } catch (error) { + console.dir(error); + const { errors } = error; + console.dir(errors); + setMessage(errors[0].message) + setLoading(false) + } + } + + const handleValidation = useCallback(() => { + // Test for Alphanumeric password + const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(newPassword) + + // Unable to send form unless fields are valid. + if (dirty) { + setDisabled( newPassword.length < 7 || !validPassword ||newPassword != confirmPassword ) + } + }, [newPassword, confirmPassword, dirty]) + + useEffect(() => { + handleValidation() + }, [handleValidation]) + + return ( +
+
+ +
+
+ {message && ( +
+ {message}. Did you {` `} + setModalView('FORGOT_VIEW')} + > + forgot your password? + +
+ )} + + + + + + + +
+
+ ) +} + + + + +export default ChangePassword diff --git a/components/auth/index.ts b/components/auth/index.ts index 11571fac7..47a783943 100644 --- a/components/auth/index.ts +++ b/components/auth/index.ts @@ -1,3 +1,4 @@ export { default as LoginView } from './LoginView' export { default as SignUpView } from './SignUpView' export { default as ForgotPassword } from './ForgotPassword' +export { default as ChangePassword } from './ChangePassword' diff --git a/components/common/Layout/Layout.tsx b/components/common/Layout/Layout.tsx index ff6d72aaf..6910ad68f 100644 --- a/components/common/Layout/Layout.tsx +++ b/components/common/Layout/Layout.tsx @@ -42,6 +42,11 @@ const FeatureBar = dynamic( dynamicProps ) +const ChangePassword = dynamic( + () => import('@components/auth/ChangePassword'), + dynamicProps +) + interface Props { pageProps: { pages?: Page[] @@ -58,6 +63,7 @@ const ModalView: FC<{ modalView: string; closeModal(): any }> = ({ {modalView === 'LOGIN_VIEW' && } {modalView === 'SIGNUP_VIEW' && } {modalView === 'FORGOT_VIEW' && } + {modalView === 'CHANGE_PASSWORD' && } ) } diff --git a/framework/bigcommerce/api/endpoints/change-password/change-password.ts b/framework/bigcommerce/api/endpoints/change-password/change-password.ts new file mode 100644 index 000000000..c56d8b0f4 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/change-password/change-password.ts @@ -0,0 +1,65 @@ +import { BigcommerceApiError } from '../../utils/errors' +import type { ChangePasswordEndpoint } from '.' + +const changePassword: ChangePasswordEndpoint['handlers']['changePassword'] = async ({ + res, + body: { email, currentPassword, newPassword }, + config, + commerce + }) => { + // // TODO: Add proper validations with something like Ajv + // if (!(email && password)) { + // return res.status(400).json({ + // data: null, + // errors: [{ message: 'Invalid request' }] + // }) + // } + // // TODO: validate the password and email + // // Passwords must be at least 7 characters and contain both alphabetic + // // and numeric characters. + // + // try { + // await config.storeApiFetch('/v3/customers', { + // method: 'POST', + // body: JSON.stringify([ + // { + // first_name: firstName, + // last_name: lastName, + // email, + // authentication: { + // new_password: password + // } + // } + // ]) + // }) + // } catch (error) { + // if (error instanceof BigcommerceApiError && error.status === 422) { + // const hasEmailError = '0.email' in error.data?.errors + // + // // If there's an error with the email, it most likely means it's duplicated + // if (hasEmailError) { + // return res.status(400).json({ + // data: null, + // errors: [ + // { + // message: 'The email is already in use', + // code: 'duplicated_email' + // } + // ] + // }) + // } + // } + // + // throw error + // } + // + // // Login the customer right after creating it + // await commerce.login({ variables: { email, password }, res, config }) + + + const supplied = `YOU SUPPLIED: "${email}" "${currentPassword}" "${newPassword}"` + console.log(supplied); + res.status(200).json({ data: null, errors: [{message: supplied }] }) +} + +export default changePassword diff --git a/framework/bigcommerce/api/endpoints/change-password/index.ts b/framework/bigcommerce/api/endpoints/change-password/index.ts new file mode 100644 index 000000000..27a5ecdeb --- /dev/null +++ b/framework/bigcommerce/api/endpoints/change-password/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import changePasswordEndpoint from '@commerce/api/endpoints/change-password' +import type { ChangePasswordSchema } from '../../../types/change-password' +import type { BigcommerceAPI } from '../..' +import changePassword from './change-password' + +export type ChangePasswordAPI = GetAPISchema + +export type ChangePasswordEndpoint = ChangePasswordAPI['endpoint'] + +export const handlers: ChangePasswordEndpoint['handlers'] = { changePassword } + +const changePasswordApi = createEndpoint({ + handler: changePasswordEndpoint, + handlers, +}) + +export default changePasswordApi diff --git a/framework/bigcommerce/auth/index.ts b/framework/bigcommerce/auth/index.ts index 36e757a89..9b9aa9d6e 100644 --- a/framework/bigcommerce/auth/index.ts +++ b/framework/bigcommerce/auth/index.ts @@ -1,3 +1,4 @@ export { default as useLogin } from './use-login' export { default as useLogout } from './use-logout' export { default as useSignup } from './use-signup' +export { default as useChangePassword } from './use-change-password' diff --git a/framework/bigcommerce/auth/use-change-password.ts b/framework/bigcommerce/auth/use-change-password.ts new file mode 100644 index 000000000..0a39ae05d --- /dev/null +++ b/framework/bigcommerce/auth/use-change-password.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' +import useChangePassword , { UseChangePassword } from '@commerce/auth/use-change-password' +import type { ChangePasswordHook } from '../types/change-password' +import useCustomer from '../customer/use-customer' + +export default useChangePassword as UseChangePassword + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/change-password', + method: 'POST', + }, + async fetcher({ input: { email, currentPassword, newPassword }, options, fetch }) { + if (!(email && currentPassword && newPassword)) { + throw new CommerceError({ + message: + 'An email, current password, and new password are required to change password', + }) + } + + console.log('fetcher') + console.dir({ email, currentPassword, newPassword }) + + return fetch({ + ...options, + body: { email, currentPassword, newPassword }, + }) + }, + useHook: ({ fetch }) => () => { + const { revalidate } = useCustomer() + + return useCallback( + async function changePassword(input) { + const data = await fetch({ input }) + await revalidate() + return data + }, + [fetch, revalidate] + ) + }, +} diff --git a/framework/bigcommerce/provider.ts b/framework/bigcommerce/provider.ts index 196855438..f3f97da9a 100644 --- a/framework/bigcommerce/provider.ts +++ b/framework/bigcommerce/provider.ts @@ -13,6 +13,7 @@ import { handler as useSearch } from './product/use-search' import { handler as useLogin } from './auth/use-login' import { handler as useLogout } from './auth/use-logout' import { handler as useSignup } from './auth/use-signup' +import { handler as useChangePassword } from './auth/use-change-password' import fetcher from './fetcher' @@ -28,7 +29,7 @@ export const bigcommerceProvider = { }, customer: { useCustomer }, products: { useSearch }, - auth: { useLogin, useLogout, useSignup }, + auth: { useLogin, useLogout, useSignup, useChangePassword }, } export type BigcommerceProvider = typeof bigcommerceProvider diff --git a/framework/bigcommerce/types/change-password.ts b/framework/bigcommerce/types/change-password.ts new file mode 100644 index 000000000..5c4edc506 --- /dev/null +++ b/framework/bigcommerce/types/change-password.ts @@ -0,0 +1 @@ +export * from '@commerce/types/change-password' diff --git a/framework/commerce/api/endpoints/change-password.ts b/framework/commerce/api/endpoints/change-password.ts new file mode 100644 index 000000000..03ad4ebec --- /dev/null +++ b/framework/commerce/api/endpoints/change-password.ts @@ -0,0 +1,35 @@ +import type { ChangePasswordSchema } from '../../types/change-password' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const changePasswordEndpoint: GetAPISchema< + any, + ChangePasswordSchema + >['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers } = ctx + + if ( + !isAllowedOperation(req, res, { + POST: handlers['changePassword'], + }) + ) { + return + } + + try { + const body = req.body ?? {} + return await handlers['changePassword']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default changePasswordEndpoint diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index 32fe8cf80..e0f4fdef0 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -9,6 +9,7 @@ import type { SignupSchema } from '../types/signup' import type { ProductsSchema } from '../types/product' import type { WishlistSchema } from '../types/wishlist' import type { CheckoutSchema } from '../types/checkout' +import type { ChangePasswordSchema } from '../types/change-password' import { defaultOperations, OPERATIONS, @@ -25,6 +26,7 @@ export type APISchemas = | ProductsSchema | WishlistSchema | CheckoutSchema + | ChangePasswordSchema export type GetAPISchema< C extends CommerceAPI, diff --git a/framework/commerce/auth/use-change-password.tsx b/framework/commerce/auth/use-change-password.tsx new file mode 100644 index 000000000..46d74958b --- /dev/null +++ b/framework/commerce/auth/use-change-password.tsx @@ -0,0 +1,20 @@ +import { useHook, useMutationHook } from '../utils/use-hook' +import { mutationFetcher } from '../utils/default-fetcher' +import type { MutationHook, HookFetcherFn } from '../utils/types' +import type { Provider } from '..' +import { ChangePasswordHook } from '@commerce/types/change-password' + +export type UseChangePassword< + H extends MutationHook> = MutationHook + > = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.auth?.useChangePassword! + +const useChangePassword: UseChangePassword = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) +} + +export default useChangePassword diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index 7ecb44dc1..69b484e55 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -15,6 +15,7 @@ import type { Signup, Login, Logout, + ChangePassword } from '@commerce/types' import type { Fetcher, SWRHook, MutationHook } from './utils/types' @@ -44,6 +45,7 @@ export type Provider = CommerceConfig & { useSignup?: MutationHook useLogin?: MutationHook useLogout?: MutationHook + useChangePassword?: MutationHook } } diff --git a/framework/commerce/types/change-password.ts b/framework/commerce/types/change-password.ts new file mode 100644 index 000000000..8c9f106a4 --- /dev/null +++ b/framework/commerce/types/change-password.ts @@ -0,0 +1,25 @@ +export type ChangePasswordBody = { + email: string + currentPassword: string + newPassword: string +} + +export type ChangePasswordTypes = { + body: ChangePasswordBody +} + +export type ChangePasswordHook = { + data: null + body: T['body'] + actionInput: T['body'] + fetcherInput: T['body'] +} + +export type ChangePasswordSchema = { + endpoint: { + options: {} + handlers: { + changePassword: ChangePasswordHook + } + } +} diff --git a/framework/commerce/types/index.ts b/framework/commerce/types/index.ts index 7ab0b7f64..cce251b24 100644 --- a/framework/commerce/types/index.ts +++ b/framework/commerce/types/index.ts @@ -1,4 +1,5 @@ import * as Cart from './cart' +import * as ChangePassword from './change-password' import * as Checkout from './checkout' import * as Common from './common' import * as Customer from './customer' @@ -12,6 +13,7 @@ import * as Wishlist from './wishlist' export type { Cart, + ChangePassword, Checkout, Common, Customer, diff --git a/pages/api/change-password.ts b/pages/api/change-password.ts new file mode 100644 index 000000000..cdfdd909c --- /dev/null +++ b/pages/api/change-password.ts @@ -0,0 +1,4 @@ +import changePasswordApi from '@framework/api/endpoints/change-password' +import commerce from '@lib/api/commerce' + +export default changePasswordApi(commerce) diff --git a/pages/profile.tsx b/pages/profile.tsx index eb54004ee..4c8f4cc84 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -2,7 +2,9 @@ import type { GetStaticPropsContext } from 'next' import useCustomer from '@framework/customer/use-customer' import commerce from '@lib/api/commerce' import { Layout } from '@components/common' -import { Container, Text } from '@components/ui' +import { Container, Text, useUI } from '@components/ui' + + export async function getStaticProps({ preview, @@ -21,7 +23,15 @@ export async function getStaticProps({ } export default function Profile() { + const { openModal, setModalView } = useUI() + const { data } = useCustomer() + + const triggerChangePassword = () => { + setModalView('CHANGE_PASSWORD') + openModal() + }; + return ( My Profile @@ -38,6 +48,14 @@ export default function Profile() { Email {data.email} + +
+ Change Password + Change Password +
)} diff --git a/tsconfig.json b/tsconfig.json index 340929669..61501d8db 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,8 +23,8 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/local"], - "@framework/*": ["framework/local/*"] + "@framework": ["framework/bigcommerce"], + "@framework/*": ["framework/bigcommerce/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], @@ -34,6 +34,11 @@ "./framework/shopify", "./framework/swell", "./framework/vendure", - "./framework/saleor" + "./framework/saleor", + "framework/saleor", + "framework/shopify", + "framework/swell", + "framework/vendure", + "framework/local" ] } diff --git a/yarn.lock b/yarn.lock index 8d1a534a5..62b7698cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1140,6 +1140,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== +"@types/uuid@8.3.1": + version "8.3.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" + integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== + "@types/websocket@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a" @@ -6044,6 +6049,19 @@ util@^0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuidv4@^6.2.10: + version "6.2.11" + resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.11.tgz#34d5a03324eb38296b87ae523a64233b5286cc27" + integrity sha512-OTS4waH9KplrXNADKo+Q1kT9AHWr8DaC0S5F54RQzEwcUaEzBEWQQlJyDUw/u1bkRhJyqkqhLD4M4lbFbV+89g== + dependencies: + "@types/uuid" "8.3.1" + uuid "8.3.2" + valid-url@1.0.9, valid-url@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"