From dc00a4ad7a79239a131016344c4e257b136d5364 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 25 Sep 2021 17:57:05 +0700 Subject: [PATCH] Add Login, Signup and Active Customer API Hook --- .../ModalAuthenticate/ModalAuthenticate.tsx | 73 ++++++++------ .../components/FormLogin/FormLogin.tsx | 95 ++++++++++++------- src/components/hooks/useActiveCustomer.tsx | 22 +++++ src/components/hooks/useLogin.tsx | 57 +++++++++++ src/components/hooks/useSignup.tsx | 67 +++++++++++++ .../AccountInfomation/AccountInfomation.tsx | 75 ++++++++------- src/domains/enums/ErrorCode.ts | 30 ++++++ src/domains/interfaces/CommonError.ts | 20 ++++ src/utils/fetcher.ts | 28 ++++++ src/utils/gglFetcher.ts | 11 +++ src/utils/rawFetcher.ts | 28 ++++++ 11 files changed, 410 insertions(+), 96 deletions(-) create mode 100644 src/components/hooks/useActiveCustomer.tsx create mode 100644 src/components/hooks/useLogin.tsx create mode 100644 src/components/hooks/useSignup.tsx create mode 100644 src/domains/enums/ErrorCode.ts create mode 100644 src/domains/interfaces/CommonError.ts create mode 100644 src/utils/fetcher.ts create mode 100644 src/utils/gglFetcher.ts create mode 100644 src/utils/rawFetcher.ts diff --git a/src/components/common/ModalAuthenticate/ModalAuthenticate.tsx b/src/components/common/ModalAuthenticate/ModalAuthenticate.tsx index b35258da5..b086098cc 100644 --- a/src/components/common/ModalAuthenticate/ModalAuthenticate.tsx +++ b/src/components/common/ModalAuthenticate/ModalAuthenticate.tsx @@ -1,45 +1,62 @@ import classNames from 'classnames' +import { useRouter } from 'next/router' import React, { useEffect, useState } from 'react' +import useActiveCustomer from 'src/components/hooks/useActiveCustomer' +import { ROUTE } from 'src/utils/constanst.utils' import ModalCommon from '../ModalCommon/ModalCommon' import FormLogin from './components/FormLogin/FormLogin' import FormRegister from './components/FormRegister/FormRegister' import s from './ModalAuthenticate.module.scss' interface Props { - visible: boolean, - closeModal: () => void, - mode?: '' | 'register' + visible: boolean + closeModal: () => void + mode?: '' | 'register' } const ModalAuthenticate = ({ visible, mode, closeModal }: Props) => { - const [isLogin, setIsLogin] = useState(true) + const [isLogin, setIsLogin] = useState(true) + const { customer } = useActiveCustomer() + const router = useRouter() - useEffect(() => { - if (mode === 'register') { - setIsLogin(false) - } else { - setIsLogin(true) - } - }, [mode]) - - const onSwitch = () => { - setIsLogin(!isLogin) + useEffect(() => { + if (mode === 'register') { + setIsLogin(false) + } else { + setIsLogin(true) } + }, [mode]) - return ( - -
-
- - -
-
-
+ useEffect(() => { + if (visible && customer) { + closeModal() + router.push(ROUTE.ACCOUNT) + } + }, [customer, visible]) - ) + const onSwitch = () => { + setIsLogin(!isLogin) + } + + return ( + +
+
+ + +
+
+
+ ) } -export default ModalAuthenticate \ No newline at end of file +export default ModalAuthenticate diff --git a/src/components/common/ModalAuthenticate/components/FormLogin/FormLogin.tsx b/src/components/common/ModalAuthenticate/components/FormLogin/FormLogin.tsx index b1059a441..9dd5296f8 100644 --- a/src/components/common/ModalAuthenticate/components/FormLogin/FormLogin.tsx +++ b/src/components/common/ModalAuthenticate/components/FormLogin/FormLogin.tsx @@ -1,51 +1,78 @@ import Link from 'next/link' -import React, { useEffect, useRef } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { ButtonCommon, Inputcommon, InputPassword } from 'src/components/common' import { ROUTE } from 'src/utils/constanst.utils' import { CustomInputCommon } from 'src/utils/type.utils' +import useLogin from 'src/components/hooks/useLogin' import s from '../FormAuthen.module.scss' import SocialAuthen from '../SocialAuthen/SocialAuthen' import styles from './FormLogin.module.scss' interface Props { - isHide: boolean, - onSwitch: () => void + isHide: boolean + onSwitch: () => void } const FormLogin = ({ onSwitch, isHide }: Props) => { - const emailRef = useRef(null) + const emailRef = useRef(null) + const { loading, login, error } = useLogin() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') - useEffect(() => { - if (!isHide) { - emailRef.current?.focus() - } - }, [isHide]) + const onLogin = () => { + login({ username: email, password }) + } - return ( -
-
-
- - {/* { + if (!isHide) { + emailRef.current?.focus() + } + }, [isHide]) + + useEffect(() => { + if (error) { + alert(error.message) + } + }, [error]) + + return ( +
+
+
+ setEmail(val.toString())} + type="email" + ref={emailRef} + /> + + {/* */} - -
-
- - - Forgot Password? - - - Sign in -
- -
- Don't have an account? - -
-
-
- ) + setPassword(val.toString())} + /> +
+
+ + + Forgot Password? + + + + Sign in + +
+ +
+ Don't have an account? + +
+
+
+ ) } -export default FormLogin \ No newline at end of file +export default FormLogin diff --git a/src/components/hooks/useActiveCustomer.tsx b/src/components/hooks/useActiveCustomer.tsx new file mode 100644 index 000000000..4226a15b7 --- /dev/null +++ b/src/components/hooks/useActiveCustomer.tsx @@ -0,0 +1,22 @@ +import { ActiveCustomerQuery } from '@framework/schema' +import { gql } from 'graphql-request' +import gglFetcher from 'src/utils/gglFetcher' +import useSWR from 'swr' + +const query = gql` + query activeCustomer { + activeCustomer { + id + firstName + lastName + emailAddress + } + } +` + +const useActiveCustomer = () => { + const { data, ...rest } = useSWR([query], gglFetcher) + return { customer: data?.activeCustomer, ...rest } +} + +export default useActiveCustomer diff --git a/src/components/hooks/useLogin.tsx b/src/components/hooks/useLogin.tsx new file mode 100644 index 000000000..0e056c2f7 --- /dev/null +++ b/src/components/hooks/useLogin.tsx @@ -0,0 +1,57 @@ +import { gql } from 'graphql-request' +import { useState } from 'react' +import useActiveCustomer from './useActiveCustomer' +import { CommonError } from 'src/domains/interfaces/CommonError' +import rawFetcher from 'src/utils/rawFetcher' +import { LoginMutation } from '@framework/schema' + +const query = gql` + mutation login($username: String!, $password: String!) { + login(username: $username, password: $password) { + __typename + ... on CurrentUser { + id + } + ... on ErrorResult { + errorCode + message + } + } + } +` + +interface LoginInput { + username: string + password: string +} + +const useLogin = () => { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const { mutate } = useActiveCustomer() + + const login = (options: LoginInput) => { + setError(null) + setLoading(true) + rawFetcher({ + query, + variables: options, + }) + .then(({ data, headers }) => { + if (data.login.__typename !== 'CurrentUser') { + throw CommonError.create(data.login.message, data.login.errorCode) + } + const authToken = headers.get('vendure-auth-token') + if (authToken != null) { + localStorage.setItem('token', authToken) + return mutate() + } + }) + .catch(setError) + .finally(() => setLoading(false)) + } + + return { loading, login, error } +} + +export default useLogin diff --git a/src/components/hooks/useSignup.tsx b/src/components/hooks/useSignup.tsx new file mode 100644 index 000000000..b06781e88 --- /dev/null +++ b/src/components/hooks/useSignup.tsx @@ -0,0 +1,67 @@ +import { gql } from 'graphql-request' +import { useState } from 'react' +import useActiveCustomer from './useActiveCustomer' +import { SignupMutation } from '@framework/schema' +import fetcher from 'src/utils/fetcher' +import { CommonError } from 'src/domains/interfaces/CommonError' + +const query = gql` + mutation signup($input: RegisterCustomerInput!) { + registerCustomerAccount(input: $input) { + __typename + ... on Success { + success + } + ... on ErrorResult { + errorCode + message + } + } + } +` + +interface SignupInput { + email: string + firstName: string + lastName: string + password: string +} + +const useSignup = () => { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const { mutate } = useActiveCustomer() + + const signup = ({ firstName, lastName, email, password }: SignupInput) => { + setError(null) + setLoading(true) + fetcher({ + query, + variables: { + input: { + firstName, + lastName, + emailAddress: email, + password, + }, + }, + }) + .then((data) => { + if (data.registerCustomerAccount.__typename !== 'Success') { + throw CommonError.create( + data.registerCustomerAccount.message, + data.registerCustomerAccount.errorCode + ) + } + console.log(data) + mutate() + return data + }) + .catch(setError) + .finally(() => setLoading(false)) + } + + return { loading, signup, error } +} + +export default useSignup diff --git a/src/components/modules/account/AccountPage/components/AccountInfomation/AccountInfomation.tsx b/src/components/modules/account/AccountPage/components/AccountInfomation/AccountInfomation.tsx index 241c631d6..5da39b54d 100644 --- a/src/components/modules/account/AccountPage/components/AccountInfomation/AccountInfomation.tsx +++ b/src/components/modules/account/AccountPage/components/AccountInfomation/AccountInfomation.tsx @@ -1,56 +1,63 @@ -import React from "react" +import React from 'react' import s from './AccountInfomation.module.scss' -import Image from "next/image" -import avatar from '../../assets/avatar.png'; +import Image from 'next/image' +import avatar from '../../assets/avatar.png' import { ButtonCommon } from 'src/components/common' +import useActiveCustomer from 'src/components/hooks/useActiveCustomer' interface AccountProps { - name: string, email: string, address: string, state: string, city: string, postalCode: string, phoneNumber: string + name: string + email: string + address: string + state: string + city: string + postalCode: string + phoneNumber: string } interface AccountInfomationProps { - account: AccountProps; - onClick: () => void; + account: AccountProps + onClick: () => void } -const AccountInfomation = ({ account, onClick } : AccountInfomationProps) => { +const AccountInfomation = ({ account, onClick }: AccountInfomationProps) => { + const { customer } = useActiveCustomer() - // need to handle call back when edit account information + // need to handle call back when edit account information - const showEditForm = () => onClick() + const showEditForm = () => onClick() - return ( -
-
- avatar -
+ return ( +
+
+ avatar +
-
- {account.name} -
-
- {account.email} -
+
+ {customer?.firstName} {customer?.lastName} +
+
{customer?.emailAddress}
-
+
-
Shipping Infomation
+
Shipping Infomation
-
- {account.address + `, ${account.state}, ${account.city}, ${account.postalCode}`} -
+
+ {account.address + + `, ${account.state}, ${account.city}, ${account.postalCode}`} +
-
- {account.phoneNumber} -
+
{account.phoneNumber}
-
- Edit -
-
- ) +
+ + Edit + +
+
+ ) } -export default AccountInfomation \ No newline at end of file +export default AccountInfomation diff --git a/src/domains/enums/ErrorCode.ts b/src/domains/enums/ErrorCode.ts new file mode 100644 index 000000000..daa3cdce6 --- /dev/null +++ b/src/domains/enums/ErrorCode.ts @@ -0,0 +1,30 @@ +export enum ErrorCode { + UnknownError = 'UNKNOWN_ERROR', + NativeAuthStrategyError = 'NATIVE_AUTH_STRATEGY_ERROR', + InvalidCredentialsError = 'INVALID_CREDENTIALS_ERROR', + OrderStateTransitionError = 'ORDER_STATE_TRANSITION_ERROR', + EmailAddressConflictError = 'EMAIL_ADDRESS_CONFLICT_ERROR', + OrderLimitError = 'ORDER_LIMIT_ERROR', + NegativeQuantityError = 'NEGATIVE_QUANTITY_ERROR', + InsufficientStockError = 'INSUFFICIENT_STOCK_ERROR', + OrderModificationError = 'ORDER_MODIFICATION_ERROR', + IneligibleShippingMethodError = 'INELIGIBLE_SHIPPING_METHOD_ERROR', + OrderPaymentStateError = 'ORDER_PAYMENT_STATE_ERROR', + IneligiblePaymentMethodError = 'INELIGIBLE_PAYMENT_METHOD_ERROR', + PaymentFailedError = 'PAYMENT_FAILED_ERROR', + PaymentDeclinedError = 'PAYMENT_DECLINED_ERROR', + CouponCodeInvalidError = 'COUPON_CODE_INVALID_ERROR', + CouponCodeExpiredError = 'COUPON_CODE_EXPIRED_ERROR', + CouponCodeLimitError = 'COUPON_CODE_LIMIT_ERROR', + AlreadyLoggedInError = 'ALREADY_LOGGED_IN_ERROR', + MissingPasswordError = 'MISSING_PASSWORD_ERROR', + PasswordAlreadySetError = 'PASSWORD_ALREADY_SET_ERROR', + VerificationTokenInvalidError = 'VERIFICATION_TOKEN_INVALID_ERROR', + VerificationTokenExpiredError = 'VERIFICATION_TOKEN_EXPIRED_ERROR', + IdentifierChangeTokenInvalidError = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR', + IdentifierChangeTokenExpiredError = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR', + PasswordResetTokenInvalidError = 'PASSWORD_RESET_TOKEN_INVALID_ERROR', + PasswordResetTokenExpiredError = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR', + NotVerifiedError = 'NOT_VERIFIED_ERROR', + NoActiveOrderError = 'NO_ACTIVE_ORDER_ERROR', +} diff --git a/src/domains/interfaces/CommonError.ts b/src/domains/interfaces/CommonError.ts new file mode 100644 index 000000000..6f603c0e3 --- /dev/null +++ b/src/domains/interfaces/CommonError.ts @@ -0,0 +1,20 @@ +import { ErrorCode } from '../enums/ErrorCode' + +// TODO: Can not find +// import { ErrorCode } from '@framework/schema' + +export class CommonError { + message: string = '' + + errorCode: ErrorCode = ErrorCode.UnknownError + + error?: Error + + static create(message: string, errorCode?: ErrorCode): CommonError { + const error = new CommonError() + error.message = message + error.errorCode = errorCode || ErrorCode.UnknownError + error.error = new Error() + return error + } +} diff --git a/src/utils/fetcher.ts b/src/utils/fetcher.ts new file mode 100644 index 000000000..e92696970 --- /dev/null +++ b/src/utils/fetcher.ts @@ -0,0 +1,28 @@ +import { request } from 'graphql-request' +import { RequestDocument, Variables } from 'graphql-request/dist/types' + +interface QueryOptions { + query: RequestDocument + variables?: Variables + onLoad?: (loading: boolean) => any + key?: string +} + +const fetcher = async (options: QueryOptions): Promise => { + const { query, variables } = options + console.log('query') + console.log(options) + const token = localStorage.getItem('token') + console.log('token') + console.log(token) + const res = await request( + process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, + query, + variables, + token ? { Authorization: 'Bearer ' + token } : {} + ) + + return res +} + +export default fetcher diff --git a/src/utils/gglFetcher.ts b/src/utils/gglFetcher.ts new file mode 100644 index 000000000..974525902 --- /dev/null +++ b/src/utils/gglFetcher.ts @@ -0,0 +1,11 @@ +import { RequestDocument, Variables } from 'graphql-request/dist/types' +import fetcher from './fetcher' + +const gglFetcher = async ( + ...params: [RequestDocument, Variables] +): Promise => { + const [query, variables] = params + return fetcher({ query, variables }) +} + +export default gglFetcher diff --git a/src/utils/rawFetcher.ts b/src/utils/rawFetcher.ts new file mode 100644 index 000000000..8ee6a05ee --- /dev/null +++ b/src/utils/rawFetcher.ts @@ -0,0 +1,28 @@ +import { rawRequest } from 'graphql-request' +import { RequestDocument, Variables } from 'graphql-request/dist/types' + +interface QueryOptions { + query: RequestDocument + variables?: Variables + onLoad?: (loading: boolean) => any + key?: string +} + +const rawFetcher = ({ + query, + variables, + onLoad = () => true, +}: QueryOptions): Promise<{ data: T; headers: any }> => { + onLoad(true) + return rawRequest( + process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, + query as string, + variables + ) + .then(({ data, headers }) => { + return { data, headers } + }) + .finally(() => onLoad(false)) +} + +export default rawFetcher