From 852c911ac8676b314cd085245c715d461a29447f Mon Sep 17 00:00:00 2001 From: oybek Date: Fri, 9 Feb 2024 19:33:54 +0400 Subject: [PATCH] feat(analytics): added shopify analytics --- .gitignore | 1 + app/layout.tsx | 2 + components/cart/actions.ts | 20 ++++++- components/cart/add-to-cart.tsx | 21 +++++-- components/layout/shopify-analytics.tsx | 13 +++++ lib/constants.ts | 5 ++ lib/shopify/hooks/use-shopify-analytics.ts | 68 ++++++++++++++++++++++ lib/shopify/index.ts | 9 ++- 8 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 components/layout/shopify-analytics.tsx create mode 100644 lib/shopify/hooks/use-shopify-analytics.ts diff --git a/.gitignore b/.gitignore index 0298027e4..5da11f77f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.idea diff --git a/app/layout.tsx b/app/layout.tsx index 58f5a9708..2b6c434a8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import { GeistSans } from 'geist/font'; import { ensureStartsWith } from 'lib/utils'; import { ReactNode, Suspense } from 'react'; import './globals.css'; +import ShopifyAnalytics from 'components/layout/shopify-analytics'; const { TWITTER_CREATOR, TWITTER_SITE, SITE_NAME } = process.env; const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL @@ -39,6 +40,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
{children}
+ ); diff --git a/components/cart/actions.ts b/components/cart/actions.ts index fa2c34d37..0c9b5dbab 100644 --- a/components/cart/actions.ts +++ b/components/cart/actions.ts @@ -5,7 +5,16 @@ import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/ import { revalidateTag } from 'next/cache'; import { cookies } from 'next/headers'; -export async function addItem(prevState: any, selectedVariantId: string | undefined) { +type AddItemResponse = { + cartId?: string; + success: boolean; + message?: string; +}; + +export async function addItem( + prevState: any, + selectedVariantId: string | undefined +): Promise { let cartId = cookies().get('cartId')?.value; let cart; @@ -20,14 +29,15 @@ export async function addItem(prevState: any, selectedVariantId: string | undefi } if (!selectedVariantId) { - return 'Missing product variant ID'; + return { success: false, message: 'Missing variant ID' }; } try { await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]); revalidateTag(TAGS.cart); + return { success: true }; } catch (e) { - return 'Error adding item to cart'; + return { success: false, message: 'Error adding item to cart' }; } } @@ -41,6 +51,10 @@ export async function removeItem(prevState: any, lineId: string) { try { await removeFromCart(cartId, [lineId]); revalidateTag(TAGS.cart); + return { + success: true, + cartId + }; } catch (e) { return 'Error removing item from cart'; } diff --git a/components/cart/add-to-cart.tsx b/components/cart/add-to-cart.tsx index 5e7afbff9..affeaf341 100644 --- a/components/cart/add-to-cart.tsx +++ b/components/cart/add-to-cart.tsx @@ -7,6 +7,8 @@ import LoadingDots from 'components/loading-dots'; import { ProductVariant } from 'lib/shopify/types'; import { useSearchParams } from 'next/navigation'; import { useFormState, useFormStatus } from 'react-dom'; +import { useEffect } from 'react'; +import { useShopifyAnalytics } from '../../lib/shopify/hooks/use-shopify-analytics'; function SubmitButton({ availableForSale, @@ -70,7 +72,8 @@ export function AddToCart({ variants: ProductVariant[]; availableForSale: boolean; }) { - const [message, formAction] = useFormState(addItem, null); + const { sendAddToCart } = useShopifyAnalytics(); + const [response, formAction] = useFormState(addItem, null); const searchParams = useSearchParams(); const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined; const variant = variants.find((variant: ProductVariant) => @@ -81,12 +84,22 @@ export function AddToCart({ const selectedVariantId = variant?.id || defaultVariantId; const actionWithVariant = formAction.bind(null, selectedVariantId); + useEffect(() => { + if (response?.success && response.cartId) { + sendAddToCart({ + cartId: response.cartId + }); + } + }, [response?.success, response?.cartId, sendAddToCart]); + return (
-

- {message} -

+ {response?.message && ( +

+ {response.message} +

+ )} ); } diff --git a/components/layout/shopify-analytics.tsx b/components/layout/shopify-analytics.tsx new file mode 100644 index 000000000..8f1857080 --- /dev/null +++ b/components/layout/shopify-analytics.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useEffect } from 'react'; +import { AnalyticsEventName } from '@shopify/hydrogen-react'; +import { useShopifyAnalytics } from 'lib/shopify/hooks/use-shopify-analytics'; + +export default function ShopifyAnalytics() { + const { sendPageView, pathname } = useShopifyAnalytics(); + useEffect(() => { + sendPageView(AnalyticsEventName.PAGE_VIEW); + }, [pathname, sendPageView]); + return null; +} diff --git a/lib/constants.ts b/lib/constants.ts index 56bc6cd12..c31f0aff9 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -29,3 +29,8 @@ export const TAGS = { export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden'; export const DEFAULT_OPTION = 'Default Title'; export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json'; + +export const currency = 'AED'; + +// use your logic to get language +export const defaultLanguage = 'EN'; diff --git a/lib/shopify/hooks/use-shopify-analytics.ts b/lib/shopify/hooks/use-shopify-analytics.ts new file mode 100644 index 000000000..f97899768 --- /dev/null +++ b/lib/shopify/hooks/use-shopify-analytics.ts @@ -0,0 +1,68 @@ +import { usePathname } from 'next/navigation'; +import { + AnalyticsEventName, + getClientBrowserParameters, + sendShopifyAnalytics, + ShopifyAnalyticsProduct, + ShopifyPageViewPayload, + ShopifySalesChannel, + useShopifyCookies +} from '@shopify/hydrogen-react'; +import { currency, defaultLanguage } from 'lib/constants'; + +const SHOP_ID = process.env.NEXT_PUBLIC_SHOPIFY_SHOP_ID!; + +type SendPageViewPayload = { + pageType?: string; + products?: ShopifyAnalyticsProduct[]; + collectionHandle?: string; + searchString?: string; + totalValue?: number; + cartId?: string; +}; + +type SendAddToCartPayload = { + cartId: string; + products?: ShopifyAnalyticsProduct[]; + totalValue?: ShopifyPageViewPayload['totalValue']; +}; + +export function useShopifyAnalytics() { + const pathname = usePathname(); + // send page view event + const sendPageView = ( + eventName: keyof typeof AnalyticsEventName, + payload?: SendPageViewPayload + ) => + sendShopifyAnalytics({ + eventName, + payload: { + ...getClientBrowserParameters(), + hasUserConsent: true, + shopifySalesChannel: ShopifySalesChannel.headless, + shopId: `gid://shopify/Shop/${SHOP_ID}`, + currency, + acceptedLanguage: defaultLanguage, + ...payload + } + }); + + // send add to cart event + const sendAddToCart = ({ cartId, totalValue, products }: SendAddToCartPayload) => + sendPageView(AnalyticsEventName.ADD_TO_CART, { + cartId, + totalValue, + products + }); + + // setup cookies for shopify analytics & enable user consent + useShopifyCookies({ + hasUserConsent: true + }); + + return { + sendPageView, + sendAddToCart, + pathname + }; +} diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index e8b6637c8..9b3c893a2 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -2,7 +2,7 @@ import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/cons import { isShopifyError } from 'lib/type-guards'; import { ensureStartsWith } from 'lib/utils'; import { revalidateTag } from 'next/cache'; -import { headers } from 'next/headers'; +import { cookies, headers } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; import { addToCartMutation, @@ -214,12 +214,19 @@ export async function addToCart( cartId: string, lines: { merchandiseId: string; quantity: number }[] ): Promise { + // get shopify cookies + const shopifyY = cookies()?.get('_shopify_y')?.value; + const shopifyS = cookies()?.get('_shopify_s')?.value; + const res = await shopifyFetch({ query: addToCartMutation, variables: { cartId, lines }, + headers: { + ...(shopifyY && shopifyS && { cookie: `_shopify_y=${shopifyY}; _shopify_s=${shopifyS};` }) + }, cache: 'no-store' }); return reshapeCart(res.body.data.cartLinesAdd.cart);