Skip to content

Commit 840a20e

Browse files
committed
feat(subplat): add support for free access program
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 `payments-api` that listens for changes in Strapi and then calls an API in auth-server, which broadcasts capability added or removed via event-broker to RPs. - Show Paid Subscriptions option to user that have access via a B2B subscription. On the subscription management page added a new section indicating services provided by the customers organization. Closes PAY-3780
1 parent f4f78e1 commit 840a20e

91 files changed

Lines changed: 4991 additions & 37 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/payments/api/.env

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Auth Server
2+
AUTH_SERVER_EMAIL_CAPABILITY_CONFIG__BASE_URL=http://localhost:9000/v1
3+
AUTH_SERVER_EMAIL_CAPABILITY_CONFIG__SUBSCRIPTIONS_SECRET=devsecret
4+
15
# MySQLConfig
26
MYSQL_CONFIG__DATABASE=fxa
37
MYSQL_CONFIG__HOST=::1
@@ -100,3 +104,7 @@ METERING_CONFIG__CLOUD_TASKS__THRESHOLD__TASK_URL=http://127.0.0.1:3000/v1/meter
100104
METERING_CONFIG__CLOUD_TASKS__THRESHOLD__QUEUE_NAME=metering-threshold-checks
101105
METERING_CONFIG__CLOUD_TASKS__THRESHOLD__BUCKET_SIZE_MS=300000
102106
METERING_CONFIG__CLOUD_TASKS__THRESHOLD__SCHEDULE_DELAY_MS=420000
107+
108+
# Free Access Program Client Config
109+
# NB: This needs to match config in payments-next
110+
FREE_ACCESS_PROGRAM_CLIENT_CONFIG__COLLECTION_NAME=subplat-free-access-program

apps/payments/api/src/app/app.module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ import {
4949
PaypalCustomerManager,
5050
} from '@fxa/payments/paypal';
5151
import { CurrencyManager } from '@fxa/payments/currency';
52+
import {
53+
AuthServerEmailCapabilityClient,
54+
FreeAccessProgramManager,
55+
FreeAccessProgramNotifierService,
56+
FreeAccessProgramReconcilerService,
57+
FreeAccessProgramWebhookController,
58+
FreeAccessProgramWebhookService,
59+
} from '@fxa/free-access-program';
5260
import { AccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account';
5361
import { AccountManager } from '@fxa/shared/account/account';
5462
import { CartManager } from '@fxa/payments/cart';
@@ -84,6 +92,7 @@ import { PaymentsMetricsAggregatorService } from '@fxa/payments/metrics-aggregat
8492
AppController,
8593
BillingAndSubscriptionsController,
8694
CmsWebhooksController,
95+
FreeAccessProgramWebhookController,
8796
FxaWebhooksController,
8897
StripeWebhooksController,
8998
],
@@ -106,7 +115,12 @@ import { PaymentsMetricsAggregatorService } from '@fxa/payments/metrics-aggregat
106115
PaymentsEmitterService,
107116
PriceManager,
108117
ProductManager,
118+
AuthServerEmailCapabilityClient,
109119
FirestoreProvider,
120+
FreeAccessProgramManager,
121+
FreeAccessProgramNotifierService,
122+
FreeAccessProgramReconcilerService,
123+
FreeAccessProgramWebhookService,
110124
GoogleIapClient,
111125
GoogleIapPurchaseManager,
112126
StatsDProvider,

apps/payments/api/src/config/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { IsDefined, ValidateNested } from 'class-validator';
33

44
import { CurrencyConfig } from '@fxa/payments/currency';
55
import { MeteringConfig } from '@fxa/entitlements/metering';
6+
import {
7+
AuthServerEmailCapabilityConfig,
8+
FreeAccessProgramClientConfig,
9+
} from '@fxa/free-access-program';
610
import { AppleIapClientConfig, GoogleIapClientConfig } from '@fxa/payments/iap';
711
import { PaymentsGleanConfig } from '@fxa/payments/metrics';
812
import { PaypalClientConfig } from '@fxa/payments/paypal';
@@ -11,7 +15,7 @@ import { StrapiClientConfig } from '@fxa/shared/cms';
1115
import { MySQLConfig } from '@fxa/shared/db/mysql/core';
1216
import { FxaWebhookConfig, StripeEventConfig } from '@fxa/payments/webhooks';
1317
import { StatsDConfig } from '@fxa/shared/metrics/statsd';
14-
import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config';
18+
import { FirestoreConfig } from '@fxa/shared/db/firestore';
1519
import { FxaOAuthConfig } from '@fxa/payments/auth';
1620

1721
export class RootConfig {
@@ -84,4 +88,14 @@ export class RootConfig {
8488
@ValidateNested()
8589
@IsDefined()
8690
public readonly meteringConfig!: Partial<MeteringConfig>;
91+
92+
@Type(() => FreeAccessProgramClientConfig)
93+
@ValidateNested()
94+
@IsDefined()
95+
public readonly freeAccessProgramClientConfig!: Partial<FreeAccessProgramClientConfig>;
96+
97+
@Type(() => AuthServerEmailCapabilityConfig)
98+
@ValidateNested()
99+
@IsDefined()
100+
public readonly authServerEmailCapabilityConfig!: Partial<AuthServerEmailCapabilityConfig>;
87101
}

apps/payments/next/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ CHURN_INTERVENTION_CONFIG__ENABLED=
9999
# Free Trial Config
100100
FREE_TRIAL_CONFIG__FIRESTORE_COLLECTION_NAME=freeTrials
101101

102+
# Free Access Program Client Config
103+
# NB: This needs to match config in payments-api
104+
FREE_ACCESS_PROGRAM_CLIENT_CONFIG__COLLECTION_NAME=subplat-free-access-program
105+
102106
# StatsD Config
103107
STATS_D_CONFIG__SAMPLE_RATE=
104108
STATS_D_CONFIG__MAX_BUFFER_SIZE=

apps/payments/next/app/[locale]/subscriptions/manage/en.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ subscription-management-page-banner-warning-link-no-payment-method = Add a payme
55
subscription-management-subscriptions-heading = Subscriptions
66
subscription-management-free-trial-heading = Free trials
77
subscription-management-your-free-trials-aria = Your free trials
8+
subscription-management-free-access-heading = Services included with your account
9+
subscription-management-your-free-access-aria = Services included with your account
810
911
# Heading for mobile only quick links menu
1012
subscription-management-jump-to-heading = Jump to

apps/payments/next/app/[locale]/subscriptions/manage/page.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { SubPlatPaymentMethodType } from '@fxa/payments/customer';
1414
import {
1515
Banner,
1616
BannerVariant,
17+
FreeAccessContent,
1718
formatPlanInterval,
1819
FreeTrialContent,
1920
getCardIcon,
@@ -76,6 +77,7 @@ export default async function Manage({
7677
appleIapSubscriptions,
7778
googleIapSubscriptions,
7879
trialSubscriptions,
80+
freeAccess,
7981
} = await getSubManPageContentAction(
8082
{ ...resolvedParams },
8183
{ ...resolvedSearchParams },
@@ -268,6 +270,44 @@ export default async function Manage({
268270
</nav>
269271
)}
270272

273+
{freeAccess && freeAccess.length > 0 && (
274+
<section
275+
id="free-access"
276+
className="scroll-mt-16"
277+
aria-labelledby="free-access-heading"
278+
>
279+
<h2
280+
id="free-access-heading"
281+
className="font-bold px-4 pt-8 pb-4 text-lg tablet:px-6"
282+
>
283+
{l10n.getString(
284+
'subscription-management-free-access-heading',
285+
'Services included with your account'
286+
)}
287+
</h2>
288+
<ul
289+
aria-label={l10n.getString(
290+
'subscription-management-your-free-access-aria',
291+
'Services included with your account'
292+
)}
293+
>
294+
{freeAccess.map((grant, index) => (
295+
<li
296+
key={`${grant.clientId}-${index}`}
297+
aria-labelledby={`${grant.clientId}-free-access-information`}
298+
className="leading-6 pb-4 last:pb-0"
299+
>
300+
<div className="w-full py-6 text-grey-600 bg-white rounded-xl border border-grey-200 opacity-100 shadow-[0_0_16px_0_rgba(0,0,0,0.08)] tablet:px-6 tablet:py-8">
301+
<div className="flex flex-col px-4 tablet:px-0 tablet:flex-row tablet:items-start">
302+
<FreeAccessContent freeAccess={grant} />
303+
</div>
304+
</div>
305+
</li>
306+
))}
307+
</ul>
308+
</section>
309+
)}
310+
271311
{trialSubscriptions.length > 0 && (
272312
<section
273313
id="free-trial"

libs/free-access-program/.swcrc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"jsc": {
3+
"target": "es2017",
4+
"parser": {
5+
"syntax": "typescript",
6+
"decorators": true,
7+
"dynamicImport": true
8+
},
9+
"transform": {
10+
"decoratorMetadata": true,
11+
"legacyDecorator": true
12+
}
13+
}
14+
}

libs/free-access-program/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# free-access-program
2+
3+
Firestore-backed projection of Strapi access records that grant
4+
free access to RP services. Each document carries a per-(email, entitlement)
5+
`expiresAt` enforced by a Firestore TTL policy; when a document is removed
6+
(TTL reap or manual delete), the resulting Eventarc `onDelete` event fans out
7+
a `subscription:update` notification so consumers re-resolve the user's
8+
capabilities.
9+
10+
This library owns the read/write data layer only. The Strapi reconciler and
11+
the Eventarc → SNS notifier are wired up in the consuming service
12+
(`payments-api`).
13+
14+
## Building
15+
16+
Run `nx build free-access-program` to build the library.
17+
18+
## Running unit tests
19+
20+
Run `nx test-unit free-access-program` to execute the unit tests via
21+
[Jest](https://jestjs.io).
22+
23+
## Reconcile script
24+
25+
Run `nx run free-access-program:reconcile-all` to invoke a full Strapi →
26+
Firestore reconciliation pass. Intended to be wired to an external scheduler
27+
(Cloud Scheduler / Kubernetes CronJob) at the operator's preferred cadence.
28+
29+
Required env vars (same `__` separator and `key_transformer` conventions as
30+
the payments-api `RootConfig`):
31+
32+
```
33+
FIRESTORE_CONFIG__PROJECT_ID=...
34+
FIRESTORE_CONFIG__KEY_FILENAME=... # or FIRESTORE_CONFIG__CREDENTIALS__*
35+
STRAPI_CLIENT_CONFIG__GRAPHQL_API_URI=...
36+
STRAPI_CLIENT_CONFIG__API_KEY=...
37+
FREE_ACCESS_PROGRAM_CLIENT_CONFIG__COLLECTION_NAME=free-access-program
38+
STATS_D_CONFIG__HOST=...
39+
STATS_D_CONFIG__PORT=...
40+
# Auth-server endpoint the notifier posts revocations to. Auth-server
41+
# resolves email → uid, invalidates the profile cache, and broadcasts
42+
# the `subscription:update` event over its existing SNS pipeline.
43+
AUTH_SERVER_EMAIL_CAPABILITY_CONFIG__BASE_URL=https://api.accounts.firefox.com
44+
AUTH_SERVER_EMAIL_CAPABILITY_CONFIG__SUBSCRIPTIONS_SECRET=...
45+
```
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* eslint-disable */
2+
import { readFileSync } from 'fs';
3+
import { Config } from 'jest';
4+
5+
// Reading the SWC compilation config and remove the "exclude"
6+
// for the test files to be compiled by SWC
7+
const { exclude: _, ...swcJestConfig } = JSON.parse(
8+
readFileSync(`${__dirname}/.swcrc`, 'utf-8')
9+
);
10+
11+
// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves.
12+
// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude"
13+
if (swcJestConfig.swcrc === undefined) {
14+
swcJestConfig.swcrc = false;
15+
}
16+
17+
const config: Config = {
18+
displayName: 'free-access-program',
19+
preset: '../../jest.preset.js',
20+
transform: {
21+
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
22+
},
23+
moduleFileExtensions: ['ts', 'js', 'html'],
24+
testEnvironment: 'node',
25+
coverageDirectory: '../../coverage/libs/free-access-program',
26+
reporters: [
27+
'default',
28+
[
29+
'jest-junit',
30+
{
31+
outputDirectory: 'artifacts/tests/free-access-program',
32+
outputName: 'free-access-program-jest-unit-results.xml',
33+
},
34+
],
35+
],
36+
};
37+
38+
export default config;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "@fxa/free-access-program",
3+
"version": "0.0.1"
4+
}

0 commit comments

Comments
 (0)