Published on

Implementing Feature Flags for Subscriptions in Next.js

Authors

Implementing Feature Flags for Subscriptions in Next.js

In this tutorial, we'll walk through implementing feature flags based on subscription plans in a Next.js application. We'll use a simple JSON data structure to manage features and subscriptions, making the setup straightforward and easy to maintain. Let's get started!

Step 1: Define Your Features and Subscriptions

First, create a features.ts file where we'll define our features and subscriptions. Each feature will have a unique key, name, description, and the plans it belongs to.

// features.ts
interface Feature {
  key: string
  name: string
  description: string
  plan: string[]
}

interface Subscription {
  type: string
  title: string
  description: string
  price: string
  buttonText: string
  buttonLink: string
}

const features: Feature[] = [
  {
    key: 'feature_one',
    name: 'Feature One',
    description: 'Description for feature one.',
    plan: ['basic', 'professional'],
  },
  {
    key: 'feature_two',
    name: 'Feature Two',
    description: 'Description for feature two.',
    plan: ['professional', 'enterprise'],
  },
  {
    key: 'feature_three',
    name: 'Feature Three',
    description: 'Description for feature three.',
    plan: ['enterprise'],
  },
]

const subscriptions: Subscription[] = [
  {
    type: 'basic',
    title: 'Basic',
    description: 'Basic subscription plan.',
    price: '10 USD / month',
    buttonText: 'Subscribe to Basic',
    buttonLink: '/subscribe-basic',
  },
  {
    type: 'professional',
    title: 'Professional',
    description: 'Professional subscription plan.',
    price: '20 USD / month',
    buttonText: 'Subscribe to Professional',
    buttonLink: '/subscribe-professional',
  },
  {
    type: 'enterprise',
    title: 'Enterprise',
    description: 'Enterprise subscription plan.',
    price: '50 USD / month',
    buttonText: 'Subscribe to Enterprise',
    buttonLink: '/subscribe-enterprise',
  },
]

export { features, subscriptions, type Feature, type Subscription }

Step 2: Create a Hook to Fetch Features

Next, create a custom hook useFeatures to fetch the features based on the subscription type.

// hooks/useFeatures.ts
import { useState, useEffect } from 'react'
import { Feature, features } from '../features'

interface UseFeaturesResult {
  features: string[]
  loading: boolean
}

function useFeatures(subscriptionType: string): UseFeaturesResult {
  const [featuresList, setFeaturesList] = useState<string[]>([])
  const [loading, setLoading] = useState<boolean>(true)

  useEffect(() => {
    function fetchFeatures() {
      try {
        const filteredFeatures = features
          .filter((feature) => feature.plan.includes(subscriptionType))
          .map((feature) => feature.key)
        setFeaturesList(filteredFeatures)
      } catch (error) {
        console.error('Error fetching features:', error)
      } finally {
        setLoading(false)
      }
    }
    fetchFeatures()
  }, [subscriptionType])

  return { features: featuresList, loading }
}

export default useFeatures

Step 3: Create a Context to Provide Features

We'll use a context to provide the features to the component tree.

// context/FeaturesContext.tsx
import { createContext, ReactNode, useContext } from 'react';
import useFeatures from '../hooks/useFeatures';

interface FeaturesContextProps {
  features: string[];
  loading: boolean;
}

interface FeaturesProviderProps {
  subscriptionType: string;
  children: ReactNode;
}

const FeaturesContext = createContext<FeaturesContextProps | undefined>(undefined);

export function FeaturesProvider({ subscriptionType, children }: FeaturesProviderProps) {
  const { features, loading } = useFeatures(subscriptionType);

  return (
    <FeaturesContext.Provider value={{ features, loading }}>
      {children}
    </FeaturesContext.Provider>
  );
}

export function useFeaturesContext(): FeaturesContextProps {
  const context = useContext(FeaturesContext);
  if (!context) {
    throw new Error('useFeaturesContext must be used within a FeaturesProvider');
  }
  return context;
}

export { FeaturesContext };

Step 4: Create a Has Component to Check Feature Availability

Create a Has component that conditionally renders its children based on the presence of specified features.


// components/Has.tsx
import { ReactNode } from 'react';
import { useFeaturesContext } from '../context/FeaturesContext';

interface HasProps {
  feature: string | string[];
  fallback?: ReactNode;
  children: ReactNode;
}

function Has({ feature, fallback, children }: HasProps) {
  const { features, loading } = useFeaturesContext();

  if (loading) return null; // Optionally handle loading state

  const hasFeature = Array.isArray(feature)
    ? feature.every((f) => features.includes(f))
    : features.includes(feature);

  if (!hasFeature) {
    return <>{fallback}</> || null;
  }

  return <>{children}</>;
}

export default Has;

Step 5: Wrap Your Application with the FeaturesProvider

Ensure your application is wrapped with the FeaturesProvider to pass the subscription type.


// pages/_app.tsx
import { AppProps } from 'next/app';
import { FeaturesProvider } from '../context/FeaturesContext';

function MyApp({ Component, pageProps }: AppProps) {
  const subscriptionType = 'basic'; // Fetch from user session or context

  return (
    <FeaturesProvider subscriptionType={subscriptionType}>
      <Component {...pageProps} />
    </FeaturesProvider>
  );
}

export default MyApp;

Step 6: Use the "Has" Component in Your Pages

Now you can use the "Has" component to conditionally render elements based on features.

// pages/index.tsx
import { Button, Stack } from '@chakra-ui/react';
import Has from '../components/Has';
import { subscriptions, features } from '../features'; // Adjust the path as necessary
import PricingCard from '../components/PricingCard'; // Adjust the path as necessary

function HomePage() {
  return (
    <Stack direction={"row"} p="12" spacing="4">
      {subscriptions.map((subscription) => {
        const subscriptionFeatures = features
          .filter((feature) => feature.plan.includes(subscription.type))
          .map((feature) => feature.name);

        return (
          <PricingCard
            key={subscription.type}
            title={subscription.title}
            features={subscriptionFeatures}
            description={subscription.description}
            price={subscription.price}
            buttonText={subscription.buttonText}
            buttonLink={subscription.buttonLink}
            subscriptionType={subscription.type}
          />
        );
      })}

      <Has feature="feature_one" fallback={<Button colorScheme="red">Feature One Disabled</Button>}>
        <Button colorScheme="green">Feature One Enabled</Button>
      </Has>

      <Has feature={["feature_two", "feature_three"]} fallback={<Button colorScheme="red">Multiple Features Disabled</Button>}>
        <Button colorScheme="green">All Required Features Enabled</Button>
      </Has>
    </Stack>
  );
}

export default HomePage;


// components/PricingCard.tsx
import { Box, Button, Heading, Text, List, ListItem } from '@chakra-ui/react';

interface PricingCardProps {
  title: string;
  features: string[];
  description: string;
  price: string;
  buttonText: string;
  buttonLink: string;
  subscriptionType: string;
}

const PricingCard: React.FC<PricingCardProps> = ({
  title,
  features,
  description,
  price,
  buttonText,
  buttonLink,
  subscriptionType,
}) => {
  return (
    <Box border="1px" borderColor="gray.200" borderRadius="md" p="6" width="full">
      <Heading as="h3" size="lg" mb="4">
        {title}
      </Heading>
      <Text mb="4">{description}</Text>
      <List spacing={3} mb="4">
        {features.map((feature, index) => (
          <ListItem key={index}>{feature}</ListItem>
        ))}
      </List>
      <Text fontSize="2xl" fontWeight="bold" mb="4">{price}</Text>
      <Button colorScheme="teal" as="a" href={buttonLink}>
        {buttonText}
      </Button>
    </Box>
  );
};

export default PricingCard;

And that's it! You've successfully implemented feature flags based on subscription plans in a Next.js application using a simple JSON data structure. This setup allows you to easily manage and extend features as your application grows.