Skip to main content
The subscription service is the core domain API for blocks that show or modify a customer’s subscription. It exposes reactive state for the current subscription and methods for every mutation a customer-facing block needs.

Setup

import {
  provideContext,
  createSubscriptionService,
  createApiSubscriptionAdapter,
  createMockSubscriptionAdapter,
  SubscriptionServiceContext,
} from "@juo/blocks";

// Production
provideContext(
  root,
  SubscriptionServiceContext,
  createSubscriptionService(createApiSubscriptionAdapter({ baseUrl: "/api/v1" })),
);

// Development / Storybook
provideContext(
  root,
  SubscriptionServiceContext,
  createSubscriptionService(createMockSubscriptionAdapter()),
);

Shape

type SubscriptionService = {
  // Reactive state
  current: Signal<Subscription | null>;
  hasPendingBilling: Signal<boolean>;

  // Reads
  search(options?: SearchOptions): Promise<Result<PaginatedList<Subscription>>>;
  getById(id: string): Promise<Result<Subscription | null>>;
  getCurrentSubscriptionId(): string | null;
  getDefaultSubscriptionId(): Promise<Result<string | null>>;
  getAvailablePlans(subscriptionId: string): Promise<Result<SellingPlan[]>>;
  getUpsellProducts(params: { subscriptionId: string }): Promise<Result<UpsellProduct[]>>;

  // Items
  addItem(subscriptionId: string, item: PostableSubscriptionItem): Promise<Result>;
  removeItem(subscriptionId: string, itemId: string): Promise<Result>;
  addUpsellProduct(params: {
    subscriptionId: string;
    variantId: string;
    quantity: number;
    oneTime?: boolean;
  }): Promise<Result>;

  // Discounts
  addDiscount(subscriptionId: string, code: string): Promise<Result>;
  removeDiscount(subscriptionId: string, discountId: string): Promise<Result>;

  // Lifecycle
  reschedule(subscriptionId: string, newDate: string): Promise<Result<Subscription>>;
  pause(subscriptionId: string): Promise<Result<Subscription>>;
  resume(subscriptionId: string): Promise<Result<Subscription>>;
  reactivate(subscriptionId: string): Promise<Result<Subscription>>;
  cancel(subscriptionId: string, reason?: string): Promise<Result<Subscription>>;
};
A Subscription carries items, discounts, status (active | paused | canceled | expired | failed), nextDeliveryDate, frequency, and the owning customerId.

Reactive state

SignalDescription
currentThe currently selected subscription, or null if none is loaded. Updates after mutations.
hasPendingBillingtrue when a billing attempt is in-flight. Use this to guard mutations that would conflict with billing.

Example: pause / resume toggle

import {
  defineBlock,
  effect,
  untracked,
  injectContext,
  SubscriptionServiceContext,
} from "@juo/blocks";

defineBlock("PauseToggle", {
  group: "theme",
  schema: { /* ... */ } as const,
  initialValue: () => ({ props: {} }),
  renderer(block) {
    const el = document.createElement("button");
    const subscription = injectContext(el, SubscriptionServiceContext);

    el.addEventListener("click", async () => {
      const current = subscription.current.value;
      if (!current || subscription.hasPendingBilling.value) return;

      const result = current.status === "paused"
        ? await subscription.resume(current.id)
        : await subscription.pause(current.id);

      if (result._tag === "Failure") console.error(result.error);
    });

    effect(() => {
      const current = subscription.current.value;
      const pending = subscription.hasPendingBilling.value;
      untracked(() => {
        el.textContent = current?.status === "paused" ? "Resume" : "Pause";
        el.disabled = pending || !current;
      });
    });

    return el;
  },
});

Example: adding a discount

async function applyCode(id: string, code: string) {
  const result = await subscription.addDiscount(id, code);
  if (result._tag === "Failure") {
    throw new Error("Invalid code");
  }
  // subscription.current updates automatically
}