Example: Purchasing and Managing Subscriptions using Connect Sessions

This is a walkthrough of our example application for purchasing and managing subscriptions in an existing application using Connect Sessions.
While this example is written in Next.js and uses TypeScript, the concepts discussed here should be transferrable to every other programming language and framework. We did our best to leave out any Next.js specifics in this guide to keep it as simple as possible.

To enhance readability and highlight the key concepts, we have simplified the code throughout this guide. As a result, you should not copy and paste code snippets directly from this guide with the expectation that they will work as is.

Additionally, to prevent redundancy, this guide refers back to previous sections where certain concepts have already been introduced. If you're skimming through and encounter unclear points, reviewing the guide in its entirety may provide clarity.

However, if you've read the complete guide and some things still are unclear, please do not hesitate to contact us at support@gigs.com so we can improve this guide further!

Further resources:

Allowing users to purchase subscriptions

Our example application is a small shop that sells mobile phones. Naturally, in order to use their phones, users need a phone plan. We'll be adding the ability to purchase a phone plan within our existing checkout success page.

Initial checkout success page

Building the UI

As a first step, we'll fetch the existing plans from our project. This will return an array of Plan objects:

export const getPlans = async (): Promise<{ error?: string; data: Plan[] }> => {
  const response = await fetch(
    `https://api.gigs.com/projects/${process.env.GIGS_PROJECT}/plans`,
    {
      headers,
    },
  )

  const data = await response.json()

  if (response.status !== 200) {
    return {
      error: data.message,
    }
  }

  return { data: data.items }
}

Note: In the headers, we pass our API Token as a Bearer token in the Authorization header. Please refer to the full implementation or the guide on the topic for more information.

In our page, we call this function to obtain all plans we can offer the user:

const CheckoutPage = async () => {
  const { error, data: plans } = await getPlans()

  return (
    // ...
  )
}

export default CheckoutPage

We then map over the plans to render a PurchasePlanCard for every plan in a carousel:

<Carousel>
  <CarouselContent>
    {plans.map((plan) => (
      <CarouselItem key={plan.id}>
        <PurchasePlanCard
          title={plan.name}
          price={plan.price}
          allowances={plan.allowances}
          planId={plan.id}
        />
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

The PurchasePlanCard displays some information such as the price and the data limits of the plan. It also has a button to purchase the plan:

export const PurchasePlanCard = ({
  title,
  allowances,
  price,
  planId,
}: PurchasePlanCardProps) => {
  const handleClick = async (planId: string) => {
    // To be done
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle>{title}</CardTitle>
      </CardHeader>
      <CardContent>
        <p>{description(allowances)}</p>
        <p>{formatPrice(price)}</p>
      </CardContent>
      <CardFooter>
        <Button size="sm" variant="default" onClick={() => handleClick(planId)}>
          Buy Now
        </Button>
      </CardFooter>
    </Card>
  )
}

Checkout success screen with offered plans

This is all the UI we'll need for now. Moving forward, we will develop the logic required to enable users to purchase a specific plan.

Creating the Connect Session

Once the user clicks the "Buy" button on one of the plan cards, we want to create a new Connect Session with the checkoutNewSubscription intent for the given subscription. Additionally, we want to forward the user data we've collected from the user who has just completed a checkout to Connect.

Preparing user data

We're going to take care of the user data first. Since our user is already logged in, we'll call the auth() function to retrieve the user details we can pass on to the Connect Session:

const currentUser = auth.getUser()

Please be aware that in our example, this function serves as a mock and returns hardcoded values. In a real-world scenario, a similar function would likely exist. In our case, this function returns the email, fullName and birthday of our user.

Knowing the email, we can now check if the user is already present in our Connect project or if we need to create them:

const { data: userRes } = await findUser(currentUser.email)
const existingUser = userRes && userRes[0]

The findUser function calls the /users/search endpoint and passes the email as a query parameter to get a list of matching users (see the full implementation here for reference). With this knowledge, we can now create the user data payload for our Connect Session. Connect Sessions accept either a user field with the ID of an existing user or a userDetails field in case the user has to be created:

const userPayload = existingUser
  ? { user: existingUser.id }
  : {
      userDetails: {
        birthday: currentUser.birthday,
        email: currentUser.email,
        fullName: currentUser.fullName,
        preferredLocale: 'en-US',
      },
    }

Assembling and using the Connect Session

The remaining parts of the Connect Session body are pretty straight forward. We need to define the intent type, pass the selected planId in the intent payload and merge in our user data:

const connectSession = {
  callbackUrl: 'http://localhost:3000/phone-plans',
  intent: {
    type: 'checkoutNewSubscription',
    checkoutNewSubscription: {
      plan: planId,
    },
  },
  ...userPayload,
}

We also set the callbackUrl in the payload. This is the URL Connect will redirect the user to after the Connect Session has been completed. With the request body ready, we can make a POST request to /connectSessions:

export const checkoutNewSubscription = async (planId: string) => {
  const currentUser = auth.getUser()

  const { data: userRes } = await findUser(currentUser.email)
  const existingUser = userRes && userRes[0]

  const userPayload = existingUser
    ? { user: existingUser.id }
    : {
        userDetails: {
          birthday: currentUser.birthday,
          email: currentUser.email,
          fullName: currentUser.fullName,
          preferredLocale: 'en-US',
        },
      }

  const connectSession = {
    callbackUrl: 'http://localhost:3000/phone-plans',
    intent: {
      type: 'checkoutNewSubscription',
      checkoutNewSubscription: {
        plan: planId,
      },
    },
    ...userPayload,
  }

  const options: RequestInit = {
    method: 'POST',
    headers,
    body: JSON.stringify(connectSession),
  }

  const response = await fetch(
    `https://api.gigs.com/projects/${process.env.GIGS_PROJECT}/connectSessions`,
    options,
  )
  const data = await response.json()

  if (response.status !== 200) {
    return {
      error: data.message,
    }
  }

  return { data }
}

Upon success, this request will return a new Connect Session with an url field. We can use this URL to redirect the user to Connect where they can purchase a subscription for the chosen plan. The last step to initialize the checkout flow, is to call the checkoutNewSubscription function in our page and redirect the user upon success:

export const PurchasePlanCard = ({
  title,
  allowances,
  price,
  planId,
}: PurchasePlanCardProps) => {
  const router = useRouter()

  const handleClick = async (planId: string) => {
    const { data: session, error } = await checkoutNewSubscription(planId)

    if (error) {
      // Do error handling here
    }

    if (session?.url) {
      router.push(session.url)
    }
  }

  return (
    // ...
  )
}

Finished purchase flow

We have successfully integrated the option for users to purchase phone plans directly from our checkout success page, eliminating the necessity for users to re-enter their information in Connect. Now, with just a few clicks — such as deciding whether to port an existing phone number — users can easily complete their purchase.

We, as the developers, do not have to be concerned about the details of creating an order, handling portings and dealing with payments. With a minimal addition of new code, we've integrated a significant new feature into our application.

Note: In the video above we see a callback redirect to the /phone-plans page. We'll be creating this in the next section.

Managing existing subscriptions

Now that users can purchase plans via our application, it's also important to enable them to manage their plans within our app. Although Connect offers this capability, our users are accustomed to logging in through our portal and navigating it comfortably. Therefore, we'll leverage Connect Sessions once more, delegating only the more complex aspects of plan management to Connect.

We want our users to be able to

  • Purchase Add-ons for their subscriptions if they run out of data
  • Change their subscription if they frequently need more data
  • Cancel their subscription if they are no longer happy with it

Building the UI

We’ll start again by building the user interface and then create Connect Sessions afterwards.

As a first step, we created a new section in our existing dashboard for managing subscriptions

Basic image of our dashboard

The first thing we need to do is fetch a user's subscriptions and display them here. To do this, we utilize the /subscriptions endpoint which accepts a user query parameter, allowing us to filter for only our current user. In order to obtain the userId we once again need to find our current user in the Connect project:

export const getSubscriptionsByUser = async () => {
  const currentUser = auth.getUser()

  // Wrapper around /users/search
  const { error: userError, data: userData } = await findUser(
    currentUser.email!,
  )

  if (userError || !userData || userData?.length === 0) {
    return { error: 'User not found' }
  }

  const userId = userData[0].id

  const response = await fetch(
    fetchUrl(`subscriptions?user=${userId}&status=active`),
    {
      headers,
    },
  )

  const data = await response.json()

  if (response.status !== 200) {
    return {
      error: data.message,
    }
  }

  return { data: data.items }
}

Note: The findUser function calls the /users/search endpoint to get a list of matching users (see the full implementation here for reference).

With this function in place, we are now able to display a list of the current user's subscriptions on our dashboard:

export default async function PhonePlansPage() {
  const { data: subscriptions } = await getSubscriptionsByUser()

  return (
    <div>
      <SideNav />
      <main>
        <Header />
        <div className="grid grid-cols-2 gap-6">
          {subscriptions &&
            subscriptions.map((subscription) => (
              <ManagePlanCard
                key={subscription.id}
                subscription={subscription}
              />
            ))}
        </div>
      </main>
    </div>
  )
}

Each ManagePlanCard presents subscription details alongside a section containing buttons (<ManagePlanActions />), enabling users to perform management actions on each subscription:

export const ManagePlanCard = async ({ subscription }: ManagePlanCardProps) => {
  const { data: addons } = await getAddons(subscription.plan.provider)

  return (
    <Card>
      <CardHeader>
        <CardTitle>{subscription.plan.name}</CardTitle>
        <CardDescription>
          {formatPrice(subscription.plan.price)}
        </CardDescription>
      </CardHeader>
      <CardContent>
        <p>{description(subscription.plan.allowances)}</p>
      </CardContent>
      <CardFooter>
        <ManagePlanActions
          subscriptionId={subscription.id}
          addons={addons || []}
        />
      </CardFooter>
    </Card>
  )
}

The ManagePlanActions component consist of three buttons: Two for changing/cancelling the subscription and one for opening a menu that lists add-ons available for purchase.

Manage Card UI

For that reason, we’re also fetching the available add-ons for a subscription in the <ManagePlanCard /> using the /subscriptionAddons endpoint. This endpoint accepts a subscription query parameter so we can filter the available add-ons by subscription. (The logic is very similar to finding users, so we’re not looking at the getAddons function in detail here, please refer to the full implementation for more details).

We pass the add-ons on to the <ManagePlanActions> component:

export const ManagePlanActions = ({ subscriptionId, addons }) => {
  return (
    <>
      <PurchaseAddonDialog addons={addons} subscriptionId={subscriptionId} />
      <Button
        variant="ghost"
        className="flex items-center gap-2 text-neutral-700"
      >
        <Replace className="h-4 w-4" />
        Change Plan
      </Button>
      <Button variant="ghost" className="flex items-center gap-2 text-rose-500">
        <Trash className="h-4 w-4" />
        Cancel Plan
      </Button>
    </>
  )
}

The PurchaseAddonDialog component lists all the available add-ons and allows users to select one they want to purchase:

export const PurchaseAddonDialog = ({ addons, subscriptionId }) => {
  return (
    <Dialog>
      <DialogTrigger>
        <PlusCircle className="h-4 w-4" />
        Buy Add-on
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle className="mb-8">Choose an Add-on to buy</DialogTitle>
        </DialogHeader>
        {addons.map((addon) => (
          <Card key={addon.id}>
            <CardHeader className="p-4 pb-2">
              <CardTitle className="text-lg">{addon.name}</CardTitle>
            </CardHeader>
            <CardContent>
              <div className="flex flex-col space-y-2">
                <div>Add-on price</div>
                <div className="text-xl font-semibold">
                  {formatPrice(addon.price)}
                </div>
              </div>
              <Button className="mt-4" variant="outline">
                Add to plan
              </Button>
            </CardContent>
          </Card>
        ))}
      </DialogContent>
    </Dialog>
  )
}

Purchase Addons Dialog

This completes the UI work. Next, we'll utilize Connect Sessions to implement the functionality of the buttons.

Creating the Connect Sessions

We have three different Connect Sessions to write:

  1. Cancelling a subscription
  2. Changing a subscription
  3. Purchasing an add-on

We’ll start with changing and cancelling a subscription, as the logic for both is very similar.

To change a subscription, we need to create a Connect Session with the changeSubscription intent, passing on the subscription ID and the user ID (which we will fetch the same way we did before, using our auth function):

export const changeSubscription = async (subscriptionId: string) => {
  // ... get user data

  const connectSession: ConnectSessionParams = {
    callbackUrl: 'http://localhost:3000/phone-plans',
    intent: {
      type: 'changeSubscription',
      changeSubscription: {
        subscription: subscriptionId,
      },
    },
    user: existingUser.id,
  }

  return await createConnectSession(connectSession)
}

Just like with purchasing, we’ll wrap this in a function (this time called changeSubscription) and issue a request to /connectSessions (please refer to the dedicated section of this guide above or see the full implementation for reference).

Cancelling subscriptions is done in a similar way, but with the cancelSubscription intent:

export const cancelSubscription = async (subscriptionId: string) => {
  // ... get user data

  const connectSession: ConnectSessionParams = {
    callbackUrl: 'http://localhost:3000/phone-plans',
    intent: {
      type: 'cancelSubscription',
      cancelSubscription: {
        subscription: subscriptionId,
      },
    },
    user: existingUser.id,
  }

  return await createConnectSession(connectSession)
}

Again, please see the full implementation if you need more context.

We will wrap this Connect Session in a function called cancelSubscription.

With both of these functions in place, we can go ahead and connect the first two buttons in our ManagePlanActions component:

export const ManagePlanActions = ({ subscriptionId, addons }) => {
  const router = useRouter()

  const handleCancelClick = async () => {
    const { data: session, error } = await cancelSubscription(subscriptionId)

    if (error) {
      // Do error handling here
    }

    if (session?.url) {
      router.push(session.url)
    }
  }

  const handleChangeClick = async () => {
    const { data: session, error } = await changeSubscription(subscriptionId)

    if (error) {
      // Do error handling here
    }

    if (session?.url) {
      router.push(session.url)
    }
  }

  return (
    <>
      <PurchaseAddonDialog addons={addons} subscriptionId={subscriptionId} />
      <Button
        variant="ghost"
        className="flex items-center gap-2 text-neutral-700"
        onClick={handleChangeClick}
      >
        <Replace className="h-4 w-4" />
        Change Plan
      </Button>
      <Button
        onClick={handleCancelClick}
        variant="ghost"
        className="flex items-center gap-2 text-rose-500"
      >
        <Trash className="h-4 w-4" />
        Cancel Plan
      </Button>
    </>
  )
}

When users press the "Change Plan" or "Cancel Plan" buttons now, they will be instantly redirected to Connect, where they can execute their desired action without having to log in or manually select the relevant plan.

Changing plan flow

For the add-ons, we need to supply a list of add-ons we want to purchase (just one in our case) and the subscription ID to the Connect Session.

const connectSession = {
  callbackUrl: 'http://localhost:3000/phone-plans',
  intent: {
    type: 'checkoutAddon',
    checkoutAddon: {
      addons: [addonId],
      subscription: subscriptionId,
    },
  },
  user: existingUser.id,
}

We will wrap this Connect Session in a function called checkoutAddon :

export const checkoutAddon = async (
  addonId: string,
  subscriptionId: string,
) => {
  // Get the current users' user ID from the Gigs API and construct the user payload
  // as seen in previous examples
  const userPayload = createUserPayload()

  const connectSession = {
    callbackUrl: 'http://localhost:3000/phone-plans',
    intent: {
      type: 'checkoutAddon',
      checkoutAddon: {
        addons: [addonId],
        subscription: subscriptionId,
      },
    },
    user: existingUser.id,
  }

  const options: RequestInit = {
    method: 'POST',
    headers,
    body: JSON.stringify(connectSession),
  }

  const response = await fetch(
    `https://api.gigs.com/projects/${process.env.GIGS_PROJECT}/connectSessions`,
    options,
  )
  const data = await response.json()

  if (response.status !== 200) {
    console.error(data)
    return {
      error: data.message,
    }
  }

  return { data }
}

With this function in place, we can connect the “Buy” Buttons in the PurchaseAddonDialog :

export const PurchaseAddonDialog = ({ addons, subscriptionId }) => {
  const router = useRouter()

  if (addons.length === 0) {
    return null
  }

  const handleBuyAddonClick = async (addonId: string) => {
    const { data: session, error } = await checkoutAddon(
      addonId,
      subscriptionId,
    )

    if (error) {
      // Do error handling here
    }

    if (session?.url) {
      router.push(session.url)
    }
  }

  return (
    <Dialog>
      <DialogTrigger>
        <PlusCircle className="h-4 w-4" />
        Buy Add-on
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle className="mb-8">Choose an Add-on to buy</DialogTitle>
        </DialogHeader>
        {addons.map((addon) => (
          <Card key={addon.id}>
            <CardHeader className="p-4 pb-2">
              <CardTitle className="text-lg">{addon.name}</CardTitle>
            </CardHeader>
            <CardContent className="flex items-center justify-between p-4">
              <div className="flex flex-col space-y-2">
                <div>Add-on price</div>
                <div className="text-xl font-semibold">
                  {formatPrice(addon.price)}
                </div>
              </div>
              <Button
                className="mt-4"
                variant="outline"
                onClick={() => handleBuyAddonClick(addon.id)}
              >
                Add to plan
              </Button>
            </CardContent>
          </Card>
        ))}
      </DialogContent>
    </Dialog>
  )
}

Handling callback redirects

Upon a successful or failed Connect Session operation, the user will be redirected back to the callbackUrl we defined when we created our Connect Session, in our case, the /phone-plans page.

The callback url will be receiving a session_id and a status parameter that we can use to identify the Connect Session, as well as the outcome of the operation.

The first step is to define a function that fetches the Connect Session that triggered the callback:

export const getConnectSession = async (connectSessionId: string)=> {
  const response = await fetch(
      `https://api.gigs.com/projects/${process.env.GIGS_PROJECT}/connectSessions/${connectSessionId}`,
    { headers },
  )
  const data = await response.json()
  if (response.status !== 200) {
    return {
      error: data.message,
    }
  }

  return { data }
}

Next, we call this function in our callback page in order to obtain the Connect Session in question. We also evaluate the outcome by checking the status value:

const successMessageMap = {
  checkoutAddon: 'Addon successfully added to subscription!',
  changeSubscription: 'Subscription successfully changed!',
  cancelSubscription: 'Subscription successfully cancelled!',
  checkoutNewSubscription: 'Subscription successfully added!',
  undefined: 'Operation successfully completed'
}

const PhonePlansPage = async ({ searchParams }: { searchParams?: { session_id?: string; status: 'success' | string }}) => {
  const isCallback = !!searchParams?.session_id

  const { data: connectSession } = isCallback
    ? await getConnectSession(searchParams.session_id!)
    : { data: null }

  const alertVariant = isCallback && (searchParams.status === 'success' ? 'success' : 'error')
  const alertMessage = isCallback && (searchParams.status === 'success'
    ? successMessageMap[connectSession?.intent.type]
    : `An error occurred: ${searchParams.status}`)

  return (
    <>
      {isCallback && <Alert variant={alertVariant} message={alertMessage} />}
      {/* rest of the phone plans page */}
    </>
}

export default PhonePlansPage

And with that, we have completed the callback handling. Users will now be redirected back to our application after completing their Connect Session, where they will be informed of the outcome of their operation.

Purchase Addon Flow

With these minimal API calls, users can now seamlessly manage their phone plans via our app, transitioning to Connect only when necessary.

Resources