Skip to content

Commit 1ee8d1c

Browse files
authored
enable shopping cart in experiments-statsig (#1105)
Enhances the Flags SDK Statsig example by making it possible to actually add items to the shopping cart.
1 parent 57f4652 commit 1ee8d1c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1327
-371
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
{
2-
"extends": "next/core-web-vitals"
3-
}
2+
"root": true,
3+
"extends": "next/core-web-vitals",
4+
"rules": {
5+
"@typescript-eslint/require-await": "off",
6+
"@typescript-eslint/no-misused-promises": "off",
7+
"import/order": "off",
8+
"camelcase": "off",
9+
"no-console": "off"
10+
}
11+
}

flags-sdk/experimentation-statsig/README.md

+20-4
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,20 @@ This allows the Flags SDK and the Flags Explorer to work correctly, by getting a
4343
vercel env pull
4444
```
4545

46-
### Step 3: Create feature flags on Statsig
46+
### Step 3: Create Feature Gates and Experiments
4747

48-
Head over to the [Statsig Console](console.statsig.com) and create the two feature flags under Feature Gates:
48+
Head over to the [Statsig Console](console.statsig.com) and create the feature gates and experiments required by this template.
49+
50+
Ensure you select `Stable ID` instead of `User ID` when creating the gates and experiments.
51+
52+
Feature Gates:
4953

5054
- `Summer Sale` with the gate id `summer_sale`, targeting the `Stable ID`
5155
- `Free Shipping` with the gate id `free_delivery`, targeting the `Stable ID`
5256

53-
Note: please make sure you select the `Stable ID` instead of the `User ID` which is selected by default.
57+
Experiments:
58+
59+
- `Proceed to Checkout` with the id `proceed_to_checkout`, targeting the `Stable ID`
5460

5561
You can also find the gate ids in the `flags.ts` file.
5662

@@ -62,7 +68,17 @@ Create a new rule by clicking on "+ Add New Rule" and set the percentage to 50%.
6268

6369
After that, click on "Save" at the bottom right corner.
6470

65-
### Step 5 (optional): Set additional environment variables
71+
### Step 5: Configure the Experiments
72+
73+
Configure the `Proceed to Checkout` experiment:
74+
75+
- Besides the default `Control` and `Test` groups, add a new group called `Test #2`
76+
- Add a parameter called `color` of type `string`
77+
- Use `blue`, `green` and `red` as the values for the different groups
78+
79+
After that, start the Experiment.
80+
81+
### Step 6 (optional): Set additional environment variables
6682

6783
If you provide the `STATSIG_CONSOLE_API_KEY` and `STATSIG_PROJECT_ID` environment variables, the Flags Explorer will fetch additional metadata from the Statsig API.
6884

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import { addToCart } from '@/lib/actions'
6+
import { useProductDetailPageContext } from '@/components/utils/product-detail-page-context'
7+
import { AddToCartButton } from '@/components/product-detail-page/add-to-cart-button'
8+
9+
export function AddToCart() {
10+
const router = useRouter()
11+
const { color, size } = useProductDetailPageContext()
12+
const [isLoading, setIsLoading] = useState(false)
13+
14+
return (
15+
<AddToCartButton
16+
isLoading={isLoading}
17+
onClick={async () => {
18+
setIsLoading(true)
19+
await addToCart({ id: 'shirt', color, size, quantity: 1 })
20+
router.push('/cart')
21+
}}
22+
/>
23+
)
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { proceedToCheckoutColorFlag } from '@/flags'
2+
import { OrderSummarySection } from '@/components/shopping-cart/order-summary-section'
3+
import { ProceedToCheckout } from './proceed-to-checkout'
4+
5+
export async function OrderSummary({
6+
showSummerBanner,
7+
freeDelivery,
8+
}: {
9+
showSummerBanner: boolean
10+
freeDelivery: boolean
11+
}) {
12+
// This is a fast feature flag so we don't suspend on it
13+
const proceedToCheckoutColor = await proceedToCheckoutColorFlag()
14+
15+
return (
16+
<OrderSummarySection
17+
showSummerBanner={showSummerBanner}
18+
freeDelivery={freeDelivery}
19+
proceedToCheckout={
20+
<ProceedToCheckout
21+
color={proceedToCheckoutColor}
22+
experiment="proceed_to_checkout"
23+
/>
24+
}
25+
/>
26+
)
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { OrderSummary } from '@/app/[code]/cart/order-summary'
2+
import { Main } from '@/components/main'
3+
import { ShoppingCart } from '@/components/shopping-cart/shopping-cart'
4+
import {
5+
productFlags,
6+
showFreeDeliveryBannerFlag,
7+
showSummerBannerFlag,
8+
} from '@/flags'
9+
10+
export default async function CartPage({
11+
params,
12+
}: {
13+
params: Promise<{ code: string }>
14+
}) {
15+
const { code } = await params
16+
const showSummerBanner = await showSummerBannerFlag(code, productFlags)
17+
const freeDeliveryBanner = await showFreeDeliveryBannerFlag(
18+
code,
19+
productFlags
20+
)
21+
22+
return (
23+
<Main>
24+
<div className="lg:grid lg:grid-cols-12 lg:items-start lg:gap-x-12 xl:gap-x-16">
25+
<ShoppingCart />
26+
<OrderSummary
27+
showSummerBanner={showSummerBanner}
28+
freeDelivery={freeDeliveryBanner}
29+
/>
30+
</div>
31+
</Main>
32+
)
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client'
2+
3+
import { ProceedToCheckoutButton } from '@/components/shopping-cart/proceed-to-checkout-button'
4+
import { StatsigExperimentExposure } from '@/statsig/statsig-gate-exposure'
5+
import { toast } from 'sonner'
6+
7+
export function ProceedToCheckout({
8+
color,
9+
experiment,
10+
}: {
11+
color: string
12+
experiment: string
13+
}) {
14+
return (
15+
<>
16+
<StatsigExperimentExposure experiment={experiment} />
17+
<ProceedToCheckoutButton
18+
color={color}
19+
onClick={() => {
20+
// Auto capture will track the event
21+
toast('End reached', {
22+
className: 'my-classname',
23+
description:
24+
'The checkout flow is not implemented in this template.',
25+
duration: 5000,
26+
})
27+
}}
28+
/>
29+
</>
30+
)
31+
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { StaticStatsigProvider } from '@/components/statsig/statsig-provider';
2-
import { productFlags } from '@/flags';
3-
import { deserialize, generatePermutations } from 'flags/next';
4-
import { FlagValues } from 'flags/react';
5-
import { Suspense } from 'react';
1+
import { StaticStatsigProvider } from '@/statsig/statsig-provider'
2+
import { deserialize, generatePermutations } from 'flags/next'
3+
import { FlagValues } from 'flags/react'
4+
import { productFlags, showFreeDeliveryBannerFlag } from '@/flags'
5+
import { FreeDelivery } from '@/app/free-delivery'
6+
import { DevTools } from '@/components/dev-tools'
7+
import { Footer } from '@/components/footer'
8+
import { Navigation } from '@/components/navigation'
69

710
export async function generateStaticParams() {
811
// Returning an empty array here is important as it enables ISR, so
@@ -12,25 +15,37 @@ export async function generateStaticParams() {
1215

1316
// Instead of returning an empty array you could also call generatePermutations
1417
// to generate the permutations upfront.
15-
const codes = await generatePermutations(productFlags);
16-
return codes.map((code) => ({ code }));
18+
const codes = await generatePermutations(productFlags)
19+
return codes.map((code) => ({ code }))
1720
}
1821

1922
export default async function Layout(props: {
20-
children: React.ReactNode;
23+
children: React.ReactNode
2124
params: Promise<{
22-
code: string;
23-
}>;
25+
code: string
26+
}>
2427
}) {
25-
const params = await props.params;
26-
const values = await deserialize(productFlags, params.code);
28+
const params = await props.params
29+
const values = await deserialize(productFlags, params.code)
30+
31+
const showFreeDeliveryBanner = await showFreeDeliveryBannerFlag(
32+
params.code,
33+
productFlags
34+
)
2735

2836
return (
2937
<StaticStatsigProvider>
30-
{props.children}
31-
<Suspense fallback={null}>
38+
<div className="bg-white">
39+
<FreeDelivery
40+
show={showFreeDeliveryBanner}
41+
gate={showFreeDeliveryBannerFlag.key}
42+
/>
43+
<Navigation />
44+
{props.children}
3245
<FlagValues values={values} />
33-
</Suspense>
46+
<Footer />
47+
<DevTools />
48+
</div>
3449
</StaticStatsigProvider>
35-
);
50+
)
3651
}
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,37 @@
1-
import { AddToCartButton } from '@/components/add-to-cart-button'
2-
import { FreeDeliveryBanner } from '@/components/banners/free-delivery-banner'
3-
import { SummerBanner } from '@/components/banners/summer-banner'
4-
import { ColorPicker } from '@/components/color-picker'
5-
import { DevTools } from '@/components/dev-tools'
6-
import { Footer } from '@/components/footer'
1+
import { SummerSale } from '@/app/summer-sale'
72
import { ImageGallery } from '@/components/image-gallery'
8-
import { Navigation } from '@/components/navigation'
9-
import { ProductDetails } from '@/components/product-details'
10-
import { ProductHeader } from '@/components/product-header'
11-
import { SizePicker } from '@/components/size-picker'
12-
import { StatsigGateExposure } from '@/components/statsig/statsig-gate-exposure'
13-
import {
14-
productFlags,
15-
showFreeDeliveryBannerFlag,
16-
showSummerBannerFlag,
17-
} from '@/flags'
18-
import { getPrecomputed } from 'flags/next'
3+
import { ProductDetails } from '@/components/product-detail-page/product-details'
4+
import { ProductHeader } from '@/components/product-detail-page/product-header'
5+
import { AddToCart } from '@/app/[code]/add-to-cart'
6+
import { ColorPicker } from '@/components/product-detail-page/color-picker'
7+
import { SizePicker } from '@/components/product-detail-page/size-picker'
8+
import { ProductDetailPageProvider } from '@/components/utils/product-detail-page-context'
9+
10+
import { productFlags, showSummerBannerFlag } from '@/flags'
11+
import { Main } from '@/components/main'
1912

2013
export default async function Page(props: {
21-
params: Promise<{ code: string }>;
14+
params: Promise<{ code: string }>
2215
}) {
23-
const params = await props.params;
24-
25-
const [showSummerBanner, showFreeDeliveryBanner] = await getPrecomputed(
26-
[showSummerBannerFlag, showFreeDeliveryBannerFlag],
27-
productFlags,
28-
params.code,
29-
);
16+
const params = await props.params
17+
const showSummerBanner = await showSummerBannerFlag(params.code, productFlags)
3018

3119
return (
32-
<div className="bg-white">
33-
<FreeDeliveryBanner show={showFreeDeliveryBanner} />
34-
<StatsigGateExposure gate={showFreeDeliveryBannerFlag.key} />
35-
36-
<Navigation />
37-
38-
<SummerBanner show={showSummerBanner} />
39-
<StatsigGateExposure gate={showSummerBannerFlag.key} />
40-
41-
<main className="mx-auto max-w-2xl px-4 pb-16 sm:px-6 sm:pb-24 lg:max-w-7xl lg:px-8">
20+
<ProductDetailPageProvider>
21+
<SummerSale show={showSummerBanner} gate={showSummerBannerFlag.key} />
22+
<Main>
4223
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
4324
<ProductHeader />
4425
<ImageGallery />
4526

4627
<div className="mt-8 lg:col-span-5">
4728
<ColorPicker />
4829
<SizePicker />
49-
<AddToCartButton />
30+
<AddToCart />
5031
<ProductDetails />
5132
</div>
5233
</div>
53-
</main>
54-
55-
<Footer />
56-
<DevTools />
57-
</div>
34+
</Main>
35+
</ProductDetailPageProvider>
5836
)
5937
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
import { FreeDeliveryBanner } from '@/components/banners/free-delivery-banner'
4+
import { StatsigGateExposure } from '@/statsig/statsig-gate-exposure'
5+
6+
export function FreeDelivery({ gate, show }: { gate: string; show: boolean }) {
7+
return (
8+
<>
9+
<StatsigGateExposure gate={gate} />
10+
{show ? <FreeDeliveryBanner /> : null}
11+
</>
12+
)
13+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
@import "tailwindcss";
1+
@import 'tailwindcss';
2+
3+
@custom-variant dark (&:is(.dark *));
4+
5+
@theme {
6+
--color-link: rgb(0 112 243 / 1);
7+
--color-success: rgb(0 112 243 / 1);
8+
--color-background: white;
9+
--color-success-dark: rgb(7 97 209 / 1);
10+
}
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1-
import { VercelToolbar } from '@vercel/toolbar/next';
2-
import type { Metadata } from 'next';
1+
import { VercelToolbar } from '@vercel/toolbar/next'
2+
import type { Metadata } from 'next'
3+
import { Toaster } from 'sonner'
34

4-
import './globals.css';
5+
import './globals.css'
6+
import { ExamplesBanner } from '@/components/banners/examples-banner'
57

68
export const metadata: Metadata = {
7-
title: 'Shirt Shop',
8-
};
9+
title: 'Statsig - Flags SDK Example',
10+
description: 'A Flags SDK ecommerce example using Statsig',
11+
}
912

1013
export default function RootLayout({
1114
children,
1215
}: Readonly<{
13-
children: React.ReactNode;
16+
children: React.ReactNode
1417
}>) {
1518
return (
1619
<html lang="en">
17-
<body className="">
20+
<body className="antialiased">
21+
<ExamplesBanner />
1822
{children}
23+
<Toaster />
1924
<VercelToolbar />
2025
</body>
2126
</html>
22-
);
27+
)
2328
}

0 commit comments

Comments
 (0)