feat(subplat): add support for free access program#20764
Conversation
43ed500 to
d30c6c2
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds a “Free Access Program” path to SubPlat/EntPlat so capabilities can be granted to allowlisted users (via Strapi) even when they have no paid subscription, and surfaces that state across auth-server, Settings, and the payments subscription-management UI.
Changes:
- Introduces a new
@fxa/free-access-programlibrary that projects Strapiaccessentries into a cached snapshot, and provides a reconciler that diffs state and emits capability deltas. - Adds auth-server support: merges free-access capabilities into
profile:subscriptions, adds ahasFreeAccesssignal to/account, and exposes a Strapi webhook endpoint + periodic reconcile script. - Updates Settings and payments-next UI to display/manage “free access” experiences (nav link gating + subscription-management cards), including new validators and session email plumbing.
Reviewed changes
Copilot reviewed 100 out of 103 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.base.json | Adds TS path alias for @fxa/free-access-program. |
| packages/fxa-settings/src/models/contexts/AppContext.ts | Adds hasFreeAccess default value to Settings app context. |
| packages/fxa-settings/src/models/contexts/AccountStateContext.tsx | Extends account state with hasFreeAccess and mapping from unified data. |
| packages/fxa-settings/src/models/Account.ts | Adds hasFreeAccess to account model + getter. |
| packages/fxa-settings/src/lib/hooks/useAccountData.ts | Maps /account response hasFreeAccess into Settings state. |
| packages/fxa-settings/src/lib/account-storage.ts | Persists hasFreeAccess in account storage/state selectors. |
| packages/fxa-settings/src/components/Settings/Nav/index.tsx | Shows subscriptions entry point when user has paid subs or free access. |
| packages/fxa-settings/src/components/Settings/Nav/index.test.tsx | Adds test for subscriptions link visibility for allowlisted users. |
| packages/fxa-auth-server/test/support/jest-setup-env.ts | Loads reflect-metadata earlier for integration test harness. |
| packages/fxa-auth-server/test/remote/subscription_tests.in.spec.ts | Ensures CMS config has placeholder Strapi client values for tests. |
| packages/fxa-auth-server/scripts/free-access-program-reconcile.ts | Adds auth-server cron-style reconcile script entrypoint. |
| packages/fxa-auth-server/scripts/convert-customers-to-stripe-automatic-tax.ts | Loads reflect-metadata to avoid decorator metadata crashes in tests. |
| packages/fxa-auth-server/lib/routes/subscriptions/index.ts | Registers free-access Strapi webhook routes when reconciler is wired. |
| packages/fxa-auth-server/lib/routes/subscriptions/free-access-program-webhook.ts | Implements Strapi webhook handler with replay dedupe + reconcile dispatch. |
| packages/fxa-auth-server/lib/routes/subscriptions/free-access-program-webhook.spec.ts | Unit tests for webhook handler behavior and dedupe. |
| packages/fxa-auth-server/lib/routes/password.spec.ts | Updates mocks (TypeScript-safe mocks for logger/statsd). |
| packages/fxa-auth-server/lib/routes/account.ts | Adds free-access capability merge + hasFreeAccess flag in /account. |
| packages/fxa-auth-server/lib/routes/account.spec.ts | Updates tests to provide CapabilityService.hasFreeAccess mock shape. |
| packages/fxa-auth-server/lib/payments/processing-tasks-setup.ts | Loads reflect-metadata for processing-task scripts. |
| packages/fxa-auth-server/lib/payments/free-access-program-setup.ts | Wires Free Access manager + reconciler into TypeDI/Nest-ish deps. |
| packages/fxa-auth-server/lib/payments/free-access-in-process-notifier.ts | New notifier that resolves email→uid and calls capability fanout logic in-process. |
| packages/fxa-auth-server/lib/payments/free-access-in-process-notifier.spec.ts | Unit tests for notifier behavior and error handling. |
| packages/fxa-auth-server/lib/payments/capability.ts | Merges free-access capabilities; adds hasFreeAccess and processEmailListChange; safer empty-list short-circuit. |
| packages/fxa-auth-server/lib/payments/capability.spec.ts | Adds coverage for new merge and processEmailListChange behavior. |
| packages/fxa-auth-server/lib/oauth/grant.spec.ts | Ensures subscriptions enabled in config mock for grant tests. |
| packages/fxa-auth-server/lib/oauth/grant.js | Gates profile:subscriptions token enrichment on subscriptions-enabled. |
| packages/fxa-auth-server/jest.setup.js | Loads reflect-metadata for unit tests. |
| packages/fxa-auth-server/jest.config.js | Adds Jest moduleNameMapper for @fxa/free-access-program. |
| packages/fxa-auth-server/config/index.ts | Adds Free Access Program config + Strapi webhook secret config. |
| packages/fxa-auth-server/bin/key_server.js | Wires Free Access manager/reconciler into auth-server boot. |
| libs/shared/db/firestore/src/index.ts | Re-exports Firestore config types for consumers via package entrypoint. |
| libs/shared/cms/src/lib/types.ts | Adds FreeAccessCardContent type for subscription-management free access cards. |
| libs/shared/cms/src/lib/strapi.client.ts | Hardens webhook signature verification (prefix + length check). |
| libs/shared/cms/src/lib/strapi.client.spec.ts | Adds tests for webhook signature verification behavior. |
| libs/shared/cms/src/lib/strapi.client.config.ts | Allows localhost-like URIs (require_tld: false). |
| libs/shared/cms/src/lib/queries/accesses/query.ts | Adds Strapi GraphQL query to fetch accesses (matchers + offerings + capabilities). |
| libs/shared/cms/src/lib/queries/accesses/index.ts | Exports accesses query + factories + result type. |
| libs/shared/cms/src/lib/queries/accesses/factories.ts | Adds test factories for accesses query result shapes. |
| libs/shared/cms/src/lib/product-configuration.manager.ts | Adds helper to hydrate free-access cards for subscription-management UI. |
| libs/shared/cms/src/lib/product-configuration.manager.spec.ts | Tests for free-access card hydration and error handling. |
| libs/shared/cms/src/index.ts | Re-exports the new accesses query module. |
| libs/shared/cms/src/generated/graphql.ts | Updates generated GraphQL types to include Access/FreeAccessProgram. |
| libs/shared/cms/src/generated/gql.ts | Updates generated gql map to include the new Accesses query. |
| libs/payments/webhooks/src/lib/cms-webhooks.controller.spec.ts | Formatting import change (no behavior change). |
| libs/payments/ui/src/lib/nestapp/validators/GetSubManPageContentActionResult.ts | Adds freeAccess response validation shape. |
| libs/payments/ui/src/lib/nestapp/validators/GetSubManPageContentActionArgs.ts | Adds optional email argument for subscription-management page content. |
| libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts | Passes email through to subscription-management service. |
| libs/payments/ui/src/lib/nestapp/config.ts | Adds Free Access Program config block to payments UI RootConfig. |
| libs/payments/ui/src/lib/nestapp/app.module.ts | Registers FreeAccessProgramManager in Nest module providers. |
| libs/payments/ui/src/lib/client/components/FreeAccessContent/index.tsx | New client component rendering a free-access “included via org” card. |
| libs/payments/ui/src/lib/client/components/FreeAccessContent/index.test.tsx | Unit tests for FreeAccessContent rendering. |
| libs/payments/ui/src/lib/client/components/FreeAccessContent/en.ftl | New FTL string for the free-access card messaging. |
| libs/payments/ui/src/lib/actions/getSubManPageContent.ts | Adds session email to action call to hydrate free-access cards. |
| libs/payments/ui/src/index.ts | Exports FreeAccessContent component. |
| libs/payments/ui-auth/src/lib/session.ts | Adds getSessionEmail() helper. |
| libs/payments/ui-auth/src/lib/session.spec.ts | Tests for getSessionEmail(). |
| libs/payments/ui-auth/src/index.ts | Exports getSessionEmail. |
| libs/payments/management/src/lib/types.ts | Adds FreeAccessContent type for management response. |
| libs/payments/management/src/lib/subscriptionManagement.service.ts | Resolves free-access grants via manager + CMS card hydration and excludes paid offerings. |
| libs/payments/management/src/lib/subscriptionManagement.service.spec.ts | Adds unit coverage for free-access card behavior. |
| libs/payments/management/src/lib/subscriptionManagement.service.in.spec.ts | Ensures freeAccess field present in integration result shape. |
| libs/free-access-program/tsconfig.spec.json | New library test TS config. |
| libs/free-access-program/tsconfig.lib.json | New library build TS config. |
| libs/free-access-program/tsconfig.json | New library root TS config with references. |
| libs/free-access-program/src/lib/util/projectAccess.ts | Projects normalized Strapi access data into per-email entitlement records. |
| libs/free-access-program/src/lib/util/projectAccess.spec.ts | Unit tests for projection behavior and skip reasons. |
| libs/free-access-program/src/lib/util/parseStrictDate.ts | Parses strict YYYY-MM-DD to “start of next day UTC” expiry. |
| libs/free-access-program/src/lib/util/parseStrictDate.spec.ts | Unit tests for date parsing edge cases. |
| libs/free-access-program/src/lib/util/parseMatcherValue.ts | Parses Strapi [date, description] tuple matcher values. |
| libs/free-access-program/src/lib/util/parseMatcherValue.spec.ts | Unit tests for matcher parsing. |
| libs/free-access-program/src/lib/util/normalizeGraphQLAccess.ts | Normalizes AccessesQuery rows into a source-agnostic projection input. |
| libs/free-access-program/src/lib/util/normalizeGraphQLAccess.spec.ts | Unit tests for normalization behavior. |
| libs/free-access-program/src/lib/util/flattenOfferingCapabilities.ts | Flattens capabilities across offerings. |
| libs/free-access-program/src/lib/util/flattenOfferingCapabilities.spec.ts | Tests for capability flattening behavior. |
| libs/free-access-program/src/lib/util/flattenCapabilities.ts | Flattens clientId→capabilities map to a set of slugs for diffing. |
| libs/free-access-program/src/lib/util/flattenCapabilities.spec.ts | Tests for map flattening behavior. |
| libs/free-access-program/src/lib/util/filterByEntitlement.ts | Filters snapshot records by entitlement id. |
| libs/free-access-program/src/lib/util/filterByEntitlement.spec.ts | Tests for entitlement filtering. |
| libs/free-access-program/src/lib/util/diffCapabilities.ts | Diffs capability maps at slug level. |
| libs/free-access-program/src/lib/util/diffCapabilities.spec.ts | Tests for capability diffing. |
| libs/free-access-program/src/lib/util/collectOfferingApiIdentifiers.ts | Collects offering apiIdentifiers from linked offerings. |
| libs/free-access-program/src/lib/util/collectOfferingApiIdentifiers.spec.ts | Tests for offering id collection. |
| libs/free-access-program/src/lib/util/collectCapabilityMap.ts | Builds clientId→capability list from Strapi capability/service refs. |
| libs/free-access-program/src/lib/util/collectCapabilityMap.spec.ts | Tests for capability map collection behavior. |
| libs/free-access-program/src/lib/util/buildSnapshotKey.ts | Defines composite snapshot key format. |
| libs/free-access-program/src/lib/util/buildSnapshotKey.spec.ts | Tests for snapshot key building. |
| libs/free-access-program/src/lib/free-access-program.types.ts | Introduces shared types + notifier token contract. |
| libs/free-access-program/src/lib/free-access-program.reconciler.service.ts | Reconciler service that diffs snapshots and fans out changes. |
| libs/free-access-program/src/lib/free-access-program.reconciler.service.spec.ts | Unit tests for reconciler behavior, metrics, and error handling. |
| libs/free-access-program/src/lib/free-access-program.manager.ts | Manager that fetches Strapi accesses and caches the projected snapshot. |
| libs/free-access-program/src/lib/free-access-program.manager.spec.ts | Unit tests for manager read/query behavior. |
| libs/free-access-program/src/lib/free-access-program.factories.ts | Test factories for the new library’s domain types. |
| libs/free-access-program/src/lib/free-access-program.client.config.ts | Config class + mock provider for the new library. |
| libs/free-access-program/src/index.ts | Library public exports. |
| libs/free-access-program/README.md | Initial documentation for the new library (currently inconsistent with implementation). |
| libs/free-access-program/project.json | Nx project configuration for the new library. |
| libs/free-access-program/package.json | Declares @fxa/free-access-program package metadata. |
| libs/free-access-program/jest.config.ts | Jest config for the new library. |
| libs/free-access-program/.swcrc | SWC config enabling decorators metadata for tests/build. |
| apps/payments/next/app/[locale]/subscriptions/manage/page.tsx | Renders free-access cards section on subscription management page. |
| apps/payments/next/app/[locale]/subscriptions/manage/en.ftl | Adds localized strings for free-access section headings/aria labels. |
| apps/payments/next/.env | Adds Free Access Program client config env vars (one typo noted). |
| apps/payments/api/src/config/index.ts | Switches FirestoreConfig import to package export path. |
Files not reviewed (2)
- libs/shared/cms/src/generated/gql.ts: Generated file
- libs/shared/cms/src/generated/graphql.ts: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * filters in-memory. | ||
| */ | ||
| @Injectable() | ||
| export class FreeAccessProgramManager { |
There was a problem hiding this comment.
FreeAccessProgramManager -> FreeAccessProgramConfigurationManager to be in line with our other configuration managers that hit strapi
| * email can't block the rest. | ||
| */ | ||
| @Injectable() | ||
| export class FreeAccessProgramReconcilerService { |
There was a problem hiding this comment.
I'm worried about this diffing strategy because it relies on nothing else purging/refreshing the cache during a Strapi update (the tail of a TTL).
I think it might be a better fit to use the information stored within the Strapi webhook to decide what to do.
| * snapshot. Matches the doc-id shape the prior Firestore repository used so | ||
| * any operator tooling that reads keys can target the same identity. | ||
| */ | ||
| export function buildSnapshotKey( |
There was a problem hiding this comment.
Related to this function but not to this line - I notice we're not using the Strapi client's built-in caching for queries, but instead re-creating our own on top of it. What's the motivation there?
| // MUST be loaded before any module that evaluates a class-validator or | ||
| // class-transformer decorator at import time — e.g. `firestore.config.ts` | ||
| // reachable via `../firestore-db` below. CLI scripts that spawn through | ||
| // `node -r ts-node/register scripts/X.ts` don't get the polyfill from | ||
| // NestJS bootstrap the way the running auth-server does, so we load it | ||
| // here at the entry point shared by every processing-task script. | ||
| import 'reflect-metadata'; |
There was a problem hiding this comment.
This seems like an odd place to put this given the comment about it's importance.
| * user: invalidate their profile cache and broadcast the added/removed | ||
| * capabilities to attached services via the existing SQS pipeline. | ||
| * | ||
| * TODO(FXA-XXXXX): Replace this with an event-driven flow where |
There was a problem hiding this comment.
This doesn't reference anything
| FREE_ACCESS_PROGRAM_CLIENT_CONFIG__FIRESTORE_CACHE_COLLECTION_NAME=subplat-free-access-program-cache | ||
| FREE_ACCESS_PROGRAM_CLIENT_CONFIG__MEM_CACHE_T_T_L=300 # 5 minutes | ||
| FREE_ACCESS_PROGRAM_CLIENT_CONFIG__FIRESTORE_CACHE_TTL=1800 # 30 minutes | ||
| FREE_ACCESS_PROGRAM_CLIENT_CONFIG__FIRESTORE_OFFLINE_CACHE_TTL=604800 # 7 days |
There was a problem hiding this comment.
Do we need new/separate env vars for this? I would suggest we treat this the same way we do the Strapi query cache.
| const DEFAULT_FIRESTORE_OFFLINE_CACHE_TTL_SECONDS = 604800; // 7 days | ||
| const DEFAULT_FIRESTORE_CACHE_TTL_SECONDS = 1800; // 30 minutes | ||
| const DEFAULT_MEM_CACHE_TTL_SECONDS = 300; // 5 minutes |
There was a problem hiding this comment.
Can these values come from the config provider rather than being re-set here?
| async getCachedProjection(): Promise<FreeAccessSnapshot> { | ||
| return this.fetchAndProject(); | ||
| } | ||
|
|
||
| /** | ||
| * Reconciler-side entry point: always re-fetches from Strapi, skipping | ||
| * the cache. Used to compute the "after" state when diffing a webhook or | ||
| * cron sweep against the cached "before". | ||
| */ | ||
| async getFreshProjection(): Promise<FreeAccessSnapshot> { | ||
| return this.fetchAndProject(); | ||
| } | ||
|
|
||
| /** | ||
| * Read the cached snapshot WITHOUT triggering a Strapi back-fill on miss. | ||
| * Returns `undefined` if neither cache layer has the snapshot — distinct | ||
| * from "cached snapshot of {}". The reconciler needs this distinction to | ||
| * avoid producing a "fresh-vs-fresh-because-cold-cache" diff (which would | ||
| * be empty and silently drop notifications). | ||
| */ | ||
| async peekCachedProjection(): Promise<FreeAccessSnapshot | undefined> { | ||
| const fromMemory = (await this.memoryCacheAdapter.get( | ||
| SNAPSHOT_CACHE_KEY | ||
| )) as FreeAccessSnapshot | undefined; | ||
| if (fromMemory) return fromMemory; | ||
| const fromFirestore = (await this.firestoreCacheAdapter.get( | ||
| SNAPSHOT_CACHE_KEY | ||
| )) as FreeAccessSnapshot | undefined; | ||
| return fromFirestore; | ||
| } |
There was a problem hiding this comment.
This pattern seems delicate/fragile (referencing peekCachedProjection and it's comment). You mustn't touch the cache (particularly close to the end of it's memcache TTL) else you risk messing up the diff. Furthermore what happens if something falls out of Firestore cache entirely?
I guess technically since it's stale while revalidate calling getCachedProjection at all will overwrite which is potentially concerning too since we rely on that for the diff.
53d13b7 to
0964cfd
Compare
Because - Transfer ownership of the Mozilla VPN Free Access program to the SubPlat and EntPlat teams, which aligns with the goal of evolving the subscirption platform beyond subscriptions. - Allow other Mozilla services to also utilize the Free Access Program. This commit - Adds support for the Free Access Program, by broadcasting capabilities for enabled customers, even if they don't have a subscription. - Read list of emails from Strapi and what capabilities should be provided to these users. - On auth-server /profile query, return the relevant capability if the user email is in the list configured in Strapi. - Add webhook listener to `auth-server` that listens for changes in Strapi and then broadcasts profile changed for entries that were added, expired or removed. - Adds a script to be added as a cron job, to broadcast a profile change to RPs when access has expired. Closes PAY-3780
0964cfd to
dcde508
Compare
Because
This pull request
auth-serverthat listens for changes in Strapi and then broadcasts profile changed for entries that were added, expired or removed.to RPs when access has expired.
Issue that this pull request solves
Closes: PAY-3780
Checklist
Put an
xin the boxes that applyHow to review (Optional)
Screenshots (Optional)
Please attach the screenshots of the changes made in case of change in user interface.
Other information (Optional)
Any other information that is important to this pull request.