diff --git a/modules/checkout-spa/.env b/modules/checkout-spa/.env new file mode 100644 index 0000000..6e9c32c --- /dev/null +++ b/modules/checkout-spa/.env @@ -0,0 +1,2 @@ +DISABLE_ESLINT_PLUGIN=true +SKIP_PREFLIGHT_CHECK=true diff --git a/modules/checkout-spa/README.markdown b/modules/checkout-spa/README.markdown new file mode 100644 index 0000000..4e98a65 --- /dev/null +++ b/modules/checkout-spa/README.markdown @@ -0,0 +1,28 @@ +

Checkout Single Page Application

+

This CX is an implementation of Checkout journey using CX, Headless API and React.

+
+

Libraries Used

+ +
+

Setup Requirement

+
    +
  1. Create a commerce channel
  2. +
  3. Create a picklist having name as any but its erc should be :- commerceChannelPicklist
  4. +
  5. Set erc of the commerce channel to any and make an entry inside the created picklist with the same erc used in commerce channel. Name, Key and ERC of the picklist entry should be same as commerce channel erc
  6. +
  7. Preparation of Checkout page follows the same strategy of Liferay. Just instead of Liferay checkout, deploy this client-extension
  8. +
  9. Checkout button will redirect to this custom cx.
  10. +
+
+

Client Extension Details

+ +
+

Please feel free to reach out for any setup queries.

\ No newline at end of file diff --git a/modules/checkout-spa/client-extension.yaml b/modules/checkout-spa/client-extension.yaml new file mode 100644 index 0000000..6116c57 --- /dev/null +++ b/modules/checkout-spa/client-extension.yaml @@ -0,0 +1,15 @@ +assemble: + - from: build/static + into: static +checkout-spa: + cssURLs: + - css/main.*.css + friendlyURLMapping: checkout-spa + htmlElementName: checkout-spa + instanceable: false + name: Checkout Spa + portletCategoryName: category.client-extensions + type: customElement + urls: + - js/main.*.js + useESM: true \ No newline at end of file diff --git a/modules/checkout-spa/package.json b/modules/checkout-spa/package.json new file mode 100644 index 0000000..2a6188e --- /dev/null +++ b/modules/checkout-spa/package.json @@ -0,0 +1,40 @@ +{ + "name": "checkout-spa", + "version": "0.1.0", + "private": true, + "dependencies": { + "@clayui/alert": "^3.111.0", + "@clayui/button": "^3.116.0", + "@clayui/form": "^3.119.0", + "jquery": "^3.7.1", + "jquery.cookie": "^1.4.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-scripts": "5.0.1", + "react-spinners": "^0.14.1", + "sass": "^1.79.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/modules/checkout-spa/public/index.html b/modules/checkout-spa/public/index.html new file mode 100644 index 0000000..3ec403f --- /dev/null +++ b/modules/checkout-spa/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + + + + + diff --git a/modules/checkout-spa/src/App.js b/modules/checkout-spa/src/App.js new file mode 100644 index 0000000..ad88ef1 --- /dev/null +++ b/modules/checkout-spa/src/App.js @@ -0,0 +1,44 @@ +import React from "react"; +import { CheckoutForm } from "./common/form/CheckoutForm"; +import { findCommerceOrderCookie } from "./common/cookies/cookies"; +import { getTranslation } from "./helper/Translation"; +import { ItemDetails } from "./common/sections/ItemDetails/ItemDetails"; + +export function App() { + + const [orderPresent, setOrderPresent] = React.useState(true); + const intervalRef = React.useRef(null); + + React.useEffect(() => { + intervalRef.current = setInterval(() => { + const orderUUID = findCommerceOrderCookie(); + if (orderUUID === null || orderUUID === undefined) { + setOrderPresent(false); + } + }, 2000); + + return () => clearInterval(intervalRef.current); + }, []); + + const handleSaveBtnEvent = () => { + clearInterval(intervalRef.current); + } + + if (orderPresent) { + return ( +
+ + +
+ ); + } else { + return ( +
+

{getTranslation("NoOrderFoundHeading", 'en')}

+
+ {getTranslation("NoOrderFoundBody", 'en')} +
+
+ ); + } +} diff --git a/modules/checkout-spa/src/common/buttons/AddAddressBtn.js b/modules/checkout-spa/src/common/buttons/AddAddressBtn.js new file mode 100644 index 0000000..fdb1df0 --- /dev/null +++ b/modules/checkout-spa/src/common/buttons/AddAddressBtn.js @@ -0,0 +1,17 @@ +import React from "react"; +import ClayButton from '@clayui/button'; +import { getTranslation } from "../../helper/Translation"; + +export function AddAddressBtn({onClick}){ + + return( +
+ + + {getTranslation("AddNewAddress", 'en')} + + +
+ ) + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/constants/Constants.js b/modules/checkout-spa/src/common/constants/Constants.js new file mode 100644 index 0000000..9f2e8a4 --- /dev/null +++ b/modules/checkout-spa/src/common/constants/Constants.js @@ -0,0 +1,11 @@ +const Constants = { + "PICKLIST_DEF_ERC":"commerceChannelPicklist", + "BILLING_ADDRESS_HEADING":"Billing Address", + "SHIPPING_ADDRESS_HEADING":"Shipping Address", + "NO_CHANNEL_EXISTS":"No Channel Found !!!", + "COMMERCE_ORDER_COOKIE":"com.liferay.commerce.model.CommerceOrder#", + "BILLING":"Billing", + "SHIPPING":"Shipping" +} + +export default Constants; \ No newline at end of file diff --git a/modules/checkout-spa/src/common/constants/Language.js b/modules/checkout-spa/src/common/constants/Language.js new file mode 100644 index 0000000..5c80166 --- /dev/null +++ b/modules/checkout-spa/src/common/constants/Language.js @@ -0,0 +1,30 @@ +export const Languages = { + en : { + BillingAddress : "Billing Address", + ShippingAddress : "Shipping Address", + AddNewAddress : "Add new address", + Submit : "Save", + SelectAddress : "Select Address", + FirstName : "First Name", + LastName : "Last Name", + PhoneNumber : "Phone Number", + City : "City", + PostalCode : "Postal Code", + Address1 : "Address Line 1", + Address2 : "Addresss Line 2", + Address3 : "Address Line 3", + GoToHome : "Go To HomePage", + ThankYou : "Thank you for placing the order with us", + NoOrderFoundHeading : "No Open Commerce Order Found !!!", + NoOrderFoundBody : "Please add atleast 1 item to the cart to proceed", + ItemName : "Prodcut Name", + ItemCode : "Product Code", + Quantity : "Quantity", + PerUnit : "/unit", + Shipping : "Shipping", + Tax : "Tax", + SubTotal : "Sub-Total", + OrderTotal : "Order-Total" + } + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/cookies/cookies.js b/modules/checkout-spa/src/common/cookies/cookies.js new file mode 100644 index 0000000..3b00164 --- /dev/null +++ b/modules/checkout-spa/src/common/cookies/cookies.js @@ -0,0 +1,36 @@ +import Constants from "../constants/Constants"; + +export function findCommerceOrderCookie(){ + const searchString = Constants.COMMERCE_ORDER_COOKIE; + const allCookies = document.cookie.split(';'); + + for (let cookie of allCookies) { + cookie = cookie.trim(); + + if (cookie.startsWith(searchString)) { + return cookie.substring(cookie.indexOf('=') + 1); + } + } + + return null; +} + +function findCookieByName(name){ + const searchString = Constants.COMMERCE_ORDER_COOKIE; + const allCookies = document.cookie.split(';'); + + for (let cookie of allCookies) { + cookie = cookie.trim(); + + if (cookie.startsWith(searchString)) { + return cookie; + } + } + + return null; +} + +export function eraseCookie(name) { + + document.cookie = findCookieByName(name) + '=; Max-Age=0; path=/;'; +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/createData/callOrderPlacedApi.js b/modules/checkout-spa/src/common/createData/callOrderPlacedApi.js new file mode 100644 index 0000000..d33f747 --- /dev/null +++ b/modules/checkout-spa/src/common/createData/callOrderPlacedApi.js @@ -0,0 +1,13 @@ +import { patchUpdatedAccountData } from "../../helper/CommerceAccount"; +import { patchCommerceOrder } from "../../helper/Order"; +import { getTranslation } from "../../helper/Translation"; +import BounceLoader from "react-spinners/ClipLoader"; + + +export function callOrderPlacedApi(billingAddressId, shippingAddressId, accountId, accountName){ + + patchUpdatedAccountData(accountId, accountName, billingAddressId, shippingAddressId); + + patchCommerceOrder(billingAddressId, shippingAddressId); + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/createData/postAddress.js b/modules/checkout-spa/src/common/createData/postAddress.js new file mode 100644 index 0000000..34c629b --- /dev/null +++ b/modules/checkout-spa/src/common/createData/postAddress.js @@ -0,0 +1,41 @@ +import baseFetch from "../services/liferay/api"; + +export function postAddress(formData){ + + console.log("formData from address form is "+JSON.stringify(formData)); + + let postAddressUrl = `/o/headless-admin-user/v1.0/accounts/${formData.accountId}/postal-addresses`; + + const jsonData = { + "addressCountry": "India", + "addressLocality": formData.city, + "addressRegion": "Delhi", + "addressType": formData.type.toLowerCase(), + "name": formData.firstName + formData.lastName, + "phoneNumber": formData.phoneNumber, + "postalCode": formData.zip, + "primary": false, + "streetAddressLine1": formData.address1, + "streetAddressLine2": formData.address2, + "streetAddressLine3": formData.address3 + } + + + console.log("jsonData for address before being posted is "+JSON.stringify(jsonData)); + async function postData(){ + await baseFetch(postAddressUrl,{ + method: 'POST', + body: JSON.stringify(jsonData) + }).then((res)=>{ + if (!res.ok){ + throw new Error("Exception while posting address"); + } + return res.json(); + }).then((data) => { + console.log(data); + }); + } + + postData(); + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/form/AddAddress.js b/modules/checkout-spa/src/common/form/AddAddress.js new file mode 100644 index 0000000..cd42c31 --- /dev/null +++ b/modules/checkout-spa/src/common/form/AddAddress.js @@ -0,0 +1,147 @@ +import React from "react"; +import Header from "../headers/Header"; +import BounceLoader from "react-spinners/ClipLoader"; +import { postAddress } from "../createData/postAddress"; +import { getTranslation } from "../../helper/Translation"; + + +export function AddAddress({ isOpen, onClose, addressType, accountId }) { + + const [formData, setFormData] = React.useState({ + accountId:'', + type:'', + firstName: '', + lastName: '', + phoneNumber: '', + address1: '', + address2: '', + address3: '', + city: '', + zip: '', + }); + + // Handle input change + const handleChange = (e) => { + const { id, value } = e.target; + setFormData((prevData) => ({ + ...prevData, + type: addressType, + accountId: accountId, + [id]: value, + })); + }; + + // Handle form submission + const submitAddressForm = (e) => { + e.preventDefault(); // Prevent the default form submission + console.log('Form Data:', formData); + document.getElementById("formmodalid").style.display="none"; + document.getElementById("bounceLoaderId").classList.remove("hide"); + postAddress(formData); + setTimeout(()=>{ + location.reload(); + },2000); + }; + + return ( +
+
+
+
+
+
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {/*
+ +
+ +
+ + +
+ +
+ + +
*/} + +
+ + +
+ +
+ + +
+ + +
+
+
+ +
+
+
+
+ +
+ ) + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/form/CheckoutForm.js b/modules/checkout-spa/src/common/form/CheckoutForm.js new file mode 100644 index 0000000..2d5bc17 --- /dev/null +++ b/modules/checkout-spa/src/common/form/CheckoutForm.js @@ -0,0 +1,105 @@ +import React from "react"; +import { BillingSection } from "../sections/Billing/BillingSection"; +import { ShippingSection } from "../sections/Shipping/ShippingSection"; +import { fetchBillingAddresses } from "../../helper/Billing"; +import { fetchShippingAddresses } from "../../helper/Shipping"; +import fetchCommerceAccount from "../../helper/CommerceAccount"; +import '../styles/formcontainer.css'; +import { getTranslation } from "../../helper/Translation"; +import { callOrderPlacedApi } from "../createData/callOrderPlacedApi"; +import ClayAlert from '@clayui/alert'; + +export function CheckoutForm({onSaveSuccess}){ + + const {billingAddress} = fetchBillingAddresses(); + + const {shippingAddress} = fetchShippingAddresses(); + + const {account} = fetchCommerceAccount(); + + const [selectedBillingAddress, setSelectedBillingAddress] = React.useState(null); + const [selectedShippingAddress, setSelectedShippingAddress] = React.useState(null); + + const [isLoading, setIsLoading] = React.useState(null); + + const handleBillingAddressChange = (value) => { + setSelectedBillingAddress(value); + console.log(`Selected Billing Address ID: ${value}`); + }; + + const handleShippingAddressChange = (value) => { + setSelectedShippingAddress(value); + console.log(`Selected Shipping Address ID: ${value}`); + }; + + const handleSubmit = async (e) => { + + e.preventDefault(); + + if (selectedBillingAddress !== null && selectedShippingAddress !== null){ + try { + setIsLoading(true); + + console.log('Selected Billing Address ID:'+ selectedBillingAddress); + console.log('Selected Shipping Address ID:'+ selectedShippingAddress); + callOrderPlacedApi(selectedBillingAddress, selectedShippingAddress, account.id, account.name); + + } catch (error) { + console.error("Exception while calling checkout api ",error); + } finally{ + setIsLoading(false); + onSaveSuccess(); + document.getElementById("confirmorderformid").style.display="none"; + document.getElementById("thankyousectionid").style.display="block"; + } + + } else{ + document.getElementById("warningMsg").classList.remove("hide"); + } + + }; + + const isDataLoaded = billingAddress && shippingAddress && account; + + return ( +
+ + {/* + Warning UI when proceeding without both addresses. + */} +
+ + Billing and Shipping Address are mandatory. + +
+ +
+ {isDataLoaded && ( + <> + + +
+ + + +
+ + + + )} + +
+ {isLoading ? ( + + ) : ( +
+
+ {getTranslation("ThankYou",'en')} +
+ {getTranslation("GoToHome", 'en')} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/headers/Header.jsx b/modules/checkout-spa/src/common/headers/Header.jsx new file mode 100644 index 0000000..5e8d119 --- /dev/null +++ b/modules/checkout-spa/src/common/headers/Header.jsx @@ -0,0 +1,9 @@ +import React from "react"; + +function Header (props){ + return( +

{props.name}

+ ) +} + +export default Header; \ No newline at end of file diff --git a/modules/checkout-spa/src/common/sections/Billing/BillingSection.js b/modules/checkout-spa/src/common/sections/Billing/BillingSection.js new file mode 100644 index 0000000..2b32231 --- /dev/null +++ b/modules/checkout-spa/src/common/sections/Billing/BillingSection.js @@ -0,0 +1,56 @@ +import React from "react"; +import Header from "../../headers/Header"; +import Constants from "../../constants/Constants"; +import BounceLoader from "react-spinners/ClipLoader"; +import { AddAddressBtn } from "../../buttons/AddAddressBtn"; +import { AddAddress } from "../../form/AddAddress"; +import { SelectTag } from "../../tags/SelectTag"; + +export function BillingSection({billingAddress, accountId, onBillingAddressChange}) { + + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const [addressType, setAddressType] = React.useState(null); + + const handleOnAddAddressBtnClick = () => { + console.log('Add Address Button clicked'); + setIsModalOpen(true); // Open the modal + setAddressType(Constants.BILLING); + }; + + const handleOnCloseModal = () => { + setIsModalOpen(false); // Close the modal + }; + + if (billingAddress == null){ + return( +
+ +
+ ) + } + + if (billingAddress.items.length > 0){ + return( +
+
+ onBillingAddressChange(event.target.value)} + /> + + +
+ ) + } else { + return( +
+
+ + +
+ ) + } + +} diff --git a/modules/checkout-spa/src/common/sections/ItemDetails/ItemDetails.js b/modules/checkout-spa/src/common/sections/ItemDetails/ItemDetails.js new file mode 100644 index 0000000..20e432c --- /dev/null +++ b/modules/checkout-spa/src/common/sections/ItemDetails/ItemDetails.js @@ -0,0 +1,56 @@ +import React from "react"; +import { CartItem } from "../../../helper/CartItem"; +import { getTranslation } from "../../../helper/Translation"; +import BounceLoader from "react-spinners/ClipLoader"; +import { fetchOrderByCookie } from "../../../helper/Order"; +import '../../styles/tablecontainer.css'; + + +export function ItemDetails(){ + + const {cartItems} = CartItem(); + + const {cart} = fetchOrderByCookie(); + + if (!cartItems || cartItems == null || cart == null){ + return ( +
+ +
+ ) + } + + if (cartItems !== null){ + return ( + +
+ + + + + + + + + + + {cartItems.items.map((item) =>( + + + + + + + ))} + +
{getTranslation("ItemName", "end")}{getTranslation("ItemCode", "en")}{getTranslation("Quantity", "en")}{getTranslation("UnitPrice", "en")}
{item.name}{item.sku}{item.quantity}{item.price.price}
+
+

{getTranslation("SubTotal","en")} : {cart.summary.subTotalFormatted}

+

{getTranslation("Tax","en")} : {cart.summary.taxValue}

+

{getTranslation("OrderTotal", "en")} : {cart.summary.totalFormatted}

+
+
+ ) + } + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/sections/Shipping/ShippingSection.js b/modules/checkout-spa/src/common/sections/Shipping/ShippingSection.js new file mode 100644 index 0000000..96e3500 --- /dev/null +++ b/modules/checkout-spa/src/common/sections/Shipping/ShippingSection.js @@ -0,0 +1,58 @@ +import React from "react"; +import Header from "../../headers/Header"; +import Constants from "../../constants/Constants"; +import BounceLoader from "react-spinners/ClipLoader"; +import { AddAddressBtn } from "../../buttons/AddAddressBtn"; +import { AddAddress } from "../../form/AddAddress"; +import { SelectTag } from "../../tags/SelectTag"; + +export function ShippingSection({shippingAddressObj, accountId, onShippingAddressChange}){ + + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const [addressType, setAddressType] = React.useState(null); + + const handleOnAddAddressBtnClick = () => { + console.log('Add Address Button clicked'); + setIsModalOpen(true); // Open the modal + setAddressType(Constants.SHIPPING); + }; + + const handleOnCloseModal = () => { + setIsModalOpen(false); // Close the modal + }; + + + if (shippingAddressObj == null){ + return( +
+ +
+ ) + } + + if (shippingAddressObj.items.length > 0){ + return( +
+
+ onShippingAddressChange(event.target.value)} + /> + + +
+ ) + } else { + return( +
+
+ + +
+ ) + } + + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/services/liferay/api.js b/modules/checkout-spa/src/common/services/liferay/api.js new file mode 100644 index 0000000..760afd1 --- /dev/null +++ b/modules/checkout-spa/src/common/services/liferay/api.js @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +import {Liferay} from './liferay.js'; + +const {REACT_APP_LIFERAY_HOST = window.location.origin} = process.env; + +const baseFetch = async (url, options = {}) => { + return fetch(REACT_APP_LIFERAY_HOST + '/' + url, { + headers: { + 'Content-Type': 'application/json', + 'x-csrf-token': Liferay.authToken, + }, + ...options, + }); +}; + +export default baseFetch; diff --git a/modules/checkout-spa/src/common/services/liferay/liferay.js b/modules/checkout-spa/src/common/services/liferay/liferay.js new file mode 100644 index 0000000..c2bcc2c --- /dev/null +++ b/modules/checkout-spa/src/common/services/liferay/liferay.js @@ -0,0 +1,32 @@ +/** + * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +export const Liferay = window.Liferay || { + OAuth2: { + getAuthorizeURL: () => '', + getBuiltInRedirectURL: () => '', + getIntrospectURL: () => '', + getTokenURL: () => '', + getUserAgentApplication: (_serviceName) => {}, + }, + OAuth2Client: { + FromParameters: (_options) => { + return {}; + }, + FromUserAgentApplication: (_userAgentApplicationId) => { + return {}; + }, + fetch: (_url, _options = {}) => {}, + }, + ThemeDisplay: { + getCompanyGroupId: () => 0, + getScopeGroupId: () => 0, + getSiteGroupId: () => 0, + isSignedIn: () => { + return false; + }, + }, + authToken: '', +}; diff --git a/modules/checkout-spa/src/common/styles/formcontainer.css b/modules/checkout-spa/src/common/styles/formcontainer.css new file mode 100644 index 0000000..38942f1 --- /dev/null +++ b/modules/checkout-spa/src/common/styles/formcontainer.css @@ -0,0 +1,11 @@ +.form-container { + border: 2px solid black; + padding: 20px; + border-radius: 8px; + margin: 20px 0; +} + +.hidden-input { + position: absolute; + left: -9999px; +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/styles/tablecontainer.css b/modules/checkout-spa/src/common/styles/tablecontainer.css new file mode 100644 index 0000000..dd1af5e --- /dev/null +++ b/modules/checkout-spa/src/common/styles/tablecontainer.css @@ -0,0 +1,10 @@ +.table-border{ + border : 5px double rgb(59, 54, 54); +} + +.price-detail{ + border : 5px double rgb(59, 54, 54); + text-align: left; + padding-left: 10px; + padding-top: 10px; +} \ No newline at end of file diff --git a/modules/checkout-spa/src/common/tags/SelectTag.jsx b/modules/checkout-spa/src/common/tags/SelectTag.jsx new file mode 100644 index 0000000..25774fa --- /dev/null +++ b/modules/checkout-spa/src/common/tags/SelectTag.jsx @@ -0,0 +1,53 @@ +import React from "react"; +import BounceLoader from "react-spinners/ClipLoader"; +import Constants from "../constants/Constants"; +import { getTranslation } from "../../helper/Translation"; + + +export function SelectTag({obj,type,onChange}){ + console.log("Obj is "+JSON.stringify(obj)); + + let typeOfAddress; + if (type === "Shipping"){ + + typeOfAddress = Constants.SHIPPING.toLowerCase(); + } else { + + typeOfAddress = Constants.BILLING.toLowerCase(); + } + + + + if (obj == null){ + return ( +
+ +
+ ) + } + else { + obj.items.forEach(item => { + console.log(`Item ID: ${item.id}, AddressType: ${item.addressType}`); + }); + return( +
+ +
+ ) + } +} \ No newline at end of file diff --git a/modules/checkout-spa/src/error/GeneralError.jsx b/modules/checkout-spa/src/error/GeneralError.jsx new file mode 100644 index 0000000..bc60ed4 --- /dev/null +++ b/modules/checkout-spa/src/error/GeneralError.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import { Liferay } from "../common/services/liferay/liferay"; + + +const GeneralError = () => { + + function goToHomePage(){ + window.location.href=`${window.location.origin}` + } + + return( +
+

${Liferay.Language.get("general-error")}

+
+ +
+
+ ) +} + +export default GeneralError; \ No newline at end of file diff --git a/modules/checkout-spa/src/error/NoChannelExist.jsx b/modules/checkout-spa/src/error/NoChannelExist.jsx new file mode 100644 index 0000000..24e64ac --- /dev/null +++ b/modules/checkout-spa/src/error/NoChannelExist.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import Header from "../common/headers/Header"; +import Constants from "../common/constants/Constants"; + +const NoChannelExist = () => { + return( +
+
+
Please Create A Channel To Proceed
+
+ ) +} + +export default NoChannelExist; \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/Billing.js b/modules/checkout-spa/src/helper/Billing.js new file mode 100644 index 0000000..0e97601 --- /dev/null +++ b/modules/checkout-spa/src/helper/Billing.js @@ -0,0 +1,49 @@ +import React from "react"; +import baseFetch from "../common/services/liferay/api"; +import NoChannelExist from "../error/NoChannelExist"; +import { fetchOrderByCookie } from "./Order"; +import Constants from "../common/constants/Constants"; + +export function fetchBillingAddresses () { + + const {cart} = fetchOrderByCookie(); + + const [billingAddress, setBillingAddress] = React.useState(); + const [accountId, setAccountId] = React.useState(); + + React.useEffect(() => { + async function fetchData () { + try{ + const billingAddressReqCall = await baseFetch(`/o/headless-admin-user/v1.0/accounts/${cart.accountId}/postal-addresses`); + + if (!billingAddressReqCall.ok){ + throw new Error("Exception while fetching data from Api"); + } + + const billingAddressResCall = await billingAddressReqCall.json(); + + // Filter the items to only include those with addressType "billing" + const filteredBillingAddresses = billingAddressResCall.items.filter( + item => item.addressType === Constants.BILLING.toLowerCase() + ); + + // Create a new object with the filtered items + const filteredBillingAddressObj = { + ...billingAddressResCall, // Copy other properties from the original object if needed + items: filteredBillingAddresses // Assign the filtered array to the items property + }; + + setBillingAddress(filteredBillingAddressObj); + + setAccountId(cart.accountId); + }catch(err){ + console.error("Exception while fetching data from Billing Address api ") + } + } + fetchData(); + }, [cart]); + + + return {billingAddress, accountId}; + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/CartItem.js b/modules/checkout-spa/src/helper/CartItem.js new file mode 100644 index 0000000..cd4a19c --- /dev/null +++ b/modules/checkout-spa/src/helper/CartItem.js @@ -0,0 +1,50 @@ +import React from "react"; +import { fetchOrderByCookie } from "./Order"; +import { findCommerceOrderCookie } from "../common/cookies/cookies"; +import baseFetch from "../common/services/liferay/api"; + +export function CartItem(){ + + const [orderPresent, setOrderPresent] = React.useState(true); + React.useEffect(() => { + const interval = setInterval(() => { + const orderUUID = findCommerceOrderCookie(); + if (orderUUID === null || orderUUID === undefined) { + console.log("No order UUID found in cookie"); + setOrderPresent(false); + } + console.log("Order UUID is present"); + }, 2000); + + return () => clearInterval(interval); + }, []); + + + const [cartItems, setCartItems] = React.useState(null); + + const {cart} = fetchOrderByCookie(); + + React.useEffect(() => { + async function fetchData () { + try{ + const cartItemReqCall = await baseFetch(`/o/headless-commerce-delivery-cart/v1.0/carts/${cart.id}/items`); + + if (!cartItemReqCall.ok){ + throw new Error("Exception while fetching data from Cart Item Api"); + } + + const cartItemResCall = await cartItemReqCall.json(); + + setCartItems(cartItemResCall); + }catch(err){ + console.error("Exception while fetching data from Cart Item api "+err); + } + } + fetchData(); + }, [cart]) + + console.log("Cart Item obj is "+cartItems); + + return {cartItems}; + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/Channel.js b/modules/checkout-spa/src/helper/Channel.js new file mode 100644 index 0000000..263a303 --- /dev/null +++ b/modules/checkout-spa/src/helper/Channel.js @@ -0,0 +1,38 @@ +import React from "react"; +import baseFetch from "../common/services/liferay/api"; +import Constants from "../common/constants/Constants"; + +export function fetchChannelObjByListTypeEntry() { + const [channelId, setChannelId] = React.useState(null); + + + React.useEffect(() => { + + async function fetchData() { + try { + + const listTypeDefReqCall = await baseFetch(`/o/headless-admin-list-type/v1.0/list-type-definitions/by-external-reference-code/${Constants.PICKLIST_DEF_ERC}`); + const listTypeDefResCall = await listTypeDefReqCall.json(); + + console.log("Channel Picklist value "+listTypeDefResCall) + const listTypeEntryReqCall = await baseFetch(`/o/headless-admin-list-type/v1.0/list-type-definitions/${listTypeDefResCall.id}/list-type-entries`); + const listTypeEntryResCall = await listTypeEntryReqCall.json(); + + const channelReqCall = await baseFetch(`/o/headless-commerce-admin-channel/v1.0/channels/by-externalReferenceCode/${listTypeEntryResCall.items[0].externalReferenceCode}`); + const channelResCall = await channelReqCall.json(); + console.log("Channel obj received is "+channelResCall); + setChannelId(channelResCall); + + + + } catch (err) { + throw new Error("Exception while calling fetching"); + } + } + + fetchData(); + + }, []); + console.log(channelId); + return { channelId }; +} \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/CommerceAccount.js b/modules/checkout-spa/src/helper/CommerceAccount.js new file mode 100644 index 0000000..b5679be --- /dev/null +++ b/modules/checkout-spa/src/helper/CommerceAccount.js @@ -0,0 +1,68 @@ +import React from "react"; +import baseFetch from "../common/services/liferay/api"; +import { fetchOrderByCookie } from "./Order"; + + +export default function fetchCommerceAccount(){ + + const [account, setAccount] = React.useState(''); + + const {cart} = fetchOrderByCookie(); + + React.useEffect(() => { + + async function fetchData(){ + try { + + const commerceAccountReqCall = await baseFetch(`/o/headless-admin-user/v1.0/accounts/${cart.accountId}`); + + if (!commerceAccountReqCall.ok){ + throw new Error("Exception while fetching data"); + } + + const commerceAccountResCall = await commerceAccountReqCall.json(); + + setAccount(commerceAccountResCall); + + } catch (error) { + + console.error("Exception while fetching data"); + + } + } + fetchData(); + }, [cart]); + + return {account}; + +} + +export function patchUpdatedAccountData(accountId, accountName, billingAddressId, shippingAddressId){ + + console.log("inside patchUpdatedAccountData"); + + const postAddressUrl = `/o/headless-admin-user/v1.0/accounts/${accountId}`; + + const jsonData = { + "defaultBillingAddressId" : billingAddressId, + "defaultShippingAddressId" : shippingAddressId, + "name":`${accountName}` + }; + + async function postData(){ + await baseFetch(postAddressUrl,{ + method: 'PATCH', + body: JSON.stringify(jsonData) + }).then((res)=>{ + if (!res.ok){ + throw new Error("Exception while posting address"); + } + return res.json(); + }).then((data) => { + console.log(data); + }); + } + + postData(); + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/Country.js b/modules/checkout-spa/src/helper/Country.js new file mode 100644 index 0000000..fafbdac --- /dev/null +++ b/modules/checkout-spa/src/helper/Country.js @@ -0,0 +1,75 @@ +import React from "react"; +import baseFetch from "../common/services/liferay/api"; + +export function getCountryListings(){ + + const [country, setCountry] = React.useState(null); + + + React.useEffect(() => { + + async function fetchData(){ + try { + + const countryReqCall = await baseFetch(`/o/headless-admin-address/v1.0/countries`); + + if (!countryReqCall.ok){ + throw new Error("Exception while fetching data"); + } + + const countryResCall = await countryReqCall.json(); + + if (Array.isArray(countryResCall)) { + // This is safe to iterate over + setCountry(countryResCall.items); + } else { + // Handle case where the response is not iterable + console.log("Response is not an array: ", countryResCall); + } + + + + } catch (error) { + console.error("Exception while fetching data"); + } + } + fetchData(); + }, []); + + console.log("Country obj is "+country); + + return {country}; + +} + +export function getCountryByName(name){ +console.log("name sent is :: "+name); + const [country, setCountry] = React.useState(null); + + + React.useEffect(() => { + + async function fetchData(){ + try { + + const countryReqCall = await baseFetch(`/o/headless-admin-address/v1.0/countries/by-name/${name}`); + + if (!countryReqCall.ok){ + throw new Error("Exception while fetching data"); + } + + const countryResCall = await countryReqCall.json(); + setCountry(countryResCall.items) + + } catch (error) { + console.error("Exception while fetching data"); + } + } + fetchData(); + }, []); + + console.log("Country obj is "+country); + + return {country}; + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/Order.js b/modules/checkout-spa/src/helper/Order.js new file mode 100644 index 0000000..5b016d1 --- /dev/null +++ b/modules/checkout-spa/src/helper/Order.js @@ -0,0 +1,98 @@ +import React from "react"; +import { eraseCookie, findCommerceOrderCookie } from "../common/cookies/cookies"; +import baseFetch from "../common/services/liferay/api"; +import Constants from "../common/constants/Constants"; + +export function fetchOrderByCookie(){ + + const [cart, setCart] = React.useState(null); + + const orderUUID = findCommerceOrderCookie(); + console.log("ORDER UUID is "+orderUUID); + + React.useEffect(()=> { + + async function fetchData(){ + try{ + + const orderObjReqCall = await baseFetch(`/o/headless-commerce-admin-order/v1.0/orders/by-externalReferenceCode/${orderUUID}`); + const orderObjResCall = await orderObjReqCall.json(); + + if (orderObjResCall === null){ + throw new Error("Exception while fetching order data"); + } + + const cartReqCall = await baseFetch(`/o/headless-commerce-delivery-cart/v1.0/carts/${orderObjResCall.id}`); + const cartResCall = await cartReqCall.json(); + + setCart(cartResCall); + + console.log("Cart Response is "+cart); + }catch(err){ + throw new Error("Exception while fetching the data"); + } + } + + fetchData(); + + }, []); + + return {cart}; +} + + +export function patchCommerceOrder(billingAddressId, shippingAddressId){ + + console.log("Inside patchCommerceOrder"); + + const orderUUID = findCommerceOrderCookie(); + + console.log("commerce order uuid is "+orderUUID); + + const jsonData = { + "billingAddressId" : billingAddressId, + "shippingAddressId" : shippingAddressId + }; + + async function patchData(){ + await baseFetch(`/o/headless-commerce-admin-order/v1.0/orders/by-externalReferenceCode/${orderUUID}` + ).then((orderObjData)=>{ + console.log("orderObjData is "+JSON.stringify(orderObjData)); + if (!orderObjData.ok){ + throw new Error("Exception while fetching commerce order object"); + } + return orderObjData.json(); + }).then(async (data) =>{ + await baseFetch(`/o/headless-commerce-delivery-cart/v1.0/carts/${data.id}`, { + method : 'PATCH', + body : JSON.stringify(jsonData) + }).then((res) => { + console.log("res is "+JSON.stringify(res)) + if (!res.ok){ + throw new Error("Exception while updating commerce order"); + } + return res.json(); + }).then(async (data) => { + console.log("updated commerce address"); + + await baseFetch(`/o/headless-commerce-delivery-cart/v1.0/carts/${data.id}/checkout`, { + method : 'POST' + }).then(res => { + if (!res.ok){ + throw new Error("Exception while calling checkout api"); + } + return res.json(); + }).then((data) => { + console.log(data); + + }); + + }) + }) + } + + + patchData(); + + eraseCookie(Constants.COMMERCE_ORDER_COOKIE); +} \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/Region.js b/modules/checkout-spa/src/helper/Region.js new file mode 100644 index 0000000..9a47631 --- /dev/null +++ b/modules/checkout-spa/src/helper/Region.js @@ -0,0 +1,44 @@ +import React from "react"; +import baseFetch from "../common/services/liferay/api"; + +export function getRegion({countryId}){ + + const [regions, setRegions] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + + async function fetchData(){ + if (countryId){ + try { + + const regionReqCall = await baseFetch(`/o/headless-admin-address/v1.0/countries/${countryId}/regions`); + + if (!regionReqCall.ok){ + throw new Error("Exception while fetching data"); + } + + const regionResCall = await regionReqCall.json(); + + setRegions(regionResCall); + + } catch (error) { + setError("Exception while fetching data"); + } finally{ + setLoading(false); + } + } + else { + setRegions([]); + setLoading(false); + } +}; + fetchData(); + }, [countryId]); + + return {regions}; + +} + + diff --git a/modules/checkout-spa/src/helper/Shipping.js b/modules/checkout-spa/src/helper/Shipping.js new file mode 100644 index 0000000..d3f8eae --- /dev/null +++ b/modules/checkout-spa/src/helper/Shipping.js @@ -0,0 +1,49 @@ +import React from "react"; +import { fetchOrderByCookie } from "./Order"; +import baseFetch from "../common/services/liferay/api"; +import Constants from "../common/constants/Constants"; + +export function fetchShippingAddresses(){ + + const {cart} = fetchOrderByCookie(); + + const [shippingAddress, setShippingAddress] = React.useState(); + const [accountId, setAccountId] = React.useState(); + + React.useEffect(() => { + async function fetchData () { + try{ + const shippingAddressReqCall = await baseFetch(`/o/headless-admin-user/v1.0/accounts/${cart.accountId}/postal-addresses`); + + if (!shippingAddressReqCall.ok){ + throw new Error("Exception while fetching data from Api"); + } + + const shippingAddressResCall = await shippingAddressReqCall.json(); + + // Filter the items to only include those with addressType "shipping" + const filteredShippingAddresses = shippingAddressResCall.items.filter( + item => item.addressType === Constants.SHIPPING.toLowerCase() + ); + + // Create a new object with the filtered items + const filteredShippingAddressObj = { + ...shippingAddressResCall, // Copy other properties from the original object if needed + items: filteredShippingAddresses // Assign the filtered array to the items property + }; + + setShippingAddress(filteredShippingAddressObj); + + setAccountId(cart.accountId); + + }catch(err){ + console.error("Exception while fetching data from Billing Address api ") + } + } + fetchData(); + }, [cart]); + + + return {shippingAddress, accountId}; + +} \ No newline at end of file diff --git a/modules/checkout-spa/src/helper/Translation.js b/modules/checkout-spa/src/helper/Translation.js new file mode 100644 index 0000000..92fe29c --- /dev/null +++ b/modules/checkout-spa/src/helper/Translation.js @@ -0,0 +1,5 @@ +import { Languages } from "../common/constants/Language" + +export const getTranslation = (key, language) => { + return Languages[language]?.[key] || key; +}; \ No newline at end of file diff --git a/modules/checkout-spa/src/index.js b/modules/checkout-spa/src/index.js new file mode 100644 index 0000000..c046a37 --- /dev/null +++ b/modules/checkout-spa/src/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import {createRoot} from 'react-dom/client'; + +import { App } from './App'; + +let container = false; + +class WebComponent extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + if (!container){ + createRoot(this).render( + + ) + container = true; + } + } +} + + +const ELEMENT_ID = 'checkout-spa'; + +if (!customElements.get(ELEMENT_ID)) { + customElements.define(ELEMENT_ID, WebComponent); +}