Route implementation for Paddle inline checkout

We can finally put all together inside the route component.

The route is defined inside routes.ts and accepts a slug parameter (checkout/:slug):

import type { RouteConfig } from "@react-router/dev/routes";
import { route } from "@react-router/dev/routes";

export const routes: RouteConfig = [route("checkout/:slug", "routes/home.tsx")];

Inside routes/home.tsx we use a React Router loader to load the client token from an environment variable using Config:

export async function loader() {
  return Effect.runPromise(
    Config.all({ paddleClientToken: Config.string("PADDLE_CLIENT_TOKEN") })
  );
}

With React Router v7 the result of loader is passed to the route component as props (loaderData):

import { Config, Effect } from "effect";

// 👇 Route types generated by React Router v7
import type * as Route from "./+types.home";

export async function loader() {
  return Effect.runPromise(
    Config.all({ paddleClientToken: Config.string("PADDLE_CLIENT_TOKEN") })
  );
}

export default function Index({
  loaderData,
  params: { slug },
}: Route.ComponentProps) {
  // ...
}

Inside the component we initialize the state machine with useMachine, passing as input slug and clientToken:

export default function Index({
  loaderData,
  params: { slug },
}: Route.ComponentProps) {
  const [snapshot] = useMachine(machine, {
    input: { clientToken: loaderData.paddleClientToken, slug },
  });
  
  // ...
}

The actual JSX contains the Breadcrumbs component as well as a div where the Paddle inline checkout will be rendered:

Breadcrumb uses snapshot.matches to check and highlight the current step based on the machine's state.

export default function Index({
  loaderData,
  params: { slug },
}: Route.ComponentProps) {
  const [snapshot] = useMachine(machine, {
    input: { clientToken: loaderData.paddleClientToken, slug },
  });
  return (
    <main className="mx-auto max-w-4xl my-12">
      <Breadcrumbs className="mb-8">
        <Breadcrumb data-selected={snapshot.matches("Customer")}>
          Customer
        </Breadcrumb>
        <Breadcrumb data-selected={snapshot.matches("Checkout")}>
          Checkout
        </Breadcrumb>
        <Breadcrumb data-selected={snapshot.matches("Success")}>
          Success
        </Breadcrumb>
      </Breadcrumbs>
      <div className={PADDLE_CONTAINER_CLASS}></div>
    </main>
  );
}

The checkout is ready! Running the app will do the following:

  1. loader loads the clientToken as an environment variable
  2. The state machine will call the API to load the product from slug
  3. Initialize the checkout by calling initializePaddle and Paddle.Checkout.open
  4. The checkout is rendered inside the div with the PADDLE_CONTAINER_CLASS class
  5. Each step of the checkout is synced with the state machine, updating the Breadcrumbs component accordingly
  6. The user will proceed with the payment until the last "Success" step