Merge pull request #16856 from mozilla/fxa-7810-add-payment-element

feat(next): add payment element and submit
This commit is contained in:
Reino Muhl 2024-05-08 11:13:29 -04:00 коммит произвёл GitHub
Родитель 69878baabb 2fb70a6886
Коммит cea7165f54
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
30 изменённых файлов: 489 добавлений и 159 удалений

Просмотреть файл

@ -25,8 +25,8 @@ MYSQL_CONFIG__ACQUIRE_TIMEOUT_MILLIS=
GEODB_CONFIG__DB_PATH=../../../libs/shared/geodb/db/cities-db.mmdb
# GeoDBManagerConfig
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__COUNTRY_CODE=
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__POSTAL_CODE=
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__COUNTRY_CODE=US
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__POSTAL_CODE=11211
# StripeConfig
STRIPE_CONFIG__API_KEY=11233
@ -50,3 +50,16 @@ CONTENTFUL_CLIENT_CONFIG__GRAPHQL_ENVIRONMENT=dev
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_COLLECTION_NAME=contentfulQueryCacheCollection
CONTENTFUL_CLIENT_CONFIG__MEM_CACHE_T_T_L=
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_T_T_L=
# Firestore Config
FIRESTORE_CONFIG__CREDENTIALS__CLIENT_EMAIL=
FIRESTORE_CONFIG__CREDENTIALS__PRIVATE_KEY=
FIRESTORE_CONFIG__KEY_FILENAME=
FIRESTORE_CONFIG__PROJECT_ID=
# StatsD Config
STATS_D_CONFIG__SAMPLE_RATE=
STATS_D_CONFIG__MAX_BUFFER_SIZE=
STATS_D_CONFIG__HOST=
STATS_D_CONFIG__PORT=
STATS_D_CONFIG__PREFIX=

Просмотреть файл

@ -44,3 +44,16 @@ CONTENTFUL_CLIENT_CONFIG__GRAPHQL_ENVIRONMENT=master
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_COLLECTION_NAME=contentfulQueryCacheCollection
CONTENTFUL_CLIENT_CONFIG__MEM_CACHE_T_T_L= 300
CONTENTFUL_CLIENT_CONFIG__FIRESTORE_CACHE_T_T_L= 604800
# Firestore Config
FIRESTORE_CONFIG__CREDENTIALS__CLIENT_EMAIL=
FIRESTORE_CONFIG__CREDENTIALS__PRIVATE_KEY=
FIRESTORE_CONFIG__KEY_FILENAME=
FIRESTORE_CONFIG__PROJECT_ID=
# StatsD Config
STATS_D_CONFIG__SAMPLE_RATE=
STATS_D_CONFIG__MAX_BUFFER_SIZE=
STATS_D_CONFIG__HOST=
STATS_D_CONFIG__PORT=
STATS_D_CONFIG__PREFIX=

Просмотреть файл

@ -1,18 +1,17 @@
/* 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 Stripe from 'stripe';
import {
app,
handleStripeErrorAction,
getCartOrRedirectAction,
SupportedPages,
} from '@fxa/payments/ui/server';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
import { auth, signIn, signOut } from 'apps/payments/next/auth';
import { auth, signIn } from 'apps/payments/next/auth';
import { headers } from 'next/headers';
import { CheckoutParams } from '../layout';
import { StripeWrapper } from '@fxa/payments/ui';
import { getFakeCartData } from 'apps/payments/next/app/_lib/apiClient';
export const dynamic = 'force-dynamic';
@ -30,23 +29,15 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
params.cartId,
SupportedPages.START
);
const [session, l10n, cart] = await Promise.all([
//TODO - Replace with cartPromise as part of FXA-8903
const fakeCartDataPromise = getFakeCartData(params.cartId);
const [session, l10n, cart, fakeCart] = await Promise.all([
sessionPromise,
l10nPromise,
cartPromise,
fakeCartDataPromise,
]);
// Temporary function used to test handleStripeErrorAction
// This is to be deleted as part of FXA-8850
async function onSubmit() {
'use server';
const error: Stripe.StripeRawError = new Error(
'Simulated Stripe Error'
) as unknown as Stripe.StripeRawError;
error.type = 'card_error';
await handleStripeErrorAction(cart.id, cart.version, error);
}
return (
<>
<section
@ -197,64 +188,11 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
</p>
</div>
</section>
<StripeWrapper amount={1099} currency="usd" />
{/*
Temporary function used to test handleStripeErrorAction
This is to be deleted as part of FXA-8850
*/}
<div>
<form action={onSubmit} className="mb-8">
<input type="text" name="name" />
<button
type="submit"
className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4"
>
<div>Test handle error</div>
</button>
</form>
</div>
{/*
Temporary section to test NextAuth prompt/no prompt signin
To be deleted as part of FXA-7521/FXA-7523 if not sooner where necessary
*/}
{!session ? (
<div className="flex flex-col gap-4">
<form
action={async () => {
'use server';
await signIn('fxa');
}}
>
<button className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4">
<div className="block">Sign In - Login</div>
</button>
</form>
<form
action={async () => {
'use server';
await signIn('fxa', undefined, { prompt: 'none' });
}}
>
<button className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4">
<div className="block">Sign In - No Prompt</div>
</button>
</form>
</div>
) : (
<div className="flex flex-col items-center gap-4">
<p>Hello {session?.user?.id}</p>
<form
action={async () => {
'use server';
await signOut();
}}
>
<button className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4">
<div className="block">Sign Out</div>
</button>
</form>
</div>
)}
<StripeWrapper
amount={fakeCart.amount}
currency={fakeCart.nextInvoice.currency}
cart={cart}
/>
</section>
</>
);

Просмотреть файл

@ -17,19 +17,19 @@ export default function Index() {
<h2 className="text-xl">With auth</h2>
<div className="flex gap-8">
<div className="flex flex-col gap-2 p-4 items-center">
<h2>123Done - Monthly</h2>
<h2>VPN - Monthly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123done/checkout/monthly/landing"
href="/en/vpn/checkout/monthly/landing"
>
Redirect
</Link>
</div>
<div className="flex flex-col gap-2 p-4 items-center">
<h2>123Done - Yearly</h2>
<h2>VPN - Yearly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123done/checkout/yearly/landing"
href="/en/vpn/checkout/yearly/landing"
>
Redirect
</Link>
@ -38,19 +38,19 @@ export default function Index() {
<h2 className="text-xl mt-8">Without auth</h2>
<div className="flex gap-8">
<div className="flex flex-col gap-2 p-4 items-center">
<h2>123Done - Monthly</h2>
<h2>VPN - Monthly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123done/checkout/monthly/new"
href="/en/vpn/checkout/monthly/new"
>
Redirect
</Link>
</div>
<div className="flex flex-col gap-2 p-4 items-center">
<h2>123Done - Yearly</h2>
<h2>VPN - Yearly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/123done/checkout/yearly/new"
href="/en/vpn/checkout/yearly/new"
>
Redirect
</Link>

Просмотреть файл

@ -6,7 +6,7 @@
/* layout */
body {
@apply grid min-h-[100vh] tablet:justify-center text-base font-body bg-grey-10 text-grey-900;
@apply grid min-h-[100vh] tablet:justify-center text-base font-body bg-grey-10 text-grey-900 leading-[1.15];
}
/* Purchase Details, offeringId */

Просмотреть файл

@ -7,11 +7,11 @@ export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
connect-src 'self' 'https://api.stripe.com';
frame-src 'https://js.stripe.com' 'https://hooks.stripe.com';
connect-src 'self' https://api.stripe.com;
frame-src https://js.stripe.com https://hooks.stripe.com;
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: http: 'unsafe-inline' ${
process.env.NODE_ENV === 'production' ? '' : `'unsafe-eval'`
} 'https://js.stripe.com';
} https://js.stripe.com;
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';

Просмотреть файл

@ -15,4 +15,15 @@ export default <Partial<Config>>{
),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {
boxShadow: {
inputError:
'0 1px 2px rgba(0, 0, 0, 0.3), 0 3px 6px rgba(0, 0, 0, 0.02), 0 0 0 1px #df1b41',
},
colors: {
'alert-red': '#D70022',
},
},
},
};

Просмотреть файл

@ -22,7 +22,7 @@ import {
} from '@fxa/shared/contentful';
import { CapabilityManager } from './capability.manager';
import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
import { MockStatsDFactory } from '@fxa/shared/metrics/statsd';
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
describe('CapabilityManager', () => {
let capabilityManager: CapabilityManager;
@ -32,7 +32,7 @@ describe('CapabilityManager', () => {
const module = await Test.createTestingModule({
providers: [
MockFirestoreProvider,
MockStatsDFactory,
MockStatsDProvider,
ContentfulClientConfig,
ContentfulClient,
ContentfulManager,

Просмотреть файл

@ -43,7 +43,7 @@ import {
ContentfulClientConfig,
ContentfulManager,
} from '@fxa/shared/contentful';
import { MockStatsDFactory } from '@fxa/shared/metrics/statsd';
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
import { ConfigService } from '@nestjs/config';
import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
import {
@ -70,7 +70,7 @@ describe('CartService', () => {
AccountCustomerManager,
ConfigService,
MockFirestoreProvider,
MockStatsDFactory,
MockStatsDProvider,
ContentfulClientConfig,
ContentfulClient,
ContentfulManager,

Просмотреть файл

@ -113,7 +113,7 @@ export class CartService {
try {
const cart = await this.cartManager.fetchCartById(cartId);
this.checkoutService.payWithStripe(cart, paymentMethodId);
await this.checkoutService.payWithStripe(cart, paymentMethodId);
await this.cartManager.finishCart(cartId, version, {});
} catch (e) {

Просмотреть файл

@ -1,13 +1,13 @@
import Stripe from 'stripe';
import { StripeError } from '@stripe/stripe-js';
import { stripeErrorToErrorReasonId } from './cart.utils';
import { CartErrorReasonId } from '@fxa/shared/db/mysql/account';
describe('utils', () => {
describe('stripeErrorReasonId', () => {
let mockStripeError: Stripe.StripeRawError;
let mockStripeError: StripeError;
beforeEach(() => {
mockStripeError = new Error() as unknown as Stripe.StripeRawError;
mockStripeError = new Error() as unknown as StripeError;
});
it('should return for type card_error', () => {
@ -17,7 +17,7 @@ describe('utils', () => {
});
it('should return for default', () => {
mockStripeError.type = 'invalid_grant';
mockStripeError.type = 'api_error';
const result = stripeErrorToErrorReasonId(mockStripeError);
expect(result).toBe(CartErrorReasonId.Unknown);
});

Просмотреть файл

@ -2,7 +2,7 @@
* 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 { Stripe } from 'stripe';
import { StripeError } from '@stripe/stripe-js';
import { EligibilityStatus } from '@fxa/payments/eligibility';
import {
CartEligibilityStatus,
@ -18,7 +18,7 @@ export const handleEligibilityStatusMap = {
};
export function stripeErrorToErrorReasonId(
stripeError: Stripe.StripeRawError
stripeError: StripeError
): CartErrorReasonId {
switch (stripeError.type) {
case 'card_error':

Просмотреть файл

@ -33,7 +33,7 @@ import {
OfferingComparison,
OfferingOverlapProductResult,
} from './eligibility.types';
import { MockStatsDFactory } from '@fxa/shared/metrics/statsd';
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
describe('EligibilityService', () => {
@ -52,7 +52,7 @@ describe('EligibilityService', () => {
MockFirestoreProvider,
ContentfulClientConfig,
ContentfulClient,
MockStatsDFactory,
MockStatsDProvider,
ContentfulManager,
EligibilityManager,
StripeConfig,

Просмотреть файл

@ -4,7 +4,7 @@
'use server';
import Stripe from 'stripe';
import { StripeError } from '@stripe/stripe-js';
import { app } from '../nestapp/app';
import { redirect } from 'next/navigation';
import { stripeErrorToErrorReasonId } from '@fxa/payments/cart';
@ -12,7 +12,7 @@ import { stripeErrorToErrorReasonId } from '@fxa/payments/cart';
export const handleStripeErrorAction = async (
cartId: string,
version: number,
stripeError: Stripe.StripeRawError
stripeError: StripeError
) => {
const errorReasonId = stripeErrorToErrorReasonId(stripeError);

Просмотреть файл

@ -0,0 +1,179 @@
/* 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/. */
'use client';
import {
PaymentElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { StripePaymentElementChangeEvent } from '@stripe/stripe-js';
import { checkoutCartWithStripe } from '../../actions/checkoutCartWithStripe';
import { useEffect, useState } from 'react';
import { handleStripeErrorAction } from '../../actions/handleStripeError';
import LockImage from '@fxa/shared/assets/images/lock.svg';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import * as Form from '@radix-ui/react-form';
interface CheckoutFormProps {
cart: {
id: string;
version: number;
email: string | null;
};
}
export function CheckoutForm({ cart }: CheckoutFormProps) {
const router = useRouter();
const stripe = useStripe();
const elements = useElements();
const [isPaymentElementLoading, setIsPaymentElementLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [stripeFieldsComplete, setStripeFieldsComplete] = useState(false);
const [fullName, setFullName] = useState('');
const [hasFullNameError, setHasFullNameError] = useState(false);
useEffect(() => {
if (elements) {
const element = elements.getElement('payment');
if (element) {
element.on('ready', () => {
setIsPaymentElementLoading(false);
});
element.on('change', (event: StripePaymentElementChangeEvent) => {
if (event.complete) {
setStripeFieldsComplete(true);
} else {
if (!stripeFieldsComplete) {
setStripeFieldsComplete(false);
}
}
});
} else {
setIsPaymentElementLoading(false);
}
}
}, [elements, stripeFieldsComplete]);
const submitHandler = async (
event: React.SyntheticEvent<HTMLFormElement>
) => {
event.preventDefault();
if (!stripe || !elements) {
// Stripe.js hasn't yet loaded.
// Make sure to disable form submission until Stripe.js has loaded.
return;
}
setLoading(true);
setHasFullNameError(!fullName);
// Trigger form validation and wallet collection
const { error: submitError } = await elements.submit();
if (submitError) {
setLoading(false);
return;
}
const { paymentMethod, error: methodError } =
await stripe.createPaymentMethod({
elements,
params: {
billing_details: {
name: fullName,
email: cart.email || '',
},
},
});
if (methodError || !paymentMethod) {
if (methodError.type === 'validation_error') {
return;
} else {
await handleStripeErrorAction(cart.id, cart.version, methodError);
return;
}
}
await checkoutCartWithStripe(cart.id, cart.version, paymentMethod.id);
// TODO - To be added in M3B - Redirect customer to '/processing' page
router.push('./start');
// TODO - To be moved in M3B - Confirm Payment on '/processing' page
// Confirm the Intent using the details collected by the Payment Element
//const { error } = await stripe.confirmPayment({
// clientSecret,
// confirmParams: {
// return_url: successRedirectUrl,
// },
//});
//
//if (error) {
// // This point is only reached if there's an immediate error when confirming the Intent.
// // Show the error to your customer (for example, "payment details incomplete").
// await handleStripeErrorAction(cart.id, cart.version, error);
//} else {
// // Your customer is redirected to your `return_url`. For some payment
// // methods like iDEAL, your customer is redirected to an intermediate
// // site first to authorize the payment, then redirected to the `return_url`.
//}
};
const nonStripeFieldsComplete = !!fullName;
return (
<Form.Root className="flex flex-col gap-4" onSubmit={submitHandler}>
{!isPaymentElementLoading && (
<Form.Field name="name" serverInvalid={hasFullNameError}>
<Form.Label className="font-medium text-sm text-grey-400 block mb-1 text-start">
Name as it appears on your card
</Form.Label>
<Form.Control asChild>
<input
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
type="text"
data-testid="name"
placeholder="Full Name"
value={fullName}
onChange={(e) => {
setFullName(e.target.value);
setHasFullNameError(!e.target.value);
}}
/>
</Form.Control>
{hasFullNameError && (
<Form.Message asChild>
<p className="text-sm mt-1 text-alert-red">
Please enter your name
</p>
</Form.Message>
)}
</Form.Field>
)}
<PaymentElement />
{!isPaymentElementLoading && (
<Form.Submit asChild>
<button
className="flex items-center justify-center bg-blue-500 font-semibold h-12 rounded-md text-white w-full p-4 mt-6 hover:bg-blue-700 aria-disabled:relative aria-disabled:after:absolute aria-disabled:after:content-[''] aria-disabled:after:top-0 aria-disabled:after:left-0 aria-disabled:after:w-full aria-disabled:after:h-full aria-disabled:after:bg-white aria-disabled:after:opacity-50 aria-disabled:after:z-30 aria-disabled:border-none"
type="submit"
aria-disabled={
!stripeFieldsComplete || !nonStripeFieldsComplete || loading
}
>
<Image
src={LockImage}
className="h-4 w-4 my-0 mx-3 relative top-0.5"
alt=""
/>
Subscribe Now
</button>
</Form.Submit>
)}
</Form.Root>
);
}

Просмотреть файл

@ -5,13 +5,19 @@
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { CheckoutForm } from './CheckoutForm';
interface StripeWrapperProps {
amount: number;
currency: string;
cart: {
id: string;
version: number;
email: string | null;
};
}
export function StripeWrapper({ amount, currency }: StripeWrapperProps) {
export function StripeWrapper({ amount, currency, cart }: StripeWrapperProps) {
// TODO - Load from config
const stripePromise = loadStripe(
'pk_test_VNpCidC0a2TJJB3wqXq7drhN00sF8r9mhs'
@ -22,11 +28,37 @@ export function StripeWrapper({ amount, currency }: StripeWrapperProps) {
amount,
currency,
paymentMethodCreation: 'manual',
appearance: {
variables: {
fontFamily:
'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSizeBase: '16px',
fontWeightNormal: '500',
colorDanger: '#D70022',
},
rules: {
'.Tab': {
borderColor: 'rgba(0,0,0,0.3)',
},
'.Input': {
borderColor: 'rgba(0,0,0,0.3)',
},
'.Input::placeholder': {
color: '#5E5E72', // Matches grey-500 of tailwind.config.js
fontWeight: '400',
},
'.Label': {
color: '#6D6D6E', // Matches grey-400 of tailwind.config.js
fontWeight: '500',
lineHeight: '1.15',
},
},
},
};
return (
<Elements stripe={stripePromise} options={options}>
Hello there from Stripe
<CheckoutForm cart={cart} />
</Elements>
);
}

Просмотреть файл

@ -29,8 +29,8 @@ import {
PayPalManager,
PaypalCustomerManager,
} from '@fxa/payments/paypal';
import { FirestoreService } from '@fxa/shared/db/firestore';
import { StatsDService } from '@fxa/shared/metrics/statsd';
import { FirestoreProvider } from '@fxa/shared/db/firestore';
import { StatsDProvider } from '@fxa/shared/metrics/statsd';
@Module({
imports: [
@ -44,14 +44,8 @@ import { StatsDService } from '@fxa/shared/metrics/statsd';
],
controllers: [],
providers: [
{
provide: FirestoreService,
useValue: {}, //Temporary value to resolve Payments Next startup issues
},
{
provide: StatsDService,
useValue: {}, //Temporary value to resolve Payments Next startup issues
},
FirestoreProvider,
StatsDProvider,
LocalizerRscFactoryProvider,
NextJSActionsService,
AccountDatabaseNestFactory,

Просмотреть файл

@ -10,6 +10,8 @@ import { GeoDBConfig, GeoDBManagerConfig } from '@fxa/shared/geodb';
import { PaypalClientConfig } from 'libs/payments/paypal/src/lib/paypal.client.config';
import { StripeConfig } from '@fxa/payments/stripe';
import { ContentfulClientConfig } from '@fxa/shared/contentful';
import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config';
import { StatsDConfig } from 'libs/shared/metrics/statsd/src/lib/statsd.config';
export class RootConfig {
@Type(() => MySQLConfig)
@ -40,4 +42,14 @@ export class RootConfig {
@ValidateNested()
@IsDefined()
public readonly contentfulClientConfig!: Partial<ContentfulClientConfig>;
@Type(() => FirestoreConfig)
@ValidateNested()
@IsDefined()
public readonly firestoreConfig!: Partial<FirestoreConfig>;
@Type(() => StatsDConfig)
@ValidateNested()
@IsDefined()
public readonly statsDConfig!: Partial<StatsDConfig>;
}

Просмотреть файл

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 6.65132H12V4.66666C12 3.60579 11.5786 2.58837 10.8284 1.83823C10.0783 1.08808 9.06087 0.666656 8 0.666656C6.93913 0.666656 5.92172 1.08808 5.17157 1.83823C4.42143 2.58837 4 3.60579 4 4.66666V6.65132H3.5C3.10218 6.65132 2.72064 6.80936 2.43934 7.09066C2.15804 7.37197 2 7.7535 2 8.15132V13.1667C2 13.5645 2.15804 13.946 2.43934 14.2273C2.72064 14.5086 3.10218 14.6667 3.5 14.6667H12.5C12.8978 14.6667 13.2794 14.5086 13.5607 14.2273C13.842 13.946 14 13.5645 14 13.1667V8.15132C14 7.7535 13.842 7.37197 13.5607 7.09066C13.2794 6.80936 12.8978 6.65132 12.5 6.65132ZM6 4.66666C6 4.13622 6.21071 3.62752 6.58579 3.25244C6.96086 2.87737 7.46957 2.66666 8 2.66666C8.53043 2.66666 9.03914 2.87737 9.41421 3.25244C9.78929 3.62752 10 4.13622 10 4.66666V6.65132H6V4.66666Z" fill="white"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 896 B

Просмотреть файл

@ -1,3 +1,5 @@
// Temporarily ignore. To be fixed during CMS refactor FXA-XXXX
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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/. */
@ -71,19 +73,21 @@ export class ContentfulClient {
return result;
}
@Cacheable({
cacheKey: (args: any) => cacheKeyForQuery(args[0], args[1]),
strategy: new NetworkFirstStrategy(),
ttlSeconds: (_, context: ContentfulClient) =>
context.contentfulClientConfig.firestoreCacheTTL ||
DEFAULT_FIRESTORE_CACHE_TTL,
client: (_, context: ContentfulClient) =>
new FirestoreAdapter(
context.firestore,
context.contentfulClientConfig.firestoreCacheCollectionName ||
CONTENTFUL_QUERY_CACHE_KEY
),
})
// Not sure what's happening here. Context is undefined which results in an error.
// To be fixed during CMS refactor
// @Cacheable({
// cacheKey: (args: any) => cacheKeyForQuery(args[0], args[1]),
// strategy: new NetworkFirstStrategy(),
// ttlSeconds: (_, context: ContentfulClient) =>
// context.contentfulClientConfig.firestoreCacheTTL ||
// DEFAULT_FIRESTORE_CACHE_TTL,
// client: (_, context: ContentfulClient) =>
// new FirestoreAdapter(
// context.firestore,
// context.contentfulClientConfig.firestoreCacheCollectionName ||
// CONTENTFUL_QUERY_CACHE_KEY
// ),
// })
async query<Result, Variables extends OperationVariables>(
query: TypedDocumentNode<Result, Variables>,
variables: Variables

Просмотреть файл

@ -0,0 +1,28 @@
/* 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 { Type } from 'class-transformer';
import { IsOptional, IsString, ValidateNested } from 'class-validator';
class FirestoreCredentialsConfig {
@IsString()
clientEmail!: string;
@IsString()
privateKey!: string;
}
export class FirestoreConfig {
@Type(() => FirestoreCredentialsConfig)
@ValidateNested()
@IsOptional()
credentials?: FirestoreCredentialsConfig;
@IsString()
@IsOptional()
keyFilename?: string;
@IsString()
@IsOptional()
projectId?: string;
}

Просмотреть файл

@ -6,6 +6,7 @@ import { Firestore } from '@google-cloud/firestore';
import * as grpc from '@grpc/grpc-js';
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { FirestoreConfig } from './firestore.config';
/**
* Creates a firestore instance from a settings object.
@ -41,19 +42,20 @@ export function setupFirestore(config: FirebaseFirestore.Settings) {
* Factory for providing access to firestore
*/
export const FirestoreService = Symbol('FIRESTORE');
export const FirestoreProvider: Provider<Firestore> = {
provide: FirestoreService,
useFactory: (configService: ConfigService) => {
const firestoreConfig = configService.get('authFirestore');
if (firestoreConfig == null) {
throw new Error(
"Could not locate config for firestore missing 'authFirestore';"
);
}
const firestore = setupFirestore(firestoreConfig);
return firestore;
useFactory: (config: FirestoreConfig) => {
const firestoreConfig: FirebaseFirestore.Settings = {
...config,
credentials: {
client_email: config.credentials?.clientEmail,
private_key: config.credentials?.privateKey,
},
};
return setupFirestore(firestoreConfig);
},
inject: [ConfigService],
inject: [FirestoreConfig],
};
/**
@ -67,3 +69,18 @@ export const MockFirestoreProvider: Provider<Firestore> = {
return {} as Firestore;
},
};
export const LegacyFirestoreProvider: Provider<Firestore> = {
provide: FirestoreService,
useFactory: (configService: ConfigService) => {
const firestoreConfig = configService.get('authFirestore');
if (firestoreConfig == null) {
throw new Error(
"Could not locate config for firestore missing 'authFirestore';"
);
}
const firestore = setupFirestore(firestoreConfig);
return firestore;
},
inject: [ConfigService],
};

Просмотреть файл

@ -0,0 +1,26 @@
/* 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 { IsInt, IsOptional, IsString } from 'class-validator';
export class StatsDConfig {
@IsInt()
@IsOptional()
sampleRate?: number;
@IsInt()
@IsOptional()
maxBufferSize?: number;
@IsString()
@IsOptional()
host?: string;
@IsInt()
@IsOptional()
port?: number;
@IsString()
@IsOptional()
prefix?: string;
}

Просмотреть файл

@ -4,7 +4,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { StatsDFactory, StatsDService } from './statsd.provider';
import { LegacyStatsDProvider, StatsDService } from './statsd.provider';
import { StatsD } from 'hot-shots';
const mockStatsd = jest.fn();
@ -16,7 +16,7 @@ jest.mock('hot-shots', () => {
};
});
describe('StatsDFactory', () => {
describe('LegacyStatsDProvider', () => {
let statsd: StatsD;
const mockConfig = {
@ -35,7 +35,7 @@ describe('StatsDFactory', () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
StatsDFactory,
LegacyStatsDProvider,
{
provide: ConfigService,
useValue: mockConfigService,

Просмотреть файл

@ -5,9 +5,34 @@
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { StatsD } from 'hot-shots';
import { StatsDConfig } from './statsd.config';
export const StatsDService = Symbol('STATSD');
export const StatsDFactory: Provider<StatsD> = {
export const StatsDProvider: Provider<StatsD> = {
provide: StatsDService,
useFactory: (config: StatsDConfig) => {
if (config.host === '') {
return new StatsD({ mock: true });
}
return new StatsD(config);
},
inject: [StatsDConfig],
};
/**
* Can be used to satisfy DI when unit testing things that should not need
* statsd.
* Note: this will cause errors to be thrown if statsd is used
*/
export const MockStatsDProvider: Provider<StatsD> = {
provide: StatsDService,
useFactory: () => {
return {} as StatsD;
},
};
export const LegacyStatsDProvider: Provider<StatsD> = {
provide: StatsDService,
useFactory: (configService: ConfigService) => {
const config = configService.get('metrics');
@ -18,15 +43,3 @@ export const StatsDFactory: Provider<StatsD> = {
},
inject: [ConfigService],
};
/**
* Can be used to satisfy DI when unit testing things that should not need
* statsd.
* Note: this will cause errors to be thrown if statsd is used
*/
export const MockStatsDFactory: Provider<StatsD> = {
provide: StatsDService,
useFactory: () => {
return {} as StatsD;
},
};

Просмотреть файл

@ -71,6 +71,7 @@
"@opentelemetry/sdk-trace-base": "^1.23.0",
"@opentelemetry/sdk-trace-node": "^1.23.0",
"@opentelemetry/sdk-trace-web": "^1.23.0",
"@radix-ui/react-form": "^0.0.3",
"@sentry/browser": "^7.113.0",
"@sentry/integrations": "^7.113.0",
"@sentry/node": "^7.113.0",

Просмотреть файл

@ -4,7 +4,7 @@
import { HealthModule } from 'fxa-shared/nestjs/health/health.module';
import { MozLoggerService } from '@fxa/shared/mozlog';
import { StatsDFactory } from '@fxa/shared/metrics/statsd';
import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
import { NotifierSnsFactory, NotifierService } from '@fxa/shared/notifier';
import { getVersionInfo } from 'fxa-shared/nestjs/version';
@ -51,7 +51,7 @@ const version = getVersionInfo(__dirname);
],
controllers: [],
providers: [
StatsDFactory,
LegacyStatsDProvider,
NotifierSnsFactory,
NotifierService,
MozLoggerService,

Просмотреть файл

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
import { MozLoggerService } from '@fxa/shared/mozlog';
import { StatsDFactory } from '@fxa/shared/metrics/statsd';
import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
@Module({
providers: [DatabaseService, MozLoggerService, StatsDFactory],
providers: [DatabaseService, MozLoggerService, LegacyStatsDProvider],
exports: [DatabaseService],
})
export class DatabaseModule {}

Просмотреть файл

@ -24,7 +24,7 @@ import { SubscriptionResolver } from './subscription.resolver';
import { ClientInfoResolver } from './clientInfo.resolver';
import { SessionResolver } from './session.resolver';
import { NotifierService, NotifierSnsFactory } from '@fxa/shared/notifier';
import { StatsDFactory } from '@fxa/shared/metrics/statsd';
import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
import { MozLoggerService } from '@fxa/shared/mozlog';
const config = Config.getProperties();
@ -52,7 +52,7 @@ export const GraphQLConfigFactory = async (
@Module({
imports: [BackendModule, CustomsModule],
providers: [
StatsDFactory,
LegacyStatsDProvider,
NotifierSnsFactory,
NotifierService,
AccountResolver,

Просмотреть файл

@ -14915,6 +14915,31 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-form@npm:^0.0.3":
version: 0.0.3
resolution: "@radix-ui/react-form@npm:0.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-label": 2.0.2
"@radix-ui/react-primitive": 1.0.3
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: a2cc8e3787643b16e8c7a77aeac3707e7d7858ca65818631d46184abd7ab7047d496f646f404cbeddc4aeaf974a4e0d778044b09236d156ca2ab6f5e21851dac
languageName: node
linkType: hard
"@radix-ui/react-id@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-id@npm:1.0.1"
@ -14931,6 +14956,26 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-label@npm:2.0.2":
version: 2.0.2
resolution: "@radix-ui/react-label@npm:2.0.2"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-primitive": 1.0.3
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: fe3bd8902bc523fb5125fa96167a13b8a60d007413787eae9573e4b00b0edff0487c4c0620ea5dc37e6da13833ebc4f8d7e00b6c846f2a5686e7f173672b8dde
languageName: node
linkType: hard
"@radix-ui/react-popper@npm:1.1.2":
version: 1.1.2
resolution: "@radix-ui/react-popper@npm:1.1.2"
@ -38670,6 +38715,7 @@ fsevents@~2.1.1:
"@opentelemetry/sdk-trace-base": ^1.23.0
"@opentelemetry/sdk-trace-node": ^1.23.0
"@opentelemetry/sdk-trace-web": ^1.23.0
"@radix-ui/react-form": ^0.0.3
"@sentry/browser": ^7.113.0
"@sentry/integrations": ^7.113.0
"@sentry/node": ^7.113.0