packages/fxa-shared/subscriptions/types.ts (190 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import cloneDeep from 'lodash.clonedeep';
import { Stripe } from 'stripe';
import {
AppStoreSubscription,
PlayStoreSubscription,
} from '../dto/auth/payments/iap-subscription';
import { LatestInvoiceItems } from '../dto/auth/payments/invoice';
import { PlanConfigurationDtoT } from '../dto/auth/payments/plan-configuration';
export type PlanInterval = Stripe.Plan['interval'];
export interface RawMetadata {
[propName: string]: any;
}
// A mapping of OAuth client ids to their corresponding capabilities.
export type ClientIdCapabilityMap = Record<string, readonly string[]>;
export const ClientIdCapabilityMap = {
merge(
a: ClientIdCapabilityMap,
b: ClientIdCapabilityMap
): ClientIdCapabilityMap {
return Object.entries(b).reduce((acc, [clientId, capabilities]) => {
if (!acc[clientId]) {
acc[clientId] = cloneDeep(capabilities);
return acc;
}
for (const capability of capabilities) {
if (!acc[clientId].includes(capability)) {
acc[clientId] = [...acc[clientId], capability];
}
}
return acc;
}, cloneDeep(a));
},
};
export interface Plan {
amount: number | null;
currency: string;
interval_count: number;
interval: PlanInterval;
plan_id: string;
plan_metadata: RawMetadata | null;
plan_name?: string;
product_id: string;
product_metadata: RawMetadata | null;
product_name: string;
active: boolean;
// TODO remove the '?' here when removing the SUBSCRIPTIONS_FIRESTORE_CONFIGS_ENABLED feature flag
configuration?: PlanConfigurationDtoT | null;
}
export enum CheckoutType {
WITH_ACCOUNT = 'with-account',
WITHOUT_ACCOUNT = 'without-account',
}
export type ConfiguredPlan = Stripe.Plan & {
configuration: PlanConfigurationDtoT | null;
};
export interface PlanMetadata {
// note: empty for now, but may be expanded in the future
}
// https://mozilla.github.io/ecosystem-platform/tutorials/subscription-platform#product-metadata
export interface ProductMetadata {
appStoreLink?: string;
capabilities?: string;
emailIconURL?: string | null;
newsletterSlug?: string;
newsletterLabelTextCode?: string;
playStoreLink?: string;
productOrder?: string | null;
productSet: string[];
upgradeCTA?: string | null;
webIconBackground?: string | null;
webIconURL: string | null;
'product:termsOfServiceDownloadURL': string;
'product:termsOfServiceURL': string;
'product:privacyNoticeDownloadURL'?: string;
'product:privacyNoticeURL': string;
'product:cancellationSurveyURL'?: string;
successActionButtonURL: string | null;
// capabilities:{clientID}: string // filtered out or ignored for now
}
// The ProductDetails type is exploded out into enums describing keys to
// make Stripe metadata parsing & validation easier.
export enum ProductDetailsStringProperties {
'name',
'subtitle',
'successActionButtonLabel',
'termsOfServiceURL',
'termsOfServiceDownloadURL',
'privacyNoticeURL',
'privacyNoticeDownloadURL',
'cancellationSurveyURL',
}
export enum ProductDetailsListProperties {
'details',
}
export type ProductDetailsStringProperty =
keyof typeof ProductDetailsStringProperties;
export type ProductDetailsListProperty =
keyof typeof ProductDetailsListProperties;
export type ProductDetails = {
[key in ProductDetailsStringProperty]?: string;
} & { [key in ProductDetailsListProperty]?: string[] };
export type AbbrevProduct = {
product_id: string;
product_metadata: Stripe.Product['metadata'];
product_name: string;
};
export type AbbrevPlan = {
amount: Stripe.Plan['amount'];
currency: Stripe.Plan['currency'];
interval_count: Stripe.Plan['interval_count'];
interval: Stripe.Plan['interval'];
plan_id: string;
plan_metadata: Stripe.Plan['metadata'];
plan_name: string;
product_id: string;
product_metadata: Stripe.Product['metadata'];
product_name: string;
active: boolean;
// TODO remove the '?' here when removing the SUBSCRIPTIONS_FIRESTORE_CONFIGS_ENABLED feature flag
configuration?: PlanConfigurationDtoT | null;
};
// Do not re-order the list items without updating their references.
export const SUBSCRIPTION_TYPES = ['web', 'iap_google', 'iap_apple'] as const;
export type SubscriptionTypes = typeof SUBSCRIPTION_TYPES;
export type SubscriptionType = SubscriptionTypes[number];
export const MozillaSubscriptionTypes = {
WEB: SUBSCRIPTION_TYPES[0],
IAP_GOOGLE: SUBSCRIPTION_TYPES[1],
IAP_APPLE: SUBSCRIPTION_TYPES[2],
} as const;
export type WebSubscription = Pick<
Stripe.Subscription,
| 'created'
| 'current_period_end'
| 'current_period_start'
| 'cancel_at_period_end'
> &
Partial<Pick<Stripe.Charge, 'failure_code' | 'failure_message'>> & {
_subscription_type: SubscriptionTypes[0];
end_at: Stripe.Subscription['ended_at'];
latest_invoice: string;
latest_invoice_items: LatestInvoiceItems;
plan_id: Stripe.Plan['id'];
product_name: Stripe.Product['name'];
product_id: Stripe.Product['id'];
status: Omit<
Stripe.Subscription.Status,
'incomplete' | 'incomplete_expired'
>;
subscription_id: Stripe.Subscription['id'];
promotion_amount_off?: number | null;
promotion_code?: string;
promotion_duration: string | null;
promotion_end: number | null;
promotion_name?: string | null;
promotion_percent_off?: number | null;
};
export type IapSubscription = PlayStoreSubscription | AppStoreSubscription;
export type MozillaSubscription = WebSubscription | IapSubscription;
export const PAYPAL_PAYMENT_ERROR_MISSING_AGREEMENT = 'missing_agreement';
export const PAYPAL_PAYMENT_ERROR_FUNDING_SOURCE = 'funding_source';
export type PaypalPaymentError =
| typeof PAYPAL_PAYMENT_ERROR_MISSING_AGREEMENT
| typeof PAYPAL_PAYMENT_ERROR_FUNDING_SOURCE;
export type SentEmailParams = {
subscriptionId: string;
};
// Used to represent upgrade eligibility only
// Invalid is used in cases where plan is not an upgrade/downgrade
// including new subscriptions
export const SubscriptionUpdateEligibility = {
UPGRADE: 'upgrade',
DOWNGRADE: 'downgrade',
INVALID: 'invalid',
} as const;
// Used to represent plan eligibility in general
export enum SubscriptionEligibilityResult {
CREATE = 'create',
UPGRADE = 'upgrade',
DOWNGRADE = 'downgrade',
BLOCKED_IAP = 'blocked_iap',
INVALID = 'invalid',
}
export type SubscriptionUpdateEligibility =
(typeof SubscriptionUpdateEligibility)[keyof typeof SubscriptionUpdateEligibility];
export enum SubscriptionStripeErrorType {
NO_MIN_CHARGE_AMOUNT = 'Currency does not have a minimum charge amount available.',
}
export class SubscriptionStripeError extends Error {
constructor(message: string) {
super(message);
this.name = 'SubscriptionStripeError';
}
}
export type InvoicePreview = [
invoicePreview: Stripe.UpcomingInvoice,
proratedInvoice?: Stripe.UpcomingInvoice,
];
export type SubscriptionChangeEligibility = {
subscriptionEligibilityResult: SubscriptionEligibilityResult;
eligibleSourcePlan?: AbbrevPlan;
redundantOverlaps?: SubscriptionChangeEligibility[];
};