Merge pull request #65 from KieIO/feature/login-signup

Add Login, Signup and Active Customer API Hook
This commit is contained in:
lytrankieio123
2021-09-28 11:21:41 +07:00
committed by GitHub
11 changed files with 410 additions and 96 deletions

View File

@@ -1,18 +1,23 @@
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,
visible: boolean
closeModal: () => void
mode?: '' | 'register'
}
const ModalAuthenticate = ({ visible, mode, closeModal }: Props) => {
const [isLogin, setIsLogin] = useState<boolean>(true)
const { customer } = useActiveCustomer()
const router = useRouter()
useEffect(() => {
if (mode === 'register') {
@@ -22,23 +27,35 @@ const ModalAuthenticate = ({ visible, mode, closeModal }: Props) => {
}
}, [mode])
useEffect(() => {
if (visible && customer) {
closeModal()
router.push(ROUTE.ACCOUNT)
}
}, [customer, visible])
const onSwitch = () => {
setIsLogin(!isLogin)
}
return (
<ModalCommon visible={visible} onClose={closeModal} title={isLogin ? 'Sign In' : 'Create Account'}>
<ModalCommon
visible={visible}
onClose={closeModal}
title={isLogin ? 'Sign In' : 'Create Account'}
>
<section className={s.formAuthenticate}>
<div className={classNames({
<div
className={classNames({
[s.inner]: true,
[s.register]: !isLogin,
})}>
})}
>
<FormLogin isHide={!isLogin} onSwitch={onSwitch} />
<FormRegister isHide={isLogin} onSwitch={onSwitch} />
</div>
</section>
</ModalCommon>
)
}

View File

@@ -1,19 +1,27 @@
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,
isHide: boolean
onSwitch: () => void
}
const FormLogin = ({ onSwitch, isHide }: Props) => {
const emailRef = useRef<CustomInputCommon>(null)
const { loading, login, error } = useLogin()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const onLogin = () => {
login({ username: email, password })
}
useEffect(() => {
if (!isHide) {
@@ -21,14 +29,31 @@ const FormLogin = ({ onSwitch, isHide }: Props) => {
}
}, [isHide])
useEffect(() => {
if (error) {
alert(error.message)
}
}, [error])
return (
<section className={s.formAuthen}>
<div className={s.inner}>
<div className={s.body}>
<Inputcommon placeholder='Email Address' type='email' ref={emailRef} />
<Inputcommon
placeholder="Email Address"
value={email}
onChange={(val) => setEmail(val.toString())}
type="email"
ref={emailRef}
/>
{/* <Inputcommon placeholder='Email Address' type='email' ref={emailRef}
isShowIconSuccess={true} isIconSuffix={true} /> */}
<InputPassword placeholder='Password'/>
<InputPassword
placeholder="Password"
value={password}
onChange={(val) => setPassword(val.toString())}
/>
</div>
<div className={styles.bottom}>
<Link href={ROUTE.FORGOT_PASSWORD}>
@@ -36,7 +61,9 @@ const FormLogin = ({ onSwitch, isHide }: Props) => {
Forgot Password?
</a>
</Link>
<ButtonCommon size='large'>Sign in</ButtonCommon>
<ButtonCommon onClick={onLogin} loading={loading} size="large">
Sign in
</ButtonCommon>
</div>
<SocialAuthen />
<div className={s.others}>

View File

@@ -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<ActiveCustomerQuery>([query], gglFetcher)
return { customer: data?.activeCustomer, ...rest }
}
export default useActiveCustomer

View File

@@ -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<CommonError | null>(null)
const { mutate } = useActiveCustomer()
const login = (options: LoginInput) => {
setError(null)
setLoading(true)
rawFetcher<LoginMutation>({
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

View File

@@ -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<Error | null>(null)
const { mutate } = useActiveCustomer()
const signup = ({ firstName, lastName, email, password }: SignupInput) => {
setError(null)
setLoading(true)
fetcher<SignupMutation>({
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

View File

@@ -1,21 +1,29 @@
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
@@ -28,26 +36,25 @@ const AccountInfomation = ({ account, onClick } : AccountInfomationProps) => {
</div>
<div className={s.accountName}>
{account.name}
</div>
<div className={s.accountEmail}>
{account.email}
{customer?.firstName} {customer?.lastName}
</div>
<div className={s.accountEmail}>{customer?.emailAddress}</div>
<div className={s.horizontalSeparator}></div>
<div className={s.shippingInfo}>Shipping Infomation</div>
<div className={s.accountAddress}>
{account.address + `, ${account.state}, ${account.city}, ${account.postalCode}`}
{account.address +
`, ${account.state}, ${account.city}, ${account.postalCode}`}
</div>
<div className={s.accountPhoneNumber}>
{account.phoneNumber}
</div>
<div className={s.accountPhoneNumber}>{account.phoneNumber}</div>
<div className={s.editInfoBtn}>
<ButtonCommon onClick={showEditForm} type="light" size="small">Edit</ButtonCommon>
<ButtonCommon onClick={showEditForm} type="light" size="small">
Edit
</ButtonCommon>
</div>
</section>
)

View File

@@ -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',
}

View File

@@ -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
}
}

28
src/utils/fetcher.ts Normal file
View File

@@ -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 <T>(options: QueryOptions): Promise<T> => {
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<T>(
process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string,
query,
variables,
token ? { Authorization: 'Bearer ' + token } : {}
)
return res
}
export default fetcher

11
src/utils/gglFetcher.ts Normal file
View File

@@ -0,0 +1,11 @@
import { RequestDocument, Variables } from 'graphql-request/dist/types'
import fetcher from './fetcher'
const gglFetcher = async <T>(
...params: [RequestDocument, Variables]
): Promise<T> => {
const [query, variables] = params
return fetcher<T>({ query, variables })
}
export default gglFetcher

28
src/utils/rawFetcher.ts Normal file
View File

@@ -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 = <T>({
query,
variables,
onLoad = () => true,
}: QueryOptions): Promise<{ data: T; headers: any }> => {
onLoad(true)
return rawRequest<T>(
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