mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 23:46:58 +00:00
feat(analytics): added shopify analytics
This commit is contained in:
parent
610b0e8692
commit
852c911ac8
1
.gitignore
vendored
1
.gitignore
vendored
@ -36,3 +36,4 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.idea
|
||||
|
@ -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 })
|
||||
<Suspense>
|
||||
<main>{children}</main>
|
||||
</Suspense>
|
||||
<ShopifyAnalytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -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<AddItemResponse> {
|
||||
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';
|
||||
}
|
||||
|
@ -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 (
|
||||
<form action={actionWithVariant}>
|
||||
<SubmitButton availableForSale={availableForSale} selectedVariantId={selectedVariantId} />
|
||||
{response?.message && (
|
||||
<p aria-live="polite" className="sr-only" role="status">
|
||||
{message}
|
||||
{response.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
13
components/layout/shopify-analytics.tsx
Normal file
13
components/layout/shopify-analytics.tsx
Normal file
@ -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;
|
||||
}
|
@ -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';
|
||||
|
68
lib/shopify/hooks/use-shopify-analytics.ts
Normal file
68
lib/shopify/hooks/use-shopify-analytics.ts
Normal file
@ -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
|
||||
};
|
||||
}
|
@ -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<Cart> {
|
||||
// get shopify cookies
|
||||
const shopifyY = cookies()?.get('_shopify_y')?.value;
|
||||
const shopifyS = cookies()?.get('_shopify_s')?.value;
|
||||
|
||||
const res = await shopifyFetch<ShopifyAddToCartOperation>({
|
||||
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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user