mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 12:24:18 +00:00
✨ feat: apply coupon code for order
:%s
This commit is contained in:
31
framework/vendure/schema.d.ts
vendored
31
framework/vendure/schema.d.ts
vendored
@@ -3053,6 +3053,7 @@ export type CartFragment = { __typename?: 'Order' } & Pick<
|
||||
| 'currencyCode'
|
||||
> & {
|
||||
shippingAddress?: Maybe<{ __typename?: 'OrderAddress' } & Pick<OrderAddress, 'streetLine1' | 'fullName' | 'city' | 'province' | 'postalCode' |'countryCode' | 'phoneNumber'>>
|
||||
discounts?: Maybe<{ __typename?: 'Discount' } & Pick<Discount, 'type' | 'amount' | 'amountWithTax'>>
|
||||
customer?: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id' | 'firstName' | 'lastName' | 'emailAddress'>>
|
||||
lines: Array<
|
||||
{ __typename?: 'OrderLine' } & Pick<
|
||||
@@ -3156,6 +3157,36 @@ export type AdjustOrderLineMutation = { __typename?: 'Mutation' } & {
|
||||
>)
|
||||
}
|
||||
|
||||
|
||||
export type ApplyCouponCodeMutationVariables = Exact<{
|
||||
couponCode: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type ApplyCouponCodeMutation = {
|
||||
applyCouponCode:
|
||||
| TestOrderFragmentFragment
|
||||
| Pick<CouponCodeExpiredError, 'errorCode' | 'message'>
|
||||
| Pick<CouponCodeInvalidError, 'errorCode' | 'message'>
|
||||
| Pick<CouponCodeLimitError, 'errorCode' | 'message'>;
|
||||
};
|
||||
|
||||
export type ApplyCouponCodeMutation = { __typename?: 'Mutation' } & {
|
||||
applyCouponCode:
|
||||
| ({ __typename: 'Order' } & CartFragment)
|
||||
| ({ __typename: 'CouponCodeExpiredError' } & Pick<
|
||||
CouponCodeExpiredError,
|
||||
'errorCode' | 'message'
|
||||
>)
|
||||
| ({ __typename: 'CouponCodeInvalidError' } & Pick<
|
||||
CouponCodeInvalidError,
|
||||
'errorCode' | 'message'
|
||||
>)
|
||||
| ({ __typename: 'CouponCodeLimitError' } & Pick<
|
||||
CouponCodeLimitError,
|
||||
'errorCode' | 'message'
|
||||
>)
|
||||
}
|
||||
|
||||
export type LoginMutationVariables = Exact<{
|
||||
username: Scalars['String']
|
||||
password: Scalars['String']
|
||||
|
@@ -0,0 +1,21 @@
|
||||
export const applyCouponCodeMutation = /* GraphQL */ `
|
||||
mutation applyCouponCode($couponCode: String!) {
|
||||
applyCouponCode(couponCode: $couponCode) {
|
||||
__typename
|
||||
... on Order {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
discounts {
|
||||
type
|
||||
amount
|
||||
amountWithTax
|
||||
}
|
||||
}
|
||||
... on ErrorResult {
|
||||
errorCode
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@@ -1,7 +1,7 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.buttonCommon {
|
||||
@apply shape-common;
|
||||
@apply shape-common h-full;
|
||||
&:hover {
|
||||
.inner {
|
||||
@apply shadow-md;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
export { default as useSetCustomerForOrder } from './useSetCustomerForOrder'
|
||||
export { default as useSetOrderShippingAddress } from './useSetOrderShippingAddress'
|
||||
export { default as useApplyCouponCode } from './useApplyCouponCode'
|
||||
|
||||
|
41
src/components/hooks/order/useApplyCouponCode.tsx
Normal file
41
src/components/hooks/order/useApplyCouponCode.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ApplyCouponCodeMutation } from '@framework/schema'
|
||||
import { applyCouponCodeMutation } from '@framework/utils/mutations/apply-coupon-code-mutation'
|
||||
import { useState } from 'react'
|
||||
import { CommonError } from 'src/domains/interfaces/CommonError'
|
||||
import rawFetcher from 'src/utils/rawFetcher'
|
||||
import { useGetActiveOrder } from '../cart'
|
||||
|
||||
|
||||
const useApplyCouponCode = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<CommonError | null>(null)
|
||||
const { mutate } = useGetActiveOrder()
|
||||
|
||||
const applyCouponCode = (couponCode: string,
|
||||
fCallBack: (isSuccess: boolean, message?: string) => void
|
||||
) => {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
rawFetcher<ApplyCouponCodeMutation>({
|
||||
query: applyCouponCodeMutation,
|
||||
variables: { couponCode },
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.applyCouponCode.__typename === 'Order') {
|
||||
fCallBack(true)
|
||||
mutate()
|
||||
} else {
|
||||
fCallBack(false, data.applyCouponCode.message)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error)
|
||||
fCallBack(false, error.message)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
return { loading, applyCouponCode, error }
|
||||
}
|
||||
|
||||
export default useApplyCouponCode
|
@@ -18,11 +18,6 @@
|
||||
min-height: 52.8rem;
|
||||
}
|
||||
.bot {
|
||||
.promo {
|
||||
// padding: 3.2rem;
|
||||
@apply bg-gray flex justify-between items-center;
|
||||
min-height: 6.4rem;
|
||||
}
|
||||
.price {
|
||||
margin-top: 3.2rem;
|
||||
.line {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import s from './CheckoutBill.module.scss'
|
||||
import { CardItemCheckout } from '../../../common'
|
||||
import { CardItemCheckoutProps } from '../../../common/CardItemCheckout/CardItemCheckout'
|
||||
import { IconCirclePlus } from 'src/components/icons'
|
||||
import s from './CheckoutBill.module.scss'
|
||||
import FormPromotionCode from './FormPromotionCode/FormPromotionCode'
|
||||
|
||||
interface CheckoutBillProps {
|
||||
data: CardItemCheckoutProps[]
|
||||
@@ -20,10 +20,7 @@ const CheckoutBill = ({ data }: CheckoutBillProps) => {
|
||||
})}
|
||||
</div>
|
||||
<div className={s.bot}>
|
||||
<div className={s.promo}>
|
||||
Apply Promotion Code
|
||||
<IconCirclePlus />
|
||||
</div>
|
||||
<FormPromotionCode/>
|
||||
<div className={s.price}>
|
||||
<div className={s.line}>
|
||||
Subtotal
|
||||
|
@@ -0,0 +1,19 @@
|
||||
.promo {
|
||||
@apply bg-gray flex justify-between items-center;
|
||||
min-height: 6.4rem;
|
||||
.modalPromotion {
|
||||
min-width: 40rem;
|
||||
.bottom {
|
||||
@apply flex justify-end items-center;
|
||||
margin-top: 3.2rem;
|
||||
button {
|
||||
&:first-child {
|
||||
margin-right: 0.8rem;
|
||||
@screen md {
|
||||
margin-right: 3.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
import { Form, Formik } from 'formik';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { ButtonCommon, InputFiledInForm, ModalCommon } from 'src/components/common';
|
||||
import { useMessage } from 'src/components/contexts';
|
||||
import { useModalCommon } from 'src/components/hooks';
|
||||
import { useApplyCouponCode } from 'src/components/hooks/order';
|
||||
import { IconCirclePlus } from 'src/components/icons';
|
||||
import { LANGUAGE } from 'src/utils/language.utils';
|
||||
import { CustomInputCommon } from 'src/utils/type.utils';
|
||||
import * as Yup from 'yup';
|
||||
import s from './FormPromotionCode.module.scss';
|
||||
|
||||
const displayingErrorMessagesSchema = Yup.object().shape({
|
||||
couponCode: Yup.string().required(LANGUAGE.MESSAGE.REQUIRED),
|
||||
})
|
||||
|
||||
const FormPromotionCode = () => {
|
||||
const { visible, openModal, closeModal } = useModalCommon({ initialValue: false })
|
||||
const { showMessageError, showMessageSuccess } = useMessage()
|
||||
const { applyCouponCode, loading } = useApplyCouponCode()
|
||||
const inputRef = useRef<CustomInputCommon>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (visible) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, 500);
|
||||
}, [visible])
|
||||
|
||||
const handleSubmit = (values: { couponCode: string }) => {
|
||||
applyCouponCode(values.couponCode, onSubmitCalBack)
|
||||
}
|
||||
|
||||
const onSubmitCalBack = (isSuccess: boolean, msg?: string) => {
|
||||
// TODO:
|
||||
if (isSuccess) {
|
||||
showMessageSuccess("Applied coupon code successfully.", 5000)
|
||||
closeModal()
|
||||
} else {
|
||||
showMessageError(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.promo}>
|
||||
Apply Promotion Code
|
||||
<button className={s.buttonAdd} onClick={openModal}>
|
||||
<IconCirclePlus />
|
||||
</button>
|
||||
<ModalCommon
|
||||
visible={visible}
|
||||
onClose={closeModal}
|
||||
>
|
||||
<div className={s.modalPromotion}>
|
||||
<Formik
|
||||
initialValues={{
|
||||
couponCode: ''
|
||||
}}
|
||||
validationSchema={displayingErrorMessagesSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ errors, touched, isValid, submitForm }) => (
|
||||
<Form className="u-form">
|
||||
<div className="body">
|
||||
|
||||
<InputFiledInForm
|
||||
name="couponCode"
|
||||
placeholder="Coupon code"
|
||||
error={
|
||||
touched.couponCode && errors.couponCode
|
||||
? errors.couponCode.toString()
|
||||
: ''
|
||||
}
|
||||
isShowIconSuccess={touched.couponCode && !errors.couponCode}
|
||||
onEnter={isValid ? submitForm : undefined}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
<div className={s.bottom}>
|
||||
<ButtonCommon disabled={loading} onClick={closeModal} type='light' size='small'>
|
||||
{LANGUAGE.BUTTON_LABEL.CANCEL}
|
||||
</ButtonCommon>
|
||||
<ButtonCommon HTMLType='submit' loading={loading} size='small'>
|
||||
Apply promotion code
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</ModalCommon>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormPromotionCode;
|
@@ -132,7 +132,7 @@ const CheckoutInfo = ({ onViewCart }: CheckoutInfoProps) => {
|
||||
{/* TODO: remove */}
|
||||
<ButtonCommon onClick={createOrder}>test create order</ButtonCommon>
|
||||
<ButtonCommon onClick={createOrder}>test get activeStep order</ButtonCommon>
|
||||
|
||||
TOTAL: {order?.totalPrice}
|
||||
|
||||
<div className={s.title}>
|
||||
<Logo />
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Form, Formik } from 'formik'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { ButtonCommon, InputFiledInForm } from 'src/components/common'
|
||||
import ModalAuthenticate from 'src/components/common/ModalAuthenticate/ModalAuthenticate'
|
||||
import { useMessage } from 'src/components/contexts'
|
||||
@@ -16,6 +16,8 @@ import ModalConfirmLogin from './ModalConfirmLogin/ModalConfirmLogin'
|
||||
interface Props {
|
||||
id: number
|
||||
onConfirm: (id: number) => void
|
||||
activeStep: number
|
||||
|
||||
}
|
||||
|
||||
const displayingErrorMessagesSchema = Yup.object().shape({
|
||||
@@ -24,7 +26,7 @@ const displayingErrorMessagesSchema = Yup.object().shape({
|
||||
emailAddress: Yup.string().email(LANGUAGE.MESSAGE.INVALID_EMAIL).required(LANGUAGE.MESSAGE.REQUIRED),
|
||||
})
|
||||
|
||||
const CustomerInfoForm = ({ id, onConfirm }: Props) => {
|
||||
const CustomerInfoForm = ({ id, onConfirm, activeStep }: Props) => {
|
||||
const firstNameRef = useRef<CustomInputCommon>(null)
|
||||
const { setCustomerForOrder, loading } = useSetCustomerForOrder()
|
||||
const { showMessageError } = useMessage()
|
||||
@@ -32,6 +34,11 @@ const CustomerInfoForm = ({ id, onConfirm }: Props) => {
|
||||
const { visible: visibleModalConfirmLogin, closeModal: closeModalConfirmLogin, openModal: openModalConfirmLogin } = useModalCommon({ initialValue: false })
|
||||
const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false })
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
firstNameRef.current?.focus()
|
||||
}, 500);
|
||||
}, [activeStep])
|
||||
|
||||
const handleSubmit = (values: { firstName: string, lastName: string, emailAddress: string }) => {
|
||||
const { firstName, lastName, emailAddress } = values
|
||||
|
@@ -41,7 +41,7 @@ const provinceOptions = [
|
||||
|
||||
const ShippingInfoForm = ({ onConfirm, id, activeStep }: ShippingInfoFormProps) => {
|
||||
const addressRef = useRef<CustomInputCommon>(null)
|
||||
const { setOrderShippingAddress } = useSetOrderShippingAddress()
|
||||
const { setOrderShippingAddress, loading } = useSetOrderShippingAddress()
|
||||
const { showMessageError } = useMessage()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -171,8 +171,7 @@ const ShippingInfoForm = ({ onConfirm, id, activeStep }: ShippingInfoFormProps)
|
||||
</div>
|
||||
<div className={s.bottom}>
|
||||
<ChekoutNotePolicy />
|
||||
{/* <ButtonCommon HTMLType='submit' loading={loading} size="large"> */}
|
||||
<ButtonCommon HTMLType='submit' size="large">
|
||||
<ButtonCommon HTMLType='submit' loading={loading} size="large">
|
||||
Continue to Payment
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
|
@@ -46,10 +46,7 @@
|
||||
color:var(--text-base);
|
||||
}
|
||||
}
|
||||
button{
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,8 @@ export const LANGUAGE = {
|
||||
CONFIRM:'Confirm',
|
||||
ADD_TO_CARD: 'Add to Cart',
|
||||
PREORDER: 'Pre-Order Now',
|
||||
SIGNIN :'Sign In'
|
||||
SIGNIN :'Sign In',
|
||||
CANCEL: 'Cancel',
|
||||
},
|
||||
PLACE_HOLDER: {
|
||||
SEARCH: 'Search',
|
||||
|
Reference in New Issue
Block a user