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
Keep the feature on.
paymentsis on unlesssrc/custom/features.tslists it inoff. See Choose your app's features.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.
Add a product under Products. Link it to its Dodo product id, and set the two fields your app reads:
Field What it is Premium type The plan key written to the buyer's record, such as plusorgold. Your app's code checks this string, so agree on it.Tier rank A number, higher means more access. A person holds one plan at a time; a lower-ranked purchase never replaces a higher active one. Let people buy it.
/pricinglists 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 lock | Use |
|---|---|
| A whole page | entitledGuard 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 code | EntitlementService: 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();
}
}
- The plan is read once, when
load()runs. Call it again to pick up a purchase made since. - The browser check is for showing and hiding. Premium data or a paid action also needs a security rule, or a check in a Cloud Function, that reads the same fields on the record.
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.
Sign the person in and take their record id from the
arccms_uidclaim (Follow the account contract).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 asgetFirestore(app, 'arccms').Map each Premium type to your own rank, and decide from
isPro,premiumStatus,premiumExpiresAtandpremiumType, failing closed. The code and every field are in Premium entitlements.
- The checkout return page proves nothing. Access starts when the payment webhook writes the fields, a few seconds later.
- Only the server writes them. The rules refuse every premium and credit field from the person's own browser, so never unlock optimistically.
- Credits are spent with the
arccms-consumeCreditscallable:{ amount, note }(a whole number above 0, and an optional note) returns{ balance, spent }, and fails with "Insufficient credits." when the balance is too low.