@pradip1995/commerce-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +15 -0
  2. package/package.json +70 -0
  3. package/src/analytics/ga4-ecommerce.ts +96 -0
  4. package/src/config.ts +36 -0
  5. package/src/constants.tsx +84 -0
  6. package/src/context/modal-context.tsx +40 -0
  7. package/src/context/wishlist-context.tsx +96 -0
  8. package/src/data/cart/abandoned.ts +111 -0
  9. package/src/data/cart/buyNow.ts +184 -0
  10. package/src/data/cart/checkout.ts +487 -0
  11. package/src/data/cart/index.ts +7 -0
  12. package/src/data/cart/mutations.ts +189 -0
  13. package/src/data/cart/promotions.ts +121 -0
  14. package/src/data/cart/region.ts +66 -0
  15. package/src/data/cart/retrieve.ts +162 -0
  16. package/src/data/categories.ts +90 -0
  17. package/src/data/collections.ts +109 -0
  18. package/src/data/contact.ts +143 -0
  19. package/src/data/cookies.ts +170 -0
  20. package/src/data/customer-registration.ts +365 -0
  21. package/src/data/customer.ts +638 -0
  22. package/src/data/dynamic-config.ts +420 -0
  23. package/src/data/fulfillment.ts +95 -0
  24. package/src/data/guest.ts +357 -0
  25. package/src/data/locale-actions.ts +74 -0
  26. package/src/data/locales.ts +28 -0
  27. package/src/data/newsletter.ts +41 -0
  28. package/src/data/notifications.ts +22 -0
  29. package/src/data/onboarding.ts +9 -0
  30. package/src/data/orders.ts +500 -0
  31. package/src/data/payment-details.ts +68 -0
  32. package/src/data/payment.ts +32 -0
  33. package/src/data/products.ts +424 -0
  34. package/src/data/regions.ts +64 -0
  35. package/src/data/returns.ts +305 -0
  36. package/src/data/reviews.ts +279 -0
  37. package/src/data/swaps.ts +154 -0
  38. package/src/data/variants.ts +38 -0
  39. package/src/data/wishlist.ts +292 -0
  40. package/src/domain/cart/abandoned-carts.ts +49 -0
  41. package/src/domain/cart/buy-now.ts +15 -0
  42. package/src/domain/cart/checkout.ts +25 -0
  43. package/src/domain/cart/index.ts +8 -0
  44. package/src/domain/cart/metadata.ts +21 -0
  45. package/src/domain/cart/payment.ts +21 -0
  46. package/src/domain/cart/phone.ts +17 -0
  47. package/src/domain/cart/reorder.ts +19 -0
  48. package/src/domain/cart/validation.ts +43 -0
  49. package/src/domain/product/pricing.ts +49 -0
  50. package/src/domain/product/variant-selection.ts +193 -0
  51. package/src/firebase.ts +48 -0
  52. package/src/hooks/index.ts +8 -0
  53. package/src/hooks/use-add-to-cart.ts +63 -0
  54. package/src/hooks/use-cart.ts +132 -0
  55. package/src/hooks/use-checkout.ts +62 -0
  56. package/src/hooks/use-in-view.tsx +29 -0
  57. package/src/hooks/use-product-actions.ts +190 -0
  58. package/src/hooks/use-product-reviews.ts +18 -0
  59. package/src/hooks/use-product-variant.ts +142 -0
  60. package/src/hooks/use-server-action.ts +30 -0
  61. package/src/hooks/use-toggle-state.tsx +46 -0
  62. package/src/hooks/use-wishlist.ts +3 -0
  63. package/src/theme/inline-vars.ts +12 -0
  64. package/src/types/account.ts +21 -0
  65. package/src/types/cart.ts +13 -0
  66. package/src/types/home.ts +52 -0
  67. package/src/types/layout.ts +29 -0
  68. package/src/types/product-card.ts +17 -0
  69. package/src/util/compare-addresses.ts +28 -0
  70. package/src/util/env.ts +3 -0
  71. package/src/util/get-locale-header.ts +8 -0
  72. package/src/util/get-percentage-diff.ts +6 -0
  73. package/src/util/get-product-price.ts +78 -0
  74. package/src/util/google-oauth.ts +28 -0
  75. package/src/util/isEmpty.ts +11 -0
  76. package/src/util/medusa-error.ts +18 -0
  77. package/src/util/money.ts +26 -0
  78. package/src/util/order-status.tsx +179 -0
  79. package/src/util/product.ts +431 -0
  80. package/src/util/repeat.ts +5 -0
  81. package/src/util/returns.ts +71 -0
  82. package/src/util/sort-products.ts +48 -0
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @pradip1995/commerce-core
2
+
3
+ Medusa storefront commerce logic: server actions, domain rules, hooks, types.
4
+
5
+ ## Exports
6
+
7
+ ```ts
8
+ import { sdk } from "@pradip1995/commerce-core/config"
9
+ import { useAddToCart } from "@pradip1995/commerce-core/hooks/use-add-to-cart"
10
+ import type { HomePageData } from "@pradip1995/commerce-core/types/home"
11
+ ```
12
+
13
+ ## Peer dependencies
14
+
15
+ `next`, `react`, `@medusajs/js-sdk`, `@medusajs/types`
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@pradip1995/commerce-core",
3
+ "version": "1.0.0",
4
+ "description": "Medusa storefront commerce logic — data, domain, hooks, types, utils",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/SmartByteLabs/medusa-storefront-kit.git",
13
+ "directory": "packages/commerce-core"
14
+ },
15
+ "sideEffects": false,
16
+ "files": [
17
+ "src"
18
+ ],
19
+ "exports": {
20
+ "./config": "./src/config.ts",
21
+ "./constants": "./src/constants.tsx",
22
+ "./firebase": "./src/firebase.ts",
23
+ "./analytics/ga4-ecommerce": "./src/analytics/ga4-ecommerce.ts",
24
+ "./context/*": "./src/context/*.tsx",
25
+ "./data/*": "./src/data/*.ts",
26
+ "./data/cart": "./src/data/cart/index.ts",
27
+ "./domain/cart": "./src/domain/cart/index.ts",
28
+ "./domain/product/*": "./src/domain/product/*.ts",
29
+ "./hooks": "./src/hooks/index.ts",
30
+ "./hooks/*": "./src/hooks/*",
31
+ "./types/*": "./src/types/*.ts",
32
+ "./util/*": "./src/util/*.ts",
33
+ "./theme/inline-vars": "./src/theme/inline-vars.ts"
34
+ },
35
+ "peerDependencies": {
36
+ "@medusajs/js-sdk": ">=2",
37
+ "@medusajs/types": ">=2",
38
+ "next": ">=15",
39
+ "react": ">=19",
40
+ "react-dom": ">=19"
41
+ },
42
+ "dependencies": {
43
+ "country-state-city": "^3.2.1",
44
+ "firebase": "^12.8.0",
45
+ "lodash": "^4.17.21",
46
+ "pg": "^8.11.3",
47
+ "qs": "^6.12.1",
48
+ "server-only": "^0.0.1"
49
+ },
50
+ "devDependencies": {
51
+ "@medusajs/icons": "latest",
52
+ "@medusajs/js-sdk": "latest",
53
+ "@medusajs/types": "latest",
54
+ "@types/lodash": "^4.17.0",
55
+ "@types/node": "^20",
56
+ "@types/react": "^19",
57
+ "@types/react-dom": "^19",
58
+ "color": "^5.0.3",
59
+ "eslint": "^8.57.0",
60
+ "next": "15.3.8",
61
+ "react": "19.0.3",
62
+ "react-dom": "19.0.3",
63
+ "typescript": "^5.7.2"
64
+ },
65
+ "scripts": {
66
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
67
+ "typecheck:full": "tsc --noEmit",
68
+ "lint": "tsc --noEmit -p tsconfig.typecheck.json"
69
+ }
70
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * GA4 E-commerce event helpers for dataLayer.
3
+ * Use these to power GA4 "Drive sales" reports: Ecommerce purchases,
4
+ * Purchase journey, Checkout journey, Transactions, Promotions.
5
+ */
6
+
7
+ export type GA4Item = {
8
+ item_id: string
9
+ item_name: string
10
+ price?: number
11
+ quantity?: number
12
+ index?: number
13
+ item_variant?: string
14
+ }
15
+
16
+ function pushToDataLayer(obj: Record<string, unknown>) {
17
+ if (typeof window === "undefined") return
18
+ const w = window as Window & { dataLayer?: unknown[] }
19
+ w.dataLayer = w.dataLayer || []
20
+ w.dataLayer.push(obj)
21
+ }
22
+
23
+ export function trackAddToCart(payload: {
24
+ currency: string
25
+ value: number
26
+ items: GA4Item[]
27
+ }) {
28
+ pushToDataLayer({ event: "add_to_cart", ecommerce: payload })
29
+ }
30
+
31
+ export function trackRemoveFromCart(payload: {
32
+ currency: string
33
+ value: number
34
+ items: GA4Item[]
35
+ }) {
36
+ pushToDataLayer({ event: "remove_from_cart", ecommerce: payload })
37
+ }
38
+
39
+ export function trackViewCart(payload: {
40
+ currency: string
41
+ value: number
42
+ items: GA4Item[]
43
+ }) {
44
+ pushToDataLayer({ event: "view_cart", ecommerce: payload })
45
+ }
46
+
47
+ export function trackViewItem(payload: {
48
+ currency: string
49
+ value: number
50
+ items: GA4Item[]
51
+ }) {
52
+ pushToDataLayer({ event: "view_item", ecommerce: payload })
53
+ }
54
+
55
+ export function trackViewItemList(payload: {
56
+ list_id?: string
57
+ list_name?: string
58
+ items: GA4Item[]
59
+ }) {
60
+ pushToDataLayer({ event: "view_item_list", ecommerce: payload })
61
+ }
62
+
63
+ export function trackBeginCheckout(payload: {
64
+ currency: string
65
+ value: number
66
+ items: GA4Item[]
67
+ }) {
68
+ pushToDataLayer({ event: "begin_checkout", ecommerce: payload })
69
+ }
70
+
71
+ export function trackAddShippingInfo(payload: {
72
+ currency: string
73
+ value: number
74
+ items: GA4Item[]
75
+ }) {
76
+ pushToDataLayer({ event: "add_shipping_info", ecommerce: payload })
77
+ }
78
+
79
+ export function trackAddPaymentInfo(payload: {
80
+ currency: string
81
+ value: number
82
+ items: GA4Item[]
83
+ }) {
84
+ pushToDataLayer({ event: "add_payment_info", ecommerce: payload })
85
+ }
86
+
87
+ export function trackPurchase(payload: {
88
+ transaction_id: string
89
+ currency: string
90
+ value: number
91
+ tax?: number
92
+ shipping?: number
93
+ items: GA4Item[]
94
+ }) {
95
+ pushToDataLayer({ event: "purchase", ecommerce: payload })
96
+ }
package/src/config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { getLocaleHeader } from "@core/util/get-locale-header"
2
+ import Medusa, { FetchArgs, FetchInput } from "@medusajs/js-sdk"
3
+
4
+ // Defaults to standard port for Medusa server
5
+ let MEDUSA_BACKEND_URL = "http://localhost:9000"
6
+
7
+ if (process.env.MEDUSA_BACKEND_URL) {
8
+ MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL
9
+ }
10
+
11
+ export const sdk = new Medusa({
12
+ baseUrl: MEDUSA_BACKEND_URL,
13
+ debug: process.env.NODE_ENV === "development",
14
+ publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
15
+ })
16
+
17
+ const originalFetch = sdk.client.fetch.bind(sdk.client)
18
+
19
+ sdk.client.fetch = async <T>(input: FetchInput, init?: FetchArgs): Promise<T> => {
20
+ const headers = init?.headers ?? {}
21
+ let localeHeader: Record<string, string | null> | undefined
22
+ try {
23
+ localeHeader = await getLocaleHeader()
24
+ headers["x-medusa-locale"] ??= localeHeader["x-medusa-locale"]
25
+ } catch {}
26
+
27
+ const newHeaders = {
28
+ ...localeHeader,
29
+ ...headers,
30
+ }
31
+ init = {
32
+ ...init,
33
+ headers: newHeaders,
34
+ }
35
+ return originalFetch(input, init)
36
+ }
@@ -0,0 +1,84 @@
1
+ import React from "react"
2
+ import { CreditCard } from "@medusajs/icons"
3
+
4
+ import Ideal from "@modules/common/icons/ideal"
5
+ import Bancontact from "@modules/common/icons/bancontact"
6
+ import PayPal from "@modules/common/icons/paypal"
7
+
8
+ /* Map of payment provider_id to their title and icon. Add in any payment providers you want to use. */
9
+ export const paymentInfoMap: Record<
10
+ string,
11
+ { title: string; icon: React.JSX.Element }
12
+ > = {
13
+ pp_stripe_stripe: {
14
+ title: "Credit card",
15
+ icon: <CreditCard />,
16
+ },
17
+ "pp_medusa-payments_default": {
18
+ title: "Credit card",
19
+ icon: <CreditCard />,
20
+ },
21
+ "pp_stripe-ideal_stripe": {
22
+ title: "iDeal",
23
+ icon: <Ideal />,
24
+ },
25
+ "pp_stripe-bancontact_stripe": {
26
+ title: "Bancontact",
27
+ icon: <Bancontact />,
28
+ },
29
+ pp_paypal_paypal: {
30
+ title: "PayPal",
31
+ icon: <PayPal />,
32
+ },
33
+ pp_system_default: {
34
+ title: "Cash on Delivery (Cash/UPI)",
35
+ icon: <CreditCard />,
36
+ },
37
+ razorpay: {
38
+ title: "Razorpay",
39
+ icon: <CreditCard />,
40
+ },
41
+ pp_razorpay_razorpay: {
42
+ title: "Razorpay",
43
+ icon: <CreditCard />,
44
+ },
45
+ // Add more payment providers here
46
+ }
47
+
48
+ // This only checks if it is native stripe or medusa payments for card payments, it ignores the other stripe-based providers
49
+ export const isStripeLike = (providerId?: string) => {
50
+ return providerId?.startsWith("pp_stripe_") || providerId?.startsWith("pp_medusa-")
51
+ }
52
+
53
+ export const isPaypal = (providerId?: string) => {
54
+ return providerId?.startsWith("pp_paypal")
55
+ }
56
+ export const isManual = (providerId?: string) => {
57
+ return providerId?.startsWith("pp_system_default")
58
+ }
59
+ export const isRazorpay = (providerId?: string) => {
60
+ return providerId?.startsWith("razorpay") || providerId?.startsWith("pp_razorpay")
61
+ }
62
+
63
+ // Add currencies that don't need to be divided by 100
64
+ export const noDivisionCurrencies = [
65
+ "krw",
66
+ "jpy",
67
+ "vnd",
68
+ "clp",
69
+ "pyg",
70
+ "xaf",
71
+ "xof",
72
+ "bif",
73
+ "djf",
74
+ "gnf",
75
+ "kmf",
76
+ "mga",
77
+ "rwf",
78
+ "xpf",
79
+ "htg",
80
+ "vuv",
81
+ "xag",
82
+ "xdr",
83
+ "xau",
84
+ ]
@@ -0,0 +1,40 @@
1
+ "use client"
2
+
3
+ import React, { createContext, useContext, useState } from "react"
4
+
5
+ interface ModalContextType {
6
+ isModalOpen: boolean
7
+ setIsModalOpen: (isOpen: boolean) => void
8
+ close: () => void
9
+ }
10
+
11
+ const ModalContext = createContext<ModalContextType | undefined>(undefined)
12
+
13
+ export const ModalProvider: React.FC<{
14
+ children: React.ReactNode
15
+ close?: () => void
16
+ }> = ({ children, close: closeProp }) => {
17
+ const [isModalOpen, setIsModalOpen] = useState(false)
18
+
19
+ const close = () => {
20
+ if (closeProp) {
21
+ closeProp()
22
+ } else {
23
+ setIsModalOpen(false)
24
+ }
25
+ }
26
+
27
+ return (
28
+ <ModalContext.Provider value={{ isModalOpen, setIsModalOpen, close }}>
29
+ {children}
30
+ </ModalContext.Provider>
31
+ )
32
+ }
33
+
34
+ export const useModal = () => {
35
+ const context = useContext(ModalContext)
36
+ if (context === undefined) {
37
+ throw new Error("useModal must be used within a ModalProvider")
38
+ }
39
+ return context
40
+ }
@@ -0,0 +1,96 @@
1
+ "use client"
2
+
3
+ import React, {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useEffect,
8
+ useCallback,
9
+ } from "react"
10
+ import {
11
+ getWishlistProductIds,
12
+ addToWishlist as addToWishlistApi,
13
+ removeFromWishlist as removeFromWishlistApi,
14
+ } from "@core/data/wishlist"
15
+
16
+ interface WishlistContextType {
17
+ wishlistIds: string[]
18
+ count: number
19
+ isWishlisted: (productId: string) => boolean
20
+ toggleWishlist: (productId: string) => Promise<{ success: boolean; error?: string }>
21
+ refreshWishlist: () => Promise<void>
22
+ }
23
+
24
+ const WishlistContext = createContext<WishlistContextType | undefined>(undefined)
25
+
26
+ export const WishlistProvider: React.FC<{ children: React.ReactNode }> = ({
27
+ children,
28
+ }) => {
29
+ const [wishlistIds, setWishlistIds] = useState<string[]>([])
30
+
31
+ const refreshWishlist = useCallback(async () => {
32
+ try {
33
+ const ids = await getWishlistProductIds()
34
+ setWishlistIds(ids)
35
+ } catch (error) {
36
+ // Error silently ignored for production
37
+ }
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ refreshWishlist()
42
+ }, [refreshWishlist])
43
+
44
+ const isWishlisted = useCallback(
45
+ (productId: string) => {
46
+ return wishlistIds.includes(productId)
47
+ },
48
+ [wishlistIds]
49
+ )
50
+
51
+ const toggleWishlist = async (productId: string) => {
52
+ const currentlyWishlisted = isWishlisted(productId)
53
+
54
+ try {
55
+ if (currentlyWishlisted) {
56
+ const result = await removeFromWishlistApi(productId)
57
+ if (result.success) {
58
+ setWishlistIds((prev) => prev.filter((id) => id !== productId))
59
+ return { success: true }
60
+ }
61
+ return { success: false, error: result.error }
62
+ } else {
63
+ const result = await addToWishlistApi(productId)
64
+ if (result.success) {
65
+ setWishlistIds((prev) => [...prev, productId])
66
+ return { success: true }
67
+ }
68
+ return { success: false, error: result.error }
69
+ }
70
+ } catch (error: any) {
71
+ return { success: false, error: error.message || "An error occurred" }
72
+ }
73
+ }
74
+
75
+ return (
76
+ <WishlistContext.Provider
77
+ value={{
78
+ wishlistIds,
79
+ count: wishlistIds.length,
80
+ isWishlisted,
81
+ toggleWishlist,
82
+ refreshWishlist,
83
+ }}
84
+ >
85
+ {children}
86
+ </WishlistContext.Provider>
87
+ )
88
+ }
89
+
90
+ export const useWishlist = () => {
91
+ const context = useContext(WishlistContext)
92
+ if (context === undefined) {
93
+ throw new Error("useWishlist must be used within a WishlistProvider")
94
+ }
95
+ return context
96
+ }
@@ -0,0 +1,111 @@
1
+ "use server"
2
+
3
+ import { sdk } from "@core/config"
4
+ import medusaError from "@core/util/medusa-error"
5
+ import { HttpTypes } from "@medusajs/types"
6
+ import { revalidateTag, revalidatePath } from "next/cache"
7
+ import { redirect } from "next/navigation"
8
+ import { headers as getRequestHeaders } from "next/headers"
9
+ import {
10
+ getAuthHeaders,
11
+ getCacheOptions,
12
+ getCacheTag,
13
+ getCartId,
14
+ removeCartId,
15
+ setCartId,
16
+ getHoldCartId,
17
+ setHoldCartId,
18
+ removeHoldCartId,
19
+ setBuyNowCartId,
20
+ removeBuyNowCartId,
21
+ } from "../cookies"
22
+ import { getRegion } from "../regions"
23
+ import { getLocale } from "@core/data/locale-actions"
24
+ import { retrieveCustomer, transferCart } from "@core/data/customer"
25
+ import { cache } from "react"
26
+ import {
27
+ createBuyNowMetadata,
28
+ filterOutCurrentCart,
29
+ getBuyNowCheckoutUrl,
30
+ getPostOrderCookieAction,
31
+ getResumeAbandonedCheckoutUrl,
32
+ isEphemeralCart,
33
+ normalizePhoneForRazorpay,
34
+ partitionAbandonedCarts,
35
+ shouldCheckEphemeralCartMetadata,
36
+ shouldSkipToPayment,
37
+ validateAddToCartInput,
38
+ validateCartId,
39
+ validateLineItemDeleteInput,
40
+ validateLineItemUpdateInput,
41
+ validateRazorpayPreflight,
42
+ } from "@core/domain/cart"
43
+
44
+ export async function getAbandonedCarts() {
45
+ const headers = {
46
+ ...(await getAuthHeaders()),
47
+ }
48
+
49
+ // If no authorization, return empty arrays
50
+ if (!headers["authorization"]) {
51
+ return { buyNowCarts: [], reorderCarts: [] }
52
+ }
53
+
54
+ try {
55
+ // Just fetch all carts for the customer and filter locally
56
+ const res = await sdk.client
57
+ .fetch<{ carts: HttpTypes.StoreCart[] }>(`/store/carts`, {
58
+ method: "GET",
59
+ query: {
60
+ fields:
61
+ "*items, *items.product, *items.product.thumbnail, *items.variant, *items.metadata, +total",
62
+ },
63
+ headers,
64
+ cache: "no-store",
65
+ })
66
+ .catch(() => null)
67
+
68
+ let buyNowCarts: HttpTypes.StoreCart[] = []
69
+ let reorderCarts: HttpTypes.StoreCart[] = []
70
+
71
+ if (res?.carts) {
72
+ const currentCartId = await getCartId()
73
+ const filteredCarts = filterOutCurrentCart(res.carts, currentCartId)
74
+ const partitioned = partitionAbandonedCarts(filteredCarts)
75
+
76
+ buyNowCarts = partitioned.buyNowCarts
77
+ reorderCarts = partitioned.reorderCarts
78
+ }
79
+
80
+ return { buyNowCarts, reorderCarts }
81
+ } catch (e) {
82
+ console.error("Failed to fetch abandoned carts:", e)
83
+ return { buyNowCarts: [], reorderCarts: [] }
84
+ }
85
+ }
86
+
87
+ export async function resumeAbandonedCart(cartId: string, countryCode: string) {
88
+ // Set as buy now cart so it temporarily overrides the main cart
89
+ // without deleting the main cart cookie (_medusa_cart_id)
90
+ await setBuyNowCartId(cartId)
91
+
92
+ redirect(getResumeAbandonedCheckoutUrl(countryCode, cartId))
93
+ }
94
+
95
+ export async function deleteCart(cartId: string) {
96
+ const headers = await getAuthHeaders()
97
+
98
+ try {
99
+ await sdk.client.fetch(`/store/carts/${cartId}`, {
100
+ method: "DELETE",
101
+ headers,
102
+ cache: "no-store",
103
+ })
104
+
105
+ revalidateTag("carts")
106
+ return { success: true }
107
+ } catch (e: any) {
108
+ console.error("Failed to delete cart:", e)
109
+ return { success: false, error: e.message }
110
+ }
111
+ }