Add payments and premium features

Arc CMS sells plans through Dodo Payments and records on each person's record which plan they hold. Your app reads that record and decides what to unlock.

Arc CMS records the plan; your app enforces it. The setup screens, webhook and emails are in Payments; the fields and their life cycle are in Premium entitlements.

Turn payments on and sell a plan

  1. Keep the feature on. payments is on unless src/custom/features.ts lists it in off. See Choose your app's features.

  2. Connect Dodo Payments in Settings, Payments: the API keys, the webhook secret, and the pages people return to after checkout. The steps are in Payments.

  3. Add a product under Products. Link it to its Dodo product id, and set the two fields your app reads:

    FieldWhat it is
    Premium typeThe plan key written to the buyer's record, such as plus or gold. Your app's code checks this string, so agree on it.
    Tier rankA number, higher means more access. A person holds one plan at a time; a lower-ranked purchase never replaces a higher active one.
  4. Let people buy it. /pricing lists the active products and starts checkout; signed-out visitors are sent to sign in first and come back. That is the recommended path.

Your own buy button: call the createCheckoutSession callable with { productId } (the Products document id). It returns { checkoutUrl, sessionId, tierApplied }; send the browser to checkoutUrl.

import { inject } from '@angular/core';
import { Functions } from '@angular/fire/functions';
import { arcCallable } from '../../app/core/config/arc-functions';

private functions = inject(Functions);

async buy(productId: string) {
    const call = arcCallable<{ productId: string }, { checkoutUrl: string }>(this.functions, 'createCheckoutSession');
    const { data } = await call({ productId });
    window.location.href = data.checkoutUrl;
}

Checkout needs an email address on the sign-in account, and it reads it from Firebase Auth, not from the users record. Someone who signed up with a phone number has none, and createCheckoutSession refuses with "Your account has no email address." Offer phone-only people another way to buy, or ask them to add an email under Profile.

Lock a feature in your app

To lockUse
A whole pageentitledGuard on the route. Signed out goes to sign-in; signed in without a plan goes to /pricing.
Part of a page*appIfEntitled shows its content to anyone with a plan; *appIfEntitled="2" to a tier rank of 2 or more.
Something in codeEntitlementService: isPro(), tierRank(), hasTier(minRank) and more.

All three are in src/app/pages/user/, and Premium entitlements describes each. A page of your own with free and paid parts, on a route protected with userGuard so the person is known when it opens:

// src/custom/pages/lessons.page.ts
import { Component, inject, OnInit } from '@angular/core';
import { EntitlementService } from '../../app/pages/user/entitlement.service';
import { IfEntitledDirective } from '../../app/pages/user/if-entitled.directive';

@Component({
    standalone: true,
    imports: [IfEntitledDirective],
    template: `
        <section>Free lessons</section>
        <section *appIfEntitled="2">Gold lessons</section>
    `,
})
export default class LessonsPage implements OnInit {
    private entitlements = inject(EntitlementService);

    ngOnInit() {
        this.entitlements.load().subscribe();
    }
}

Read the plan from a separate app

An app outside this repository, in the same Firebase project, reads the plan straight from the person's users record, which its owner may read. There is no callable for it: the record is the source of truth.

  1. Sign the person in and take their record id from the arccms_uid claim (Follow the account contract).

  2. Listen to users/{arccms_uid} in the Arc CMS database. When Arc CMS runs in its own named database (Share a Firebase project), open that one, such as getFirestore(app, 'arccms').

  3. Map each Premium type to your own rank, and decide from isPro, premiumStatus, premiumExpiresAt and premiumType, failing closed. The code and every field are in Premium entitlements.