Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Added Shopify Analytics #1318

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
52f85ba
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
4bede78
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
fcc7180
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
0535a43
Merge branch 'vercel:main' into main
oybek-daniyarov Feb 12, 2024
135c821
Update dependencies. (#1314)
leerob Mar 26, 2024
96c4169
Remove stray revalidate
leerob Mar 31, 2024
61b96f0
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
f9af1cd
feat(analytics): updated action error messages
oybek-daniyarov Apr 1, 2024
b3169b1
feat(analytics): added products to add to cart event
oybek-daniyarov Apr 1, 2024
3f7ac19
feat(analytics): added products to add to cart event
oybek-daniyarov Apr 1, 2024
82b4a5d
feat(analytics): updated readme
oybek-daniyarov Apr 1, 2024
54a8272
feat(analytics): package.json
oybek-daniyarov Apr 1, 2024
852c911
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
e307703
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
27c01de
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
69062c0
feat(analytics): added shopify analytics
oybek-daniyarov Feb 9, 2024
7be35ae
feat(analytics): updated action error messages
oybek-daniyarov Apr 1, 2024
ff5c75b
feat(analytics): added products to add to cart event
oybek-daniyarov Apr 1, 2024
86cd033
feat(analytics): added products to add to cart event
oybek-daniyarov Apr 1, 2024
5aa464e
feat(analytics): updated readme
oybek-daniyarov Apr 1, 2024
e14d8d0
Merge remote-tracking branch 'origin/main'
oybek-daniyarov Apr 1, 2024
d22bed0
feat(analytics): package.json
oybek-daniyarov Apr 1, 2024
badca3f
feat(analytics): package.json
oybek-daniyarov Apr 1, 2024
bc23896
feat(readme): updated readme
oybek-daniyarov Apr 1, 2024
4d28219
feat(main): refactoring
oybek-daniyarov Apr 1, 2024
482d2a0
feat(readme): updated readme
oybek-daniyarov Apr 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ SITE_NAME="Next.js Commerce"
SHOPIFY_REVALIDATION_SECRET=""
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
NEXT_PUBLIC_SHOPIFY_SHOP_ID="[your-shopify-shop-id]"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.idea
45 changes: 37 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@ A Next.js 14 and App Router-ready ecommerce template featuring:
- Styling with Tailwind CSS
- Checkout and payments with Shopify
- Automatic light/dark mode based on system settings
- Shopify Analytics

<h3 id="v1-note"></h3>

> Note: Looking for Next.js Commerce v1? View the [code](https://github.com/vercel/commerce/tree/v1), [demo](https://commerce-v1.vercel.store), and [release notes](https://github.com/vercel/commerce/releases/tag/v1).
> Note: Looking for Next.js Commerce v1? View
> the [code](https://github.com/vercel/commerce/tree/v1), [demo](https://commerce-v1.vercel.store),
> and [release notes](https://github.com/vercel/commerce/releases/tag/v1).

## Providers

Vercel will only be actively maintaining a Shopify version [as outlined in our vision and strategy for Next.js Commerce](https://github.com/vercel/commerce/pull/966).
Vercel will only be actively maintaining a Shopify
version [as outlined in our vision and strategy for Next.js Commerce](https://github.com/vercel/commerce/pull/966).

Vercel is happy to partner and work with any commerce provider to help them get a similar template up and running and listed below. Alternative providers should be able to fork this repository and swap out the `lib/shopify` file with their own implementation while leaving the rest of the template mostly unchanged.
Vercel is happy to partner and work with any commerce provider to help them get a similar template up and running and
listed below. Alternative providers should be able to fork this repository and swap out the `lib/shopify` file with
their own implementation while leaving the rest of the template mostly unchanged.

- Shopify (this repository)
- [BigCommerce](https://github.com/bigcommerce/nextjs-commerce) ([Demo](https://next-commerce-v2.vercel.app/))
Expand All @@ -34,21 +40,41 @@ Vercel is happy to partner and work with any commerce provider to help them get
- [Umbraco](https://github.com/umbraco/Umbraco.VercelCommerce.Demo) ([Demo](https://vercel-commerce-demo.umbraco.com/))
- [Wix](https://github.com/wix/nextjs-commerce) ([Demo](https://wix-nextjs-commerce.vercel.app/))

> Note: Providers, if you are looking to use similar products for your demo, you can [download these assets](https://drive.google.com/file/d/1q_bKerjrwZgHwCw0ovfUMW6He9VtepO_/view?usp=sharing).
> Note: Providers, if you are looking to use similar products for your demo, you
> can [download these assets](https://drive.google.com/file/d/1q_bKerjrwZgHwCw0ovfUMW6He9VtepO_/view?usp=sharing).

## Integrations

Integrations enable upgraded or additional functionality for Next.js Commerce

- [Orama](https://github.com/oramasearch/nextjs-commerce) ([Demo](https://vercel-commerce.oramasearch.com/))
- Upgrades search to include typeahead with dynamic re-rendering, vector-based similarity search, and JS-based configuration.
- Upgrades search to include typeahead with dynamic re-rendering, vector-based similarity search, and JS-based
configuration.
- Search runs entirely in the browser for smaller catalogs or on a CDN for larger.

## Shopify analytics

1. Visit https://[your-store-id].myshopify.com/shop.json
2. Search for `shopId` and add it to the `NEXT_PUBLIC_SHOPIFY_SHOP_ID` in your `.env` file.

To test out Shopify analytics, in your localhost, you can use the following steps:

1. Install ngrok
2. Setup custom domain in ngrok dashboard
3. Expose your local development server (e.g., running on port 3000) with a command:

```bash
ngrok http --domain=YOUR_NGROK_DOMAIN 3000
```

## Running locally

You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's
recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for
this, but a `.env` file is all that is necessary.

> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control your Shopify store.
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control your Shopify
> store.

1. Install Vercel CLI: `npm i -g vercel`
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
Expand All @@ -69,8 +95,11 @@ Your app should now be running on [localhost:3000](http://localhost:3000/).
1. Connect to the existing `commerce-shopify` project.
1. Run `vc env pull` to get environment variables.
1. Run `pnpm dev` to ensure everything is working correctly.

</details>

## Vercel, Next.js Commerce, and Shopify Integration Guide

You can use this comprehensive [integration guide](http://vercel.com/docs/integrations/shopify) with step-by-step instructions on how to configure Shopify as a headless CMS using Next.js Commerce as your headless Shopify storefront on Vercel.
You can use this comprehensive [integration guide](http://vercel.com/docs/integrations/shopify) with step-by-step
instructions on how to configure Shopify as a headless CMS using Next.js Commerce as your headless Shopify storefront on
Vercel.
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Navbar from 'components/layout/navbar';
import { GeistSans } from 'geist/font';
import ShopifyAnalytics from 'components/layout/shopify-analytics';
import { ensureStartsWith } from 'lib/utils';
import { ReactNode, Suspense } from 'react';
import './globals.css';
Expand Down Expand Up @@ -39,6 +40,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
<Suspense>
<main>{children}</main>
</Suspense>
<ShopifyAnalytics />
</body>
</html>
);
Expand Down
27 changes: 23 additions & 4 deletions components/cart/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@ import { TAGS } from 'lib/constants';
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';
import { ShopifyAnalyticsProduct } from '@shopify/hydrogen-react';
import { productToAnalytics } from 'lib/utils';

export async function addItem(prevState: any, selectedVariantId: string | undefined) {
type AddItemResponse = {
cartId?: string;
success: boolean;
message?: string;
products?: ShopifyAnalyticsProduct[];
};

export async function addItem(
prevState: any,
selectedVariantId: string | undefined
): Promise<AddItemResponse> {
let cartId = cookies().get('cartId')?.value;
let cart;
const quantity = 1;

if (cartId) {
cart = await getCart(cartId);
Expand All @@ -20,14 +33,20 @@ export async function addItem(prevState: any, selectedVariantId: string | undefi
}

if (!selectedVariantId) {
return 'Missing product variant ID';
return { success: false, message: 'Missing product variant ID' };
}

try {
await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
const response = await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity }]);
revalidateTag(TAGS.cart);
return {
success: true,
message: 'Item added to cart',
cartId,
products: productToAnalytics(response.lines, quantity, selectedVariantId)
};
} catch (e) {
return 'Error adding item to cart';
return { success: false, message: 'Error adding item to cart' };
}
}

Expand Down
23 changes: 19 additions & 4 deletions components/cart/add-to-cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) =>
Expand All @@ -81,12 +84,24 @@ export function AddToCart({
const selectedVariantId = variant?.id || defaultVariantId;
const actionWithVariant = formAction.bind(null, selectedVariantId);

useEffect(() => {
if (response?.success && response.cartId) {
sendAddToCart({
cartId: response.cartId,
products: response.products,
totalValue: Number(response.products?.[0]?.price)
});
}
}, [response?.success, response?.cartId, sendAddToCart, response?.products]);

return (
<form action={actionWithVariant}>
<SubmitButton availableForSale={availableForSale} selectedVariantId={selectedVariantId} />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
{response?.message && (
<p aria-live="polite" className="sr-only" role="status">
{response.message}
</p>
)}
</form>
);
}
13 changes: 13 additions & 0 deletions components/layout/shopify-analytics.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 DEFAULT_CURRENCY = 'AED';

// use your logic to get language
export const DEFAULT_LANGUAGE = 'EN';
4 changes: 4 additions & 0 deletions lib/shopify/fragments/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const cartFragment = /* GraphQL */ `
name
value
}
price {
amount
currencyCode
}
product {
...product
}
Expand Down
68 changes: 68 additions & 0 deletions lib/shopify/hooks/use-shopify-analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { usePathname } from 'next/navigation';
import {
AnalyticsEventName,
getClientBrowserParameters,
sendShopifyAnalytics,
ShopifyAnalyticsProduct,
ShopifyPageViewPayload,
ShopifySalesChannel,
useShopifyCookies
} from '@shopify/hydrogen-react';
import { DEFAULT_CURRENCY, DEFAULT_LANGUAGE } 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: DEFAULT_CURRENCY,
acceptedLanguage: DEFAULT_LANGUAGE,
...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
};
}
9 changes: 8 additions & 1 deletion lib/shopify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions lib/shopify/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type CartItem = {
merchandise: {
id: string;
title: string;
price: Money;
selectedOptions: {
name: string;
value: string;
Expand Down
29 changes: 29 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ReadonlyURLSearchParams } from 'next/navigation';
import { Cart } from './shopify/types';
import { ShopifyAnalyticsProduct } from '@shopify/hydrogen-react';

export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => {
const paramsString = params.toString();
Expand Down Expand Up @@ -37,3 +39,30 @@ export const validateEnvironmentVariables = () => {
);
}
};

/**
* This function takes a cart and a quantity and returns an array of ShopifyAnalyticsProduct objects.
* */
export const productToAnalytics = (
cartItems: Cart['lines'],
quantity: number,
variantId: string
) => {
const line = cartItems.find((line) => line.merchandise.id === variantId);
if (!line) return;

const { merchandise } = line;

if (!merchandise) return;

return [
{
productGid: merchandise?.product.id,
variantGid: variantId,
name: merchandise?.product.title,
variantName: merchandise?.title,
price: merchandise?.price.amount,
quantity
} as ShopifyAnalyticsProduct
];
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.3",
"@shopify/hydrogen-react": "^2024.1.1",
"clsx": "^2.1.0",
"geist": "^1.3.0",
"next": "14.1.4",
Expand Down
Loading
Loading