mirror of
https://github.com/mozilla/fxa.git
synced 2025-12-28 07:03:55 +00:00
Merge pull request #19348 from mozilla/PAY-3178-payment-methods-page-and-utils
feat(payments-next):Page and util support for alternate payment methods
This commit is contained in:
commit
35eea17916
@ -148,7 +148,55 @@ export default async function CheckoutSuccess({
|
||||
cart.latestInvoicePreview?.currency,
|
||||
locale
|
||||
)}
|
||||
{cart.paymentInfo.type === 'external_paypal' ? (
|
||||
{cart.paymentInfo.walletType === 'apple_pay' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src={getCardIcon('apple_pay', l10n).img}
|
||||
alt={l10n.getString('apple-pay-logo-alt-text', 'Apple Pay logo')}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{cart.paymentInfo.brand && (
|
||||
<Image
|
||||
src={getCardIcon(cart.paymentInfo.brand, l10n).img}
|
||||
alt={getCardIcon(cart.paymentInfo.brand, l10n).altText}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{l10n.getString(
|
||||
'next-payment-confirmation-cc-card-ending-in',
|
||||
{ last4: cart.paymentInfo.last4 ?? '' },
|
||||
`Card ending in ${cart.paymentInfo.last4}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : cart.paymentInfo.walletType === 'google_pay' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src={getCardIcon('google_pay', l10n).img}
|
||||
alt={l10n.getString('google-pay-logo-alt-text', 'Google Pay logo')}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{cart.paymentInfo.brand && (
|
||||
<Image
|
||||
src={getCardIcon(cart.paymentInfo.brand, l10n).img}
|
||||
alt={getCardIcon(cart.paymentInfo.brand, l10n).altText}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{l10n.getString(
|
||||
'next-payment-confirmation-cc-card-ending-in',
|
||||
{ last4: cart.paymentInfo.last4 ?? '' },
|
||||
`Card ending in ${cart.paymentInfo.last4}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : cart.paymentInfo.type === 'external_paypal' ? (
|
||||
<Image
|
||||
src={getCardIcon('paypal', l10n).img}
|
||||
alt={l10n.getString('paypal-logo-alt-text', 'PayPal logo')}
|
||||
|
||||
@ -149,7 +149,55 @@ export default async function UpgradeSuccess({
|
||||
cart.latestInvoicePreview?.currency,
|
||||
locale
|
||||
)}
|
||||
{cart.paymentInfo.type === 'external_paypal' ? (
|
||||
{cart.paymentInfo.walletType === 'apple_pay' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src={getCardIcon('apple_pay', l10n).img}
|
||||
alt={l10n.getString('apple-pay-logo-alt-text', 'Apple Pay logo')}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{cart.paymentInfo.brand && (
|
||||
<Image
|
||||
src={getCardIcon(cart.paymentInfo.brand, l10n).img}
|
||||
alt={getCardIcon(cart.paymentInfo.brand, l10n).altText}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{l10n.getString(
|
||||
'next-payment-confirmation-cc-card-ending-in',
|
||||
{ last4: cart.paymentInfo.last4 ?? '' },
|
||||
`Card ending in ${cart.paymentInfo.last4}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : cart.paymentInfo.walletType === 'google_pay' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src={getCardIcon('google_pay', l10n).img}
|
||||
alt={l10n.getString('google-pay-logo-alt-text', 'Google Pay logo')}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{cart.paymentInfo.brand && (
|
||||
<Image
|
||||
src={getCardIcon(cart.paymentInfo.brand, l10n).img}
|
||||
alt={getCardIcon(cart.paymentInfo.brand, l10n).altText}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{l10n.getString(
|
||||
'next-payment-confirmation-cc-card-ending-in',
|
||||
{ last4: cart.paymentInfo.last4 ?? '' },
|
||||
`Card ending in ${cart.paymentInfo.last4}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : cart.paymentInfo.type === 'external_paypal' ? (
|
||||
<Image
|
||||
src={getCardIcon('paypal', l10n).img}
|
||||
alt={l10n.getString('paypal-logo-alt-text', 'PayPal logo')}
|
||||
|
||||
@ -90,7 +90,55 @@ export default async function Upgrade({
|
||||
|
||||
{cart.paymentInfo && (
|
||||
<div className="flex items-center justify-between mt-4 text-sm">
|
||||
{cart.paymentInfo.type === 'external_paypal' ? (
|
||||
{cart.paymentInfo.walletType === 'apple_pay' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src={getCardIcon('apple_pay', l10n).img}
|
||||
alt={l10n.getString('apple-pay-logo-alt-text', 'Apple Pay logo')}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{cart.paymentInfo.brand && (
|
||||
<Image
|
||||
src={getCardIcon(cart.paymentInfo.brand, l10n).img}
|
||||
alt={getCardIcon(cart.paymentInfo.brand, l10n).altText}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{l10n.getString(
|
||||
'next-payment-confirmation-cc-card-ending-in',
|
||||
{ last4: cart.paymentInfo.last4 ?? '' },
|
||||
`Card ending in ${cart.paymentInfo.last4}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : cart.paymentInfo.walletType === 'google_pay' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src={getCardIcon('google_pay', l10n).img}
|
||||
alt={l10n.getString('google-pay-logo-alt-text', 'Google Pay logo')}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{cart.paymentInfo.brand && (
|
||||
<Image
|
||||
src={getCardIcon(cart.paymentInfo.brand, l10n).img}
|
||||
alt={getCardIcon(cart.paymentInfo.brand, l10n).altText}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{l10n.getString(
|
||||
'next-payment-confirmation-cc-card-ending-in',
|
||||
{ last4: cart.paymentInfo.last4 ?? '' },
|
||||
`Card ending in ${cart.paymentInfo.last4}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : cart.paymentInfo.type === 'external_paypal' ? (
|
||||
<Image
|
||||
src={getCardIcon('paypal', l10n).img}
|
||||
alt={l10n.getString('paypal-logo-alt-text', 'PayPal logo')}
|
||||
|
||||
@ -11,6 +11,8 @@ visa-logo-alt-text = { -brand-visa } logo
|
||||
# Alt text for generic payment card logo
|
||||
unbranded-logo-alt-text = Unbranded logo
|
||||
link-logo-alt-text = { -brand-link } logo
|
||||
apple-pay-logo-alt-text = { -brand-apple-pay } logo
|
||||
google-pay-logo-alt-text = { -brand-google-pay } logo
|
||||
|
||||
## Error pages - /checkout and /upgrade
|
||||
## Common strings used in multiple pages
|
||||
|
||||
@ -51,7 +51,7 @@ export default async function Manage({
|
||||
appleIapSubscriptions,
|
||||
googleIapSubscriptions,
|
||||
} = await getSubManPageContentAction(session.user?.id);
|
||||
const { billingAgreementId, brand, expMonth, expYear, last4, type } =
|
||||
const { billingAgreementId, brand, expMonth, expYear, last4, type, walletType } =
|
||||
defaultPaymentMethod || {};
|
||||
const expirationDate =
|
||||
expMonth && expYear
|
||||
@ -152,7 +152,65 @@ export default async function Manage({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{type === 'card' && brand && (
|
||||
{type === 'card' && walletType && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="leading-5 text-sm">
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<Image
|
||||
src={getCardIcon(walletType === 'apple_pay' ? 'apple_pay' : 'google_pay', l10n).img}
|
||||
alt={
|
||||
walletType === 'apple_pay'
|
||||
? l10n.getString('apple-pay-logo-alt-text', 'Apple Pay logo')
|
||||
: l10n.getString('google-pay-logo-alt-text', 'Google Pay logo')
|
||||
}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
{brand && (
|
||||
<Image
|
||||
src={getCardIcon(brand, l10n).img}
|
||||
alt={getCardIcon(brand, l10n).altText}
|
||||
width={40}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{last4 && (
|
||||
<div>
|
||||
{l10n.getString(
|
||||
'subscription-management-card-ending-in',
|
||||
{ last4 },
|
||||
`Card ending in ${last4}`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{expirationDate && (
|
||||
<div>
|
||||
{l10n.getString(
|
||||
'subscription-management-card-expires-date',
|
||||
{ expirationDate },
|
||||
`Expires ${expirationDate}`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
className={CSS_SECONDARY_LINK}
|
||||
href={`${config.paymentsNextHostedUrl}/${locale}/subscriptions/payments/stripe`}
|
||||
aria-label={l10n.getString(
|
||||
'subscription-management-button-change-payment-method-aria',
|
||||
'Change payment method'
|
||||
)}
|
||||
>
|
||||
{l10n.getString(
|
||||
'subscription-management-button-change-payment-method',
|
||||
'Change'
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'card' && brand && !walletType && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="leading-5 text-sm">
|
||||
<Image
|
||||
|
||||
@ -20,13 +20,13 @@ import {
|
||||
PaymentMethodManager,
|
||||
CustomerSessionManager,
|
||||
PaymentIntentManager,
|
||||
determinePaymentMethodType,
|
||||
retrieveSubscriptionItem,
|
||||
TaxAddress,
|
||||
PriceManager,
|
||||
getSubplatInterval,
|
||||
SetupIntentManager,
|
||||
PromotionCodeError,
|
||||
SubPlatPaymentMethodType,
|
||||
} from '@fxa/payments/customer';
|
||||
import {
|
||||
EligibilityService,
|
||||
@ -877,24 +877,35 @@ export class CartService {
|
||||
}
|
||||
|
||||
let paymentInfo: PaymentInfo | undefined;
|
||||
const paymentMethodType = determinePaymentMethodType(
|
||||
const paymentMethodType = await this.paymentMethodManager.determineType(
|
||||
customer,
|
||||
subscriptions
|
||||
);
|
||||
if (paymentMethodType?.type === 'stripe') {
|
||||
const paymentMethod = await this.paymentMethodManager.retrieve(
|
||||
paymentMethodType.paymentMethodId
|
||||
);
|
||||
paymentInfo = {
|
||||
type: paymentMethod.type,
|
||||
last4: paymentMethod.card?.last4,
|
||||
brand: paymentMethod.card?.brand,
|
||||
customerSessionClientSecret: customerSession?.client_secret,
|
||||
};
|
||||
} else if (paymentMethodType?.type === 'external_paypal') {
|
||||
paymentInfo = {
|
||||
type: 'external_paypal',
|
||||
};
|
||||
|
||||
switch (paymentMethodType?.type) {
|
||||
case SubPlatPaymentMethodType.PayPal:
|
||||
paymentInfo = {
|
||||
type: 'external_paypal',
|
||||
};
|
||||
break;
|
||||
case SubPlatPaymentMethodType.Link:
|
||||
case SubPlatPaymentMethodType.Card:
|
||||
case SubPlatPaymentMethodType.ApplePay:
|
||||
case SubPlatPaymentMethodType.GooglePay:
|
||||
case SubPlatPaymentMethodType.Stripe: {
|
||||
const paymentMethod = await this.paymentMethodManager.retrieve(
|
||||
paymentMethodType.paymentMethodId
|
||||
);
|
||||
const walletType = paymentMethod.card?.wallet?.type;
|
||||
paymentInfo = {
|
||||
type: paymentMethod.type,
|
||||
last4: paymentMethod.card?.last4,
|
||||
brand: paymentMethod.card?.brand,
|
||||
customerSessionClientSecret: customerSession?.client_secret,
|
||||
...(walletType ? { walletType } : {}),
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cart latest invoice data
|
||||
|
||||
@ -57,6 +57,7 @@ export interface PaymentInfo {
|
||||
last4?: string;
|
||||
brand?: string;
|
||||
customerSessionClientSecret?: string;
|
||||
walletType?: string;
|
||||
}
|
||||
|
||||
export type ResultCart = Readonly<Omit<Cart, 'id' | 'uid'>> & {
|
||||
|
||||
@ -23,6 +23,8 @@ export function throwIntentFailedError(
|
||||
intentType: 'SetupIntent' | 'PaymentIntent'
|
||||
) {
|
||||
switch (errorCode) {
|
||||
case 'payment_intent_payment_attempt_failed':
|
||||
case 'payment_method_provider_decline':
|
||||
case 'card_declined': {
|
||||
switch (declineCode) {
|
||||
case 'approve_with_id':
|
||||
|
||||
@ -19,5 +19,4 @@ export * from './lib/factories/tax-address.factory';
|
||||
export * from './lib/customer.error';
|
||||
export * from './lib/util/stripeInvoiceToFirstInvoicePreviewDTO';
|
||||
export * from './lib/util/getSubplatInterval';
|
||||
export * from './lib/util/determinePaymentMethodType';
|
||||
export * from './lib/util/retrieveSubscriptionItem';
|
||||
|
||||
@ -5,13 +5,15 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PaymentMethodManager } from './paymentMethod.manager';
|
||||
import { determinePaymentMethodType } from './util/determinePaymentMethodType';
|
||||
import {
|
||||
MockPaypalClientConfigProvider,
|
||||
PaypalBillingAgreementManager,
|
||||
PayPalClient,
|
||||
PaypalCustomerManager,
|
||||
} from '@fxa/payments/paypal';
|
||||
import {
|
||||
SubPlatPaymentMethodType,
|
||||
} from '@fxa/payments/customer';
|
||||
import {
|
||||
StripeClient,
|
||||
MockStripeConfigProvider,
|
||||
@ -23,9 +25,6 @@ import {
|
||||
import { MockAccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account';
|
||||
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
|
||||
jest.mock('./util/determinePaymentMethodType');
|
||||
const mockDeterminePaymentMethodType = jest.mocked(determinePaymentMethodType);
|
||||
|
||||
describe('PaymentMethodManager', () => {
|
||||
let paymentMethodManager: PaymentMethodManager;
|
||||
let paypalBillingAgreementManager: PaypalBillingAgreementManager;
|
||||
@ -100,8 +99,8 @@ describe('PaymentMethodManager', () => {
|
||||
const mockSubscriptions = [StripeSubscriptionFactory()];
|
||||
const mockUid = faker.string.uuid();
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue({
|
||||
type: 'stripe',
|
||||
jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({
|
||||
type: SubPlatPaymentMethodType.Stripe,
|
||||
paymentMethodId: 'pm_id',
|
||||
});
|
||||
jest
|
||||
@ -119,6 +118,7 @@ describe('PaymentMethodManager', () => {
|
||||
last4: mockPaymentMethod.card?.last4,
|
||||
expMonth: mockPaymentMethod.card?.exp_month,
|
||||
expYear: mockPaymentMethod.card?.exp_year,
|
||||
walletType: mockPaymentMethod.card?.wallet?.type,
|
||||
});
|
||||
});
|
||||
|
||||
@ -128,8 +128,8 @@ describe('PaymentMethodManager', () => {
|
||||
const mockSubscriptions = [StripeSubscriptionFactory()];
|
||||
const mockUid = faker.string.uuid();
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue({
|
||||
type: 'external_paypal',
|
||||
jest.spyOn(paymentMethodManager, 'determineType').mockResolvedValue({
|
||||
type: SubPlatPaymentMethodType.PayPal,
|
||||
});
|
||||
jest
|
||||
.spyOn(paypalBillingAgreementManager, 'retrieveActiveId')
|
||||
@ -147,4 +147,158 @@ describe('PaymentMethodManager', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineType', () => {
|
||||
it('returns card', async () => {
|
||||
const mockCustomer = StripeCustomerFactory({
|
||||
invoice_settings: {
|
||||
custom_fields: null,
|
||||
default_payment_method: 'any',
|
||||
footer: null,
|
||||
rendering_options: null,
|
||||
},
|
||||
});
|
||||
|
||||
const mockPaymentMethod = StripeResponseFactory(
|
||||
StripePaymentMethodFactory({
|
||||
type: 'card',
|
||||
})
|
||||
);
|
||||
jest.spyOn(paymentMethodManager, 'retrieve').mockResolvedValue(mockPaymentMethod);
|
||||
|
||||
await expect(
|
||||
paymentMethodManager.determineType(mockCustomer)
|
||||
).resolves.toEqual({
|
||||
type: SubPlatPaymentMethodType.Card,
|
||||
paymentMethodId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns external_paypal', async () => {
|
||||
const mockSubscription = StripeSubscriptionFactory({
|
||||
collection_method: 'send_invoice',
|
||||
});
|
||||
|
||||
await expect(
|
||||
paymentMethodManager.determineType(undefined, [mockSubscription])
|
||||
).resolves.toEqual({
|
||||
type: SubPlatPaymentMethodType.PayPal,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns link', async () => {
|
||||
const mockCustomer = StripeCustomerFactory({
|
||||
invoice_settings: {
|
||||
custom_fields: null,
|
||||
default_payment_method: 'any',
|
||||
footer: null,
|
||||
rendering_options: null,
|
||||
},
|
||||
});
|
||||
|
||||
const mockPaymentMethod = StripeResponseFactory(
|
||||
StripePaymentMethodFactory({
|
||||
type: 'link',
|
||||
})
|
||||
);
|
||||
jest.spyOn(paymentMethodManager, 'retrieve').mockResolvedValue(mockPaymentMethod);
|
||||
|
||||
await expect(
|
||||
paymentMethodManager.determineType(mockCustomer)
|
||||
).resolves.toEqual({
|
||||
type: SubPlatPaymentMethodType.Link,
|
||||
paymentMethodId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns apple_pay', async () => {
|
||||
const mockCustomer = StripeCustomerFactory({
|
||||
invoice_settings: {
|
||||
custom_fields: null,
|
||||
default_payment_method: 'any',
|
||||
footer: null,
|
||||
rendering_options: null,
|
||||
},
|
||||
});
|
||||
|
||||
const mockPaymentMethod = StripeResponseFactory(
|
||||
StripePaymentMethodFactory({
|
||||
type: 'card',
|
||||
card: {
|
||||
wallet: {
|
||||
type: 'apple_pay',
|
||||
dynamic_last4: null
|
||||
},
|
||||
brand: '',
|
||||
checks: null,
|
||||
country: null,
|
||||
display_brand: null,
|
||||
exp_month: 0,
|
||||
exp_year: 0,
|
||||
funding: '',
|
||||
generated_from: null,
|
||||
last4: '',
|
||||
networks: null,
|
||||
three_d_secure_usage: null
|
||||
},
|
||||
})
|
||||
);
|
||||
jest.spyOn(paymentMethodManager, 'retrieve').mockResolvedValue(mockPaymentMethod);
|
||||
|
||||
await expect(
|
||||
paymentMethodManager.determineType(mockCustomer)
|
||||
).resolves.toEqual({
|
||||
type: SubPlatPaymentMethodType.ApplePay,
|
||||
paymentMethodId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns google_pay', async () => {
|
||||
const mockCustomer = StripeCustomerFactory({
|
||||
invoice_settings: {
|
||||
custom_fields: null,
|
||||
default_payment_method: 'any',
|
||||
footer: null,
|
||||
rendering_options: null,
|
||||
},
|
||||
});
|
||||
|
||||
const mockPaymentMethod = StripeResponseFactory(
|
||||
StripePaymentMethodFactory({
|
||||
type: 'card',
|
||||
card: {
|
||||
wallet: {
|
||||
type: 'google_pay',
|
||||
dynamic_last4: null
|
||||
},
|
||||
brand: '',
|
||||
checks: null,
|
||||
country: null,
|
||||
display_brand: null,
|
||||
exp_month: 0,
|
||||
exp_year: 0,
|
||||
funding: '',
|
||||
generated_from: null,
|
||||
last4: '',
|
||||
networks: null,
|
||||
three_d_secure_usage: null
|
||||
},
|
||||
})
|
||||
);
|
||||
jest.spyOn(paymentMethodManager, 'retrieve').mockResolvedValue(mockPaymentMethod);
|
||||
|
||||
await expect(
|
||||
paymentMethodManager.determineType(mockCustomer)
|
||||
).resolves.toEqual({
|
||||
type: SubPlatPaymentMethodType.GooglePay,
|
||||
paymentMethodId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null', async () => {
|
||||
await expect(
|
||||
paymentMethodManager.determineType(undefined, undefined)
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,8 +10,17 @@ import {
|
||||
StripeCustomer,
|
||||
StripeSubscription,
|
||||
} from '@fxa/payments/stripe';
|
||||
import { DefaultPaymentMethod } from './types';
|
||||
import { determinePaymentMethodType } from './util/determinePaymentMethodType';
|
||||
import {
|
||||
DefaultPaymentMethod,
|
||||
SubPlatPaymentMethodType,
|
||||
StripePaymentMethod,
|
||||
PayPalPaymentMethod,
|
||||
} from './types';
|
||||
|
||||
type PaymentMethodTypeResponse =
|
||||
| StripePaymentMethod
|
||||
| PayPalPaymentMethod
|
||||
| null;
|
||||
|
||||
@Injectable()
|
||||
export class PaymentMethodManager {
|
||||
@ -37,11 +46,11 @@ export class PaymentMethodManager {
|
||||
uid: string
|
||||
) {
|
||||
let defaultPaymentMethod: DefaultPaymentMethod | undefined;
|
||||
const paymentMethodType = determinePaymentMethodType(
|
||||
const paymentMethodType = await this.determineType(
|
||||
customer,
|
||||
subscriptions
|
||||
);
|
||||
if (paymentMethodType?.type === 'stripe') {
|
||||
if (paymentMethodType?.type === SubPlatPaymentMethodType.Stripe) {
|
||||
const paymentMethod = await this.retrieve(
|
||||
paymentMethodType.paymentMethodId
|
||||
);
|
||||
@ -51,6 +60,7 @@ export class PaymentMethodManager {
|
||||
last4: paymentMethod.card?.last4,
|
||||
expMonth: paymentMethod.card?.exp_month,
|
||||
expYear: paymentMethod.card?.exp_year,
|
||||
walletType: paymentMethod.card?.wallet?.type,
|
||||
};
|
||||
} else if (paymentMethodType?.type === 'external_paypal') {
|
||||
const billingAgreementId =
|
||||
@ -63,4 +73,57 @@ export class PaymentMethodManager {
|
||||
}
|
||||
return defaultPaymentMethod;
|
||||
}
|
||||
|
||||
async determineType (
|
||||
customer?: StripeCustomer,
|
||||
subscriptions?: StripeSubscription[]
|
||||
): Promise <PaymentMethodTypeResponse> {
|
||||
// First check if payment method is PayPal
|
||||
// Note, this needs to happen first since a customer could also have a
|
||||
// default payment method. However if PayPal is set as the payment method,
|
||||
// it should take precedence.
|
||||
if (
|
||||
subscriptions?.length &&
|
||||
subscriptions[0].collection_method === 'send_invoice'
|
||||
) {
|
||||
return {
|
||||
type: SubPlatPaymentMethodType.PayPal,
|
||||
};
|
||||
}
|
||||
|
||||
if (customer?.invoice_settings.default_payment_method) {
|
||||
const paymentMethod = await this.retrieve(
|
||||
customer.invoice_settings.default_payment_method
|
||||
);
|
||||
if (paymentMethod.card?.wallet?.type === 'apple_pay') {
|
||||
return {
|
||||
type: SubPlatPaymentMethodType.ApplePay,
|
||||
paymentMethodId: customer.invoice_settings.default_payment_method,
|
||||
}
|
||||
} else if (paymentMethod.card?.wallet?.type === 'google_pay') {
|
||||
return {
|
||||
type: SubPlatPaymentMethodType.GooglePay,
|
||||
paymentMethodId: customer.invoice_settings.default_payment_method,
|
||||
}
|
||||
} else if (paymentMethod.type === 'link') {
|
||||
return {
|
||||
type: SubPlatPaymentMethodType.Link,
|
||||
paymentMethodId: customer.invoice_settings.default_payment_method,
|
||||
}
|
||||
} else if (paymentMethod.type === 'card') {
|
||||
return {
|
||||
type: SubPlatPaymentMethodType.Card,
|
||||
paymentMethodId: customer.invoice_settings.default_payment_method,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: SubPlatPaymentMethodType.Stripe,
|
||||
paymentMethodId: customer.invoice_settings.default_payment_method,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -28,6 +28,28 @@ export type InvoicePreview = {
|
||||
subsequentTax?: TaxAmount[];
|
||||
};
|
||||
|
||||
export enum SubPlatPaymentMethodType {
|
||||
PayPal = 'external_paypal',
|
||||
Stripe = 'stripe',
|
||||
Card = 'card',
|
||||
ApplePay = 'apple_pay',
|
||||
GooglePay = 'google_pay',
|
||||
Link = 'link',
|
||||
}
|
||||
|
||||
export interface StripePaymentMethod {
|
||||
type: SubPlatPaymentMethodType.Card
|
||||
| SubPlatPaymentMethodType.ApplePay
|
||||
| SubPlatPaymentMethodType.GooglePay
|
||||
| SubPlatPaymentMethodType.Link
|
||||
| SubPlatPaymentMethodType.Stripe
|
||||
paymentMethodId: string;
|
||||
}
|
||||
|
||||
export interface PayPalPaymentMethod {
|
||||
type: SubPlatPaymentMethodType.PayPal;
|
||||
}
|
||||
|
||||
export interface Interval {
|
||||
interval: NonNullable<StripePrice['recurring']>['interval'];
|
||||
intervalCount: number;
|
||||
@ -51,6 +73,7 @@ export interface DefaultPaymentMethod {
|
||||
expMonth?: number;
|
||||
expYear?: number;
|
||||
billingAgreementId?: string;
|
||||
walletType?: string;
|
||||
}
|
||||
|
||||
export interface PricingForCurrency {
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
/* 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 {
|
||||
StripeCustomerFactory,
|
||||
StripeSubscriptionFactory,
|
||||
} from '@fxa/payments/stripe';
|
||||
import { determinePaymentMethodType } from './determinePaymentMethodType';
|
||||
|
||||
describe('determinePaymentMethodType', () => {
|
||||
it('returns stripe', () => {
|
||||
const mockCustomer = StripeCustomerFactory();
|
||||
expect(determinePaymentMethodType(mockCustomer)).toEqual({
|
||||
type: 'stripe',
|
||||
paymentMethodId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns external_paypal', () => {
|
||||
const mockSubscription = StripeSubscriptionFactory({
|
||||
collection_method: 'send_invoice',
|
||||
});
|
||||
expect(determinePaymentMethodType(undefined, [mockSubscription])).toEqual({
|
||||
type: 'external_paypal',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null', () => {
|
||||
expect(determinePaymentMethodType(undefined, undefined));
|
||||
});
|
||||
});
|
||||
@ -1,46 +0,0 @@
|
||||
/* 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 { StripeCustomer, StripeSubscription } from '@fxa/payments/stripe';
|
||||
|
||||
interface StripePaymentMethod {
|
||||
type: 'stripe';
|
||||
paymentMethodId: string;
|
||||
}
|
||||
|
||||
interface PayPalPaymentMethod {
|
||||
type: 'external_paypal';
|
||||
}
|
||||
|
||||
type PaymentMethodTypeResponse =
|
||||
| StripePaymentMethod
|
||||
| PayPalPaymentMethod
|
||||
| null;
|
||||
|
||||
export const determinePaymentMethodType = (
|
||||
customer?: StripeCustomer,
|
||||
subscriptions?: StripeSubscription[]
|
||||
): PaymentMethodTypeResponse => {
|
||||
// First check if payment method is PayPal
|
||||
// Note, this needs to happen first since a customer could also have a
|
||||
// default payment method. However if PayPal is set as the payment method,
|
||||
// it should take precedence.
|
||||
if (
|
||||
subscriptions?.length &&
|
||||
subscriptions[0].collection_method === 'send_invoice'
|
||||
) {
|
||||
return {
|
||||
type: 'external_paypal',
|
||||
};
|
||||
}
|
||||
|
||||
if (customer?.invoice_settings.default_payment_method) {
|
||||
return {
|
||||
type: 'stripe',
|
||||
paymentMethodId: customer.invoice_settings.default_payment_method,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -10,6 +10,9 @@ import {
|
||||
} from '@fxa/payments/metrics';
|
||||
import { LocationStatus } from '@fxa/payments/eligibility';
|
||||
import { TaxChangeAllowedStatus } from '@fxa/payments/cart';
|
||||
import {
|
||||
SubPlatPaymentMethodType,
|
||||
} from '@fxa/payments/customer';
|
||||
|
||||
export type CheckoutEvents = CommonMetrics;
|
||||
export type CheckoutPaymentEvents = CommonMetrics & {
|
||||
@ -21,7 +24,7 @@ export type SubscriptionEndedEvents = {
|
||||
priceId: string;
|
||||
priceInterval?: string;
|
||||
priceIntervalCount?: number;
|
||||
paymentProvider?: PaymentProvidersType;
|
||||
paymentProvider?: SubPlatPaymentMethodType;
|
||||
providerEventId: string;
|
||||
cancellationReason: CancellationReason;
|
||||
uid?: string;
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
MockCurrencyConfigProvider,
|
||||
} from '@fxa/payments/currency';
|
||||
import {
|
||||
determinePaymentMethodType,
|
||||
CustomerManager,
|
||||
InvoiceManager,
|
||||
PaymentMethodManager,
|
||||
@ -100,9 +99,6 @@ import {
|
||||
|
||||
import { LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
|
||||
jest.mock('../../../customer/src/lib/util/determinePaymentMethodType');
|
||||
const mockDeterminePaymentMethodType = jest.mocked(determinePaymentMethodType);
|
||||
|
||||
jest.mock('@fxa/shared/error', () => ({
|
||||
...jest.requireActual('@fxa/shared/error'),
|
||||
SanitizeExceptions: jest.fn(() => {
|
||||
@ -340,10 +336,6 @@ describe('SubscriptionManagementService', () => {
|
||||
supportUrl: mockIapOfferingResult2.offering.commonContent.supportUrl,
|
||||
};
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue({
|
||||
type: 'stripe',
|
||||
paymentMethodId: 'pm_id',
|
||||
});
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -399,7 +391,6 @@ describe('SubscriptionManagementService', () => {
|
||||
purchaseDetails: [],
|
||||
});
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -443,7 +434,6 @@ describe('SubscriptionManagementService', () => {
|
||||
purchaseDetails: [],
|
||||
});
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -527,14 +517,12 @@ describe('SubscriptionManagementService', () => {
|
||||
supportUrl: mockIapOfferingResult2.offering.commonContent.supportUrl,
|
||||
};
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
jest
|
||||
.spyOn(paymentMethodManager, 'getDefaultPaymentMethod')
|
||||
.mockResolvedValue(undefined);
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -579,7 +567,6 @@ describe('SubscriptionManagementService', () => {
|
||||
purchaseDetails: [],
|
||||
});
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -633,7 +620,6 @@ describe('SubscriptionManagementService', () => {
|
||||
purchaseDetails: [],
|
||||
});
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -646,7 +632,6 @@ describe('SubscriptionManagementService', () => {
|
||||
jest
|
||||
.spyOn(paymentMethodManager, 'getDefaultPaymentMethod')
|
||||
.mockResolvedValue(undefined);
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -706,7 +691,6 @@ describe('SubscriptionManagementService', () => {
|
||||
purchaseDetails: [],
|
||||
});
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -719,7 +703,6 @@ describe('SubscriptionManagementService', () => {
|
||||
jest
|
||||
.spyOn(paymentMethodManager, 'getDefaultPaymentMethod')
|
||||
.mockResolvedValue(undefined);
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -775,7 +758,6 @@ describe('SubscriptionManagementService', () => {
|
||||
purchaseDetails: [],
|
||||
});
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -788,7 +770,6 @@ describe('SubscriptionManagementService', () => {
|
||||
jest
|
||||
.spyOn(paymentMethodManager, 'getDefaultPaymentMethod')
|
||||
.mockResolvedValue(undefined);
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -845,7 +826,6 @@ describe('SubscriptionManagementService', () => {
|
||||
purchaseDetails: [],
|
||||
});
|
||||
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
@ -858,7 +838,6 @@ describe('SubscriptionManagementService', () => {
|
||||
jest
|
||||
.spyOn(paymentMethodManager, 'getDefaultPaymentMethod')
|
||||
.mockResolvedValue(undefined);
|
||||
mockDeterminePaymentMethodType.mockReturnValue(null);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
|
||||
@ -6,6 +6,9 @@ import Stripe from 'stripe';
|
||||
|
||||
export const CheckoutTypes = ['with-accounts', 'without-accounts'] as const;
|
||||
export type CheckoutTypesType = (typeof CheckoutTypes)[number];
|
||||
import {
|
||||
SubPlatPaymentMethodType,
|
||||
} from '@fxa/payments/customer';
|
||||
|
||||
export const PaymentProvidersTypePartial = [
|
||||
'card',
|
||||
@ -16,6 +19,7 @@ export const PaymentProvidersTypePartial = [
|
||||
] as const;
|
||||
export type PaymentProvidersType =
|
||||
| Stripe.PaymentMethod.Type
|
||||
| SubPlatPaymentMethodType
|
||||
| 'google_iap'
|
||||
| 'apple_iap'
|
||||
| 'external_paypal';
|
||||
|
||||
@ -82,6 +82,7 @@ interface CheckoutFormProps {
|
||||
last4?: string;
|
||||
brand?: string;
|
||||
customerSessionClientSecret?: string;
|
||||
walletType?: string;
|
||||
};
|
||||
};
|
||||
locale: string;
|
||||
@ -122,6 +123,7 @@ export function CheckoutForm({
|
||||
const linkAuthOptions = sessionEmail
|
||||
? { defaultValues: { email: sessionEmail } }
|
||||
: {};
|
||||
const [isNotCard, setIsNotCard] = useState(false);
|
||||
|
||||
const engageGlean = useCallbackOnce(() => {
|
||||
recordEmitterEventAction(
|
||||
@ -152,6 +154,10 @@ export function CheckoutForm({
|
||||
const hasSavedPaymentMethod = !!event?.value?.payment_method?.id;
|
||||
const isNewCardSelected = event?.value?.type === 'card' && !hasSavedPaymentMethod;
|
||||
|
||||
const selectedType = event?.value?.type || '';
|
||||
const isNotCardType = selectedType !== 'card';
|
||||
setIsNotCard(isNotCardType);
|
||||
|
||||
setShowLinkAuthElement(isNewCardSelected && hasSavedPaymentMethod);
|
||||
|
||||
setSelectedPaymentMethod(event?.value?.type || '');
|
||||
@ -166,7 +172,7 @@ export function CheckoutForm({
|
||||
const showPayPalButton = selectedPaymentMethod === 'external_paypal';
|
||||
const isStripe = cart?.paymentInfo?.type !== 'external_paypal';
|
||||
const showFullNameInput =
|
||||
!isPaymentElementLoading && !showPayPalButton && !isSavedPaymentMethod;
|
||||
!isPaymentElementLoading && !showPayPalButton && !isSavedPaymentMethod && selectedPaymentMethod === 'card' && !isNotCard;
|
||||
const nonStripeFieldsComplete = !showFullNameInput || !!fullName;
|
||||
|
||||
const submitHandler = async (
|
||||
|
||||
@ -46,6 +46,7 @@ export function PaymentMethodManagement({
|
||||
const [hasFullNameError, setHasFullNameError] = useState(false);
|
||||
const [isNonDefaultCardSelected, setIsNonDefaultCardSelected] =
|
||||
useState(false);
|
||||
const [isNonCardSelected, setIsNonCardSelected] = useState(false);
|
||||
|
||||
const handleReady = () => {
|
||||
setIsReady(true);
|
||||
@ -56,6 +57,14 @@ export function PaymentMethodManagement({
|
||||
) => {
|
||||
setIsComplete(event.complete);
|
||||
|
||||
if (event.value.type !== 'card') {
|
||||
setIsNonCardSelected(true);
|
||||
setIsInputNewCardDetails(false);
|
||||
setHasFullNameError(false);
|
||||
return;
|
||||
}
|
||||
setIsNonCardSelected(false);
|
||||
|
||||
if (event.value.type === 'card' && !event.value.payment_method) {
|
||||
setIsInputNewCardDetails(true);
|
||||
|
||||
@ -108,6 +117,11 @@ export function PaymentMethodManagement({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInputNewCardDetails && !fullName) {
|
||||
setHasFullNameError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
@ -250,7 +264,7 @@ export function PaymentMethodManagement({
|
||||
</Form.Message>
|
||||
)}
|
||||
</Form.Field>
|
||||
{isInputNewCardDetails && (
|
||||
{(isInputNewCardDetails || isNonCardSelected) && (
|
||||
<div className="flex flex-row justify-center pt-4">
|
||||
<Form.Submit asChild>
|
||||
<BaseButton
|
||||
|
||||
@ -49,6 +49,7 @@ interface PaymentFormProps {
|
||||
last4?: string;
|
||||
brand?: string;
|
||||
customerSessionClientSecret?: string;
|
||||
walletType?: string;
|
||||
};
|
||||
hasActiveSubscriptions: boolean;
|
||||
};
|
||||
|
||||
@ -115,6 +115,7 @@ interface StripeWrapperProps {
|
||||
last4?: string;
|
||||
brand?: string;
|
||||
customerSessionClientSecret?: string;
|
||||
walletType?: string;
|
||||
};
|
||||
hasActiveSubscriptions?: boolean;
|
||||
};
|
||||
|
||||
@ -3,8 +3,10 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Amex from '@fxa/shared/assets/images/payment-methods/amex.svg';
|
||||
import ApplePay from '@fxa/shared/assets/images/payment-methods/apple-pay.svg';
|
||||
import Diners from '@fxa/shared/assets/images/payment-methods/diners.svg';
|
||||
import Discover from '@fxa/shared/assets/images/payment-methods/discover.svg';
|
||||
import GooglePay from '@fxa/shared/assets/images/payment-methods/google-pay.svg';
|
||||
import Jcb from '@fxa/shared/assets/images/payment-methods/jcb.svg';
|
||||
import Link from '@fxa/shared/assets/images/payment-methods/link.svg';
|
||||
import Mastercard from '@fxa/shared/assets/images/payment-methods/mastercard.svg';
|
||||
@ -21,6 +23,11 @@ export function getCardIcon(cardBrand: string, l10n: LocalizerRsc) {
|
||||
img: Amex,
|
||||
altText: l10n.getString('amex-logo-alt-text', 'American Express logo'),
|
||||
};
|
||||
case 'apple_pay':
|
||||
return {
|
||||
img: ApplePay,
|
||||
altText: l10n.getString('apple-pay-logo-alt-text', 'Apple Pay logo'),
|
||||
};
|
||||
case 'diners':
|
||||
return {
|
||||
img: Diners,
|
||||
@ -31,6 +38,11 @@ export function getCardIcon(cardBrand: string, l10n: LocalizerRsc) {
|
||||
img: Discover,
|
||||
altText: l10n.getString('discover-logo-alt-text', 'Discover logo'),
|
||||
};
|
||||
case 'google_pay':
|
||||
return {
|
||||
img: GooglePay,
|
||||
altText: l10n.getString('google-pay-logo-alt-text', 'Google Pay logo'),
|
||||
};
|
||||
case 'jcb':
|
||||
return {
|
||||
img: Jcb,
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
InvoiceManager,
|
||||
PriceManager,
|
||||
SubscriptionManager,
|
||||
PaymentMethodManager,
|
||||
} from '@fxa/payments/customer';
|
||||
import { PaymentsEmitterService } from '@fxa/payments/events';
|
||||
import {
|
||||
@ -54,6 +55,10 @@ describe('StripeEventManager', () => {
|
||||
log: jest.fn(),
|
||||
};
|
||||
|
||||
const paymentMethodManagerMock = {
|
||||
determineType: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
@ -61,6 +66,10 @@ describe('StripeEventManager', () => {
|
||||
provide: Logger,
|
||||
useValue: mockLogger,
|
||||
},
|
||||
{
|
||||
provide: PaymentMethodManager,
|
||||
useValue: paymentMethodManagerMock
|
||||
},
|
||||
MockStripeConfigProvider,
|
||||
StripeClient,
|
||||
StripeEventManager,
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
InvoiceManager,
|
||||
PriceManager,
|
||||
SubscriptionManager,
|
||||
PaymentMethodManager,
|
||||
} from '@fxa/payments/customer';
|
||||
import { PaymentsEmitterService } from '@fxa/payments/events';
|
||||
import {
|
||||
@ -55,6 +56,10 @@ describe('StripeWebhookService', () => {
|
||||
log: jest.fn(),
|
||||
};
|
||||
|
||||
const paymentMethodManagerMock = {
|
||||
determineType: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
@ -62,6 +67,10 @@ describe('StripeWebhookService', () => {
|
||||
provide: Logger,
|
||||
useValue: mockLogger,
|
||||
},
|
||||
{
|
||||
provide: PaymentMethodManager,
|
||||
useValue: paymentMethodManagerMock
|
||||
},
|
||||
MockStripeConfigProvider,
|
||||
StripeClient,
|
||||
StripeEventManager,
|
||||
|
||||
@ -21,6 +21,8 @@ import {
|
||||
InvoiceManager,
|
||||
PriceManager,
|
||||
SubscriptionManager,
|
||||
PaymentMethodManager,
|
||||
SubPlatPaymentMethodType,
|
||||
} from '@fxa/payments/customer';
|
||||
import { PaymentsEmitterService } from '@fxa/payments/events';
|
||||
import {
|
||||
@ -47,7 +49,6 @@ import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
|
||||
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { MockAccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account';
|
||||
import { CustomerSubscriptionDeletedResponseFactory } from './factories';
|
||||
import { determinePaymentMethodType } from '@fxa/payments/customer';
|
||||
import {
|
||||
CancellationReason,
|
||||
determineCancellation,
|
||||
@ -55,7 +56,6 @@ import {
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
jest.mock('@fxa/payments/customer');
|
||||
const mockDeterminePaymentMethodType = jest.mocked(determinePaymentMethodType);
|
||||
jest.mock('./util/determineCancellation');
|
||||
const mockDetermineCancellation = jest.mocked(determineCancellation);
|
||||
|
||||
@ -65,6 +65,7 @@ describe('SubscriptionEventsService', () => {
|
||||
let customerManager: CustomerManager;
|
||||
let invoiceManager: InvoiceManager;
|
||||
let emitterService: PaymentsEmitterService;
|
||||
let paymentMethodManager: PaymentMethodManager;
|
||||
|
||||
const mockEmitter = {
|
||||
emit: jest.fn(),
|
||||
@ -73,6 +74,9 @@ describe('SubscriptionEventsService', () => {
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
};
|
||||
const paymentMethodManagerMock = {
|
||||
determineType: jest.fn(),
|
||||
};
|
||||
|
||||
const { event: mockEvent, eventObjectData: mockEventObjectData } =
|
||||
CustomerSubscriptionDeletedResponseFactory();
|
||||
@ -84,6 +88,10 @@ describe('SubscriptionEventsService', () => {
|
||||
provide: Logger,
|
||||
useValue: mockLogger,
|
||||
},
|
||||
{
|
||||
provide: PaymentMethodManager,
|
||||
useValue: paymentMethodManagerMock
|
||||
},
|
||||
MockStripeConfigProvider,
|
||||
StripeClient,
|
||||
StripeEventManager,
|
||||
@ -118,6 +126,7 @@ describe('SubscriptionEventsService', () => {
|
||||
customerManager = module.get(CustomerManager);
|
||||
invoiceManager = module.get(InvoiceManager);
|
||||
emitterService = module.get(PaymentsEmitterService);
|
||||
paymentMethodManager = module.get(PaymentMethodManager);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -138,8 +147,8 @@ describe('SubscriptionEventsService', () => {
|
||||
jest
|
||||
.spyOn(emitterService, 'getEmitter')
|
||||
.mockReturnValue(mockEmitter as any);
|
||||
mockDeterminePaymentMethodType.mockReturnValue({
|
||||
type: 'stripe',
|
||||
(paymentMethodManager.determineType as jest.Mock).mockResolvedValue({
|
||||
type: SubPlatPaymentMethodType.Card,
|
||||
paymentMethodId: 'pm_id',
|
||||
});
|
||||
});
|
||||
@ -161,8 +170,8 @@ describe('SubscriptionEventsService', () => {
|
||||
});
|
||||
|
||||
it('should emit the subscriptionEnded event, with paymentProvider external_paypal', async () => {
|
||||
mockDeterminePaymentMethodType.mockReturnValue({
|
||||
type: 'external_paypal',
|
||||
(paymentMethodManager.determineType as jest.Mock).mockResolvedValueOnce({
|
||||
type: SubPlatPaymentMethodType.PayPal,
|
||||
});
|
||||
await subscriptionEventsService.handleCustomerSubscriptionDeleted(
|
||||
mockEvent,
|
||||
|
||||
@ -5,12 +5,12 @@
|
||||
import {
|
||||
CustomerDeletedError,
|
||||
CustomerManager,
|
||||
determinePaymentMethodType,
|
||||
InvoiceManager,
|
||||
SubscriptionManager,
|
||||
PaymentMethodManager,
|
||||
SubPlatPaymentMethodType,
|
||||
} from '@fxa/payments/customer';
|
||||
import { PaymentsEmitterService } from '@fxa/payments/events';
|
||||
import { PaymentProvidersType } from '@fxa/payments/metrics';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
@ -24,7 +24,8 @@ export class SubscriptionEventsService {
|
||||
private subscriptionManager: SubscriptionManager,
|
||||
private customerManager: CustomerManager,
|
||||
private invoiceManager: InvoiceManager,
|
||||
private emitterService: PaymentsEmitterService
|
||||
private emitterService: PaymentsEmitterService,
|
||||
private paymentMethodManager: PaymentMethodManager,
|
||||
) {}
|
||||
|
||||
async handleCustomerSubscriptionDeleted(
|
||||
@ -36,7 +37,7 @@ export class SubscriptionEventsService {
|
||||
);
|
||||
const price = subscription.items.data[0].price;
|
||||
|
||||
let paymentProvider: PaymentProvidersType | undefined;
|
||||
let paymentProvider: SubPlatPaymentMethodType | undefined;
|
||||
let determinedCancellation: CancellationReason | undefined;
|
||||
let uid: string | undefined;
|
||||
try {
|
||||
@ -44,17 +45,13 @@ export class SubscriptionEventsService {
|
||||
subscription.customer
|
||||
);
|
||||
uid = customer.metadata['userid'];
|
||||
const paymentMethodType = determinePaymentMethodType(customer, [
|
||||
const paymentMethodType = await this.paymentMethodManager.determineType(customer, [
|
||||
subscription,
|
||||
]);
|
||||
if (paymentMethodType?.type === 'stripe') {
|
||||
paymentProvider = 'card';
|
||||
} else if (paymentMethodType?.type === 'external_paypal') {
|
||||
paymentProvider = 'external_paypal';
|
||||
}
|
||||
paymentProvider = paymentMethodType?.type;
|
||||
|
||||
const latestInvoice =
|
||||
paymentProvider === 'external_paypal' && subscription.latest_invoice
|
||||
paymentProvider === SubPlatPaymentMethodType.PayPal && subscription.latest_invoice
|
||||
? await this.invoiceManager.retrieve(subscription.latest_invoice)
|
||||
: undefined;
|
||||
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
* 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 { PaymentProvidersType } from '@fxa/payments/metrics';
|
||||
import { StripeInvoice, StripeSubscription } from '@fxa/payments/stripe';
|
||||
import {
|
||||
SubPlatPaymentMethodType,
|
||||
} from '@fxa/payments/customer';
|
||||
|
||||
export enum CancellationReason {
|
||||
CustomerInitiated = 'customer_initiated',
|
||||
@ -11,8 +13,16 @@ export enum CancellationReason {
|
||||
Redundant = 'redundant',
|
||||
}
|
||||
|
||||
const STRIPE_PAYMENT_PROVIDER_TYPES = new Set([
|
||||
SubPlatPaymentMethodType.Card,
|
||||
SubPlatPaymentMethodType.GooglePay,
|
||||
SubPlatPaymentMethodType.ApplePay,
|
||||
SubPlatPaymentMethodType.Link,
|
||||
SubPlatPaymentMethodType.Stripe,
|
||||
]);
|
||||
|
||||
export const determineCancellation = (
|
||||
paymentProvider: PaymentProvidersType,
|
||||
paymentProvider: SubPlatPaymentMethodType,
|
||||
subscription: StripeSubscription,
|
||||
latestInvoice?: StripeInvoice
|
||||
): CancellationReason | undefined => {
|
||||
@ -20,7 +30,7 @@ export const determineCancellation = (
|
||||
return CancellationReason.Redundant;
|
||||
}
|
||||
|
||||
if (paymentProvider === 'external_paypal') {
|
||||
if (paymentProvider === SubPlatPaymentMethodType.PayPal) {
|
||||
if (!latestInvoice) {
|
||||
return undefined;
|
||||
} else {
|
||||
@ -28,7 +38,7 @@ export const determineCancellation = (
|
||||
? CancellationReason.CustomerInitiated
|
||||
: CancellationReason.Involuntary;
|
||||
}
|
||||
} else if (paymentProvider === 'card') {
|
||||
} else if (STRIPE_PAYMENT_PROVIDER_TYPES.has(paymentProvider)) {
|
||||
return subscription.cancellation_details
|
||||
? subscription.cancellation_details?.reason === 'cancellation_requested'
|
||||
? CancellationReason.CustomerInitiated
|
||||
|
||||
16
libs/shared/assets/src/images/payment-methods/apple-pay.svg
Normal file
16
libs/shared/assets/src/images/payment-methods/apple-pay.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg width="37" height="24" viewBox="0 0 37 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10480_11384)">
|
||||
<path d="M33.6865 0H3.31352C3.18702 0 3.0603 0 2.93403 0.000747416C2.82728 0.00151748 2.72079 0.00271788 2.6143 0.00566224C2.38204 0.0120266 2.1478 0.0259104 1.91845 0.0676751C1.68544 0.110165 1.46861 0.17947 1.25707 0.288593C1.04912 0.395745 0.858732 0.535897 0.693742 0.703137C0.528681 0.870377 0.390356 1.06296 0.284624 1.27389C0.176901 1.48822 0.108455 1.70798 0.0668097 1.94426C0.0253683 2.1767 0.0115738 2.41399 0.0053148 2.64907C0.00245354 2.75697 0.00122632 2.86487 0.000531123 2.97274C-0.000206548 3.10096 3.93424e-05 3.22909 3.93424e-05 3.35753V20.6427C3.93424e-05 20.7712 -0.000206548 20.8991 0.000531123 21.0275C0.00122632 21.1354 0.00245354 21.2433 0.0053148 21.3512C0.0115738 21.5861 0.0253683 21.8234 0.0668097 22.0557C0.108455 22.2921 0.176901 22.5118 0.284624 22.7261C0.390356 22.9371 0.528681 23.1299 0.693742 23.2969C0.858732 23.4644 1.04912 23.6045 1.25707 23.7114C1.46861 23.8208 1.68544 23.8902 1.91845 23.9326C2.1478 23.9741 2.38204 23.9882 2.6143 23.9946C2.72079 23.9971 2.82728 23.9985 2.93403 23.999C3.0603 24 3.18702 24 3.31352 24H33.6865C33.8128 24 33.9395 24 34.0658 23.999C34.1723 23.9985 34.2788 23.9971 34.3857 23.9946C34.6175 23.9882 34.8517 23.9741 35.0816 23.9326C35.3143 23.8902 35.5312 23.8208 35.7427 23.7114C35.9509 23.6045 36.1407 23.4644 36.3061 23.2969C36.4709 23.1299 36.6092 22.9371 36.7152 22.7261C36.8232 22.5118 36.8915 22.2921 36.933 22.0557C36.9744 21.8234 36.9879 21.5861 36.9942 21.3512C36.9971 21.2433 36.9985 21.1354 36.999 21.0275C37 20.8991 37 20.7712 37 20.6427V3.35753C37 3.22909 37 3.10096 36.999 2.97274C36.9985 2.86487 36.9971 2.75697 36.9942 2.64907C36.9879 2.41399 36.9744 2.1767 36.933 1.94426C36.8915 1.70798 36.8232 1.48822 36.7152 1.27389C36.6092 1.06296 36.4709 0.870377 36.3061 0.703137C36.1407 0.535897 35.9509 0.395745 35.7427 0.288593C35.5312 0.17947 35.3143 0.110165 35.0816 0.0676751C34.8517 0.0259104 34.6175 0.0120266 34.3857 0.00566224C34.2788 0.00271788 34.1723 0.00151748 34.0658 0.000747416C33.9395 0 33.8128 0 33.6865 0Z" fill="black"/>
|
||||
<path d="M33.6865 0.799957L34.0602 0.800682C34.1614 0.801407 34.2626 0.802517 34.3644 0.805325C34.5414 0.810172 34.7485 0.819888 34.9416 0.854949C35.1094 0.88557 35.2501 0.932137 35.3852 1.0018C35.5185 1.07045 35.6407 1.16042 35.7473 1.26834C35.8544 1.37698 35.9433 1.50097 36.012 1.63756C36.0803 1.77334 36.126 1.91526 36.1561 2.08653C36.1906 2.28 36.2001 2.49041 36.205 2.6709C36.2077 2.77277 36.209 2.87465 36.2095 2.97895C36.2105 3.10508 36.2105 3.23114 36.2105 3.35752V20.6427C36.2105 20.7691 36.2105 20.8949 36.2095 21.0238C36.209 21.1256 36.2077 21.2275 36.205 21.3295C36.2001 21.5098 36.1906 21.7201 36.1556 21.9158C36.126 22.0847 36.0804 22.2267 36.0116 22.3631C35.9432 22.4994 35.8544 22.6232 35.7478 22.7312C35.6405 22.8399 35.5188 22.9296 35.3838 22.9989C35.2498 23.0682 35.1093 23.1147 34.9431 23.145C34.7462 23.1806 34.5304 23.1904 34.3679 23.1948C34.2656 23.1972 34.1639 23.1986 34.0597 23.1991C33.9354 23.2001 33.8108 23.2 33.6865 23.2H3.31352C3.31187 23.2 3.31026 23.2 3.30858 23.2C3.18577 23.2 3.06272 23.2 2.93767 23.1991C2.83571 23.1986 2.73398 23.1972 2.63563 23.1949C2.46941 23.1904 2.25349 23.1806 2.05814 23.1453C1.89056 23.1147 1.75006 23.0682 1.61424 22.998C1.48059 22.9293 1.35892 22.8397 1.2516 22.7308C1.14511 22.6231 1.05661 22.4996 0.988185 22.3632C0.919694 22.2268 0.873869 22.0845 0.843738 21.9135C0.808909 21.7182 0.799342 21.5087 0.79456 21.3297C0.791831 21.2272 0.790691 21.1248 0.790043 21.0229L0.789551 20.7221L0.789573 20.6427V3.35752L0.789551 3.27812L0.79002 2.978C0.790691 2.87551 0.791831 2.77304 0.79456 2.67065C0.799342 2.49147 0.808909 2.28192 0.844027 2.08492C0.873891 1.91553 0.919694 1.77316 0.988543 1.63616C1.05643 1.50074 1.14509 1.37712 1.25214 1.26868C1.35877 1.1606 1.48084 1.07075 1.61534 1.00144C1.74971 0.932114 1.89047 0.88557 2.05805 0.855017C2.25114 0.819866 2.45838 0.810172 2.63587 0.805302C2.73705 0.802517 2.83822 0.801407 2.93863 0.800705L3.31352 0.799957H33.6865Z" fill="white"/>
|
||||
<path d="M10.1008 8.0722C10.4176 7.67071 10.6325 7.13163 10.5758 6.58075C10.1121 6.60411 9.54616 6.89075 9.21852 7.29255C8.92433 7.63664 8.66394 8.19829 8.73183 8.72607C9.25242 8.77183 9.77253 8.46242 10.1008 8.0722Z" fill="black"/>
|
||||
<path d="M10.5699 8.82914C9.81391 8.78351 9.17111 9.26389 8.81007 9.26389C8.44883 9.26389 7.89596 8.85213 7.29798 8.86323C6.51968 8.87481 5.7975 9.32068 5.40255 10.0298C4.59019 11.4485 5.18817 13.5528 5.97814 14.7082C6.36177 15.2798 6.82411 15.9092 7.43331 15.8866C8.0089 15.8637 8.23451 15.509 8.93417 15.509C9.63331 15.509 9.83654 15.8866 10.4458 15.8752C11.0777 15.8637 11.4727 15.3033 11.8563 14.7311C12.2964 14.0795 12.4766 13.4503 12.4879 13.4158C12.4766 13.4043 11.2695 12.9351 11.2583 11.5283C11.2469 10.3503 12.206 9.79001 12.2512 9.75526C11.7096 8.94364 10.8633 8.85213 10.5699 8.82914Z" fill="black"/>
|
||||
<path d="M17.1528 7.23493C18.796 7.23493 19.9402 8.38257 19.9402 10.0534C19.9402 11.7303 18.7725 12.8839 17.1116 12.8839H15.2923V15.8154H13.9778V7.23492L17.1528 7.23493ZM15.2923 11.766H16.8005C17.945 11.766 18.5963 11.1417 18.5963 10.0594C18.5963 8.97726 17.945 8.35882 16.8064 8.35882H15.2923V11.766Z" fill="black"/>
|
||||
<path d="M20.2837 14.0375C20.2837 12.9433 21.1112 12.2714 22.5785 12.1881L24.2686 12.0871V11.6055C24.2686 10.9098 23.8049 10.4935 23.0304 10.4935C22.2966 10.4935 21.8388 10.8502 21.7274 11.4093H20.5302C20.6007 10.2794 21.5513 9.44693 23.0772 9.44693C24.5737 9.44693 25.5303 10.2497 25.5303 11.5043V15.8154H24.3154V14.7867H24.2862C23.9283 15.4824 23.1476 15.9224 22.3378 15.9224C21.1288 15.9224 20.2837 15.1613 20.2837 14.0375ZM24.2686 13.4726V12.9791L22.7485 13.0742C21.9914 13.1277 21.5631 13.4667 21.5631 14.0018C21.5631 14.5488 22.0091 14.9056 22.6899 14.9056C23.576 14.9056 24.2686 14.2872 24.2686 13.4726Z" fill="black"/>
|
||||
<path d="M26.6772 18.1166V17.076C26.7709 17.0997 26.9822 17.0997 27.0879 17.0997C27.6747 17.0997 27.9917 16.8501 28.1853 16.2079C28.1853 16.1959 28.2969 15.8273 28.2969 15.8214L26.0668 9.55991H27.4399L29.0012 14.65H29.0245L30.5858 9.55991H31.9238L29.6113 16.1424C29.0834 17.6588 28.473 18.1463 27.1936 18.1463C27.0879 18.1463 26.7709 18.1344 26.6772 18.1166Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10480_11384">
|
||||
<rect width="37" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
11
libs/shared/assets/src/images/payment-methods/google-pay.svg
Normal file
11
libs/shared/assets/src/images/payment-methods/google-pay.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="45" height="24" viewBox="0 0 45 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M33.0319 0H11.9681C5.38564 0 0 5.4 0 12C0 18.6 5.38564 24 11.9681 24H33.0319C39.6144 24 45 18.6 45 12C45 5.4 39.6144 0 33.0319 0Z" fill="white"/>
|
||||
<path d="M33.0319 0.972C34.51 0.972 35.9461 1.266 37.2985 1.842C38.609 2.4 39.7819 3.198 40.7992 4.212C41.8105 5.226 42.6064 6.408 43.1629 7.722C43.7374 9.078 44.0306 10.518 44.0306 12C44.0306 13.482 43.7374 14.922 43.1629 16.278C42.6064 17.592 41.8105 18.768 40.7992 19.788C39.7879 20.802 38.609 21.6 37.2985 22.158C35.9461 22.734 34.51 23.028 33.0319 23.028H11.9681C10.49 23.028 9.05386 22.734 7.70146 22.158C6.39096 21.6 5.21808 20.802 4.2008 19.788C3.18949 18.774 2.39362 17.592 1.8371 16.278C1.26263 14.922 0.969415 13.482 0.969415 12C0.969415 10.518 1.26263 9.078 1.8371 7.722C2.39362 6.408 3.18949 5.232 4.2008 4.212C5.2121 3.198 6.39096 2.4 7.70146 1.842C9.05386 1.266 10.49 0.972 11.9681 0.972H33.0319ZM33.0319 0H11.9681C5.38564 0 0 5.4 0 12C0 18.6 5.38564 24 11.9681 24H33.0319C39.6144 24 45 18.6 45 12C45 5.4 39.6144 0 33.0319 0Z" fill="#3C4043"/>
|
||||
<path d="M21.4587 12.852V16.482H20.3098V7.51801H23.3557C24.1276 7.51801 24.7859 7.77601 25.3244 8.29201C25.875 8.80801 26.1502 9.43801 26.1502 10.182C26.1502 10.944 25.875 11.574 25.3244 12.084C24.7919 12.594 24.1336 12.846 23.3557 12.846H21.4587V12.852ZM21.4587 8.62201V11.748H23.3796C23.8344 11.748 24.2174 11.592 24.5166 11.286C24.8218 10.98 24.9774 10.608 24.9774 10.188C24.9774 9.77401 24.8218 9.40801 24.5166 9.10201C24.2174 8.78401 23.8404 8.62801 23.3796 8.62801H21.4587V8.62201Z" fill="#3C4043"/>
|
||||
<path d="M29.1542 10.146C30.004 10.146 30.6742 10.374 31.1649 10.83C31.6556 11.286 31.9009 11.91 31.9009 12.702V16.482H30.8058V15.63H30.758C30.2852 16.332 29.6509 16.68 28.861 16.68C28.1848 16.68 27.6223 16.482 27.1675 16.08C26.7127 15.678 26.4854 15.18 26.4854 14.58C26.4854 13.944 26.7247 13.44 27.2034 13.068C27.6822 12.69 28.3225 12.504 29.1183 12.504C29.8005 12.504 30.363 12.63 30.7998 12.882V12.618C30.7998 12.216 30.6443 11.88 30.3271 11.598C30.01 11.316 29.6389 11.178 29.2141 11.178C28.5738 11.178 28.0651 11.448 27.6941 11.994L26.6828 11.358C27.2393 10.548 28.0651 10.146 29.1542 10.146ZM27.6702 14.598C27.6702 14.898 27.7959 15.15 28.0532 15.348C28.3045 15.546 28.6037 15.648 28.9448 15.648C29.4295 15.648 29.8604 15.468 30.2373 15.108C30.6143 14.748 30.8058 14.328 30.8058 13.842C30.4468 13.56 29.9501 13.416 29.3098 13.416C28.8431 13.416 28.4541 13.53 28.1429 13.752C27.8258 13.986 27.6702 14.268 27.6702 14.598Z" fill="#3C4043"/>
|
||||
<path d="M38.1482 10.344L34.3184 19.176H33.1336L34.5578 16.086L32.0325 10.344H33.2832L35.1023 14.748H35.1263L36.8976 10.344H38.1482Z" fill="#3C4043"/>
|
||||
<path d="M16.8888 12.12C16.8888 11.7444 16.8552 11.385 16.793 11.0394H11.9771V13.0194L14.7507 13.02C14.6382 13.6788 14.2761 14.2404 13.7214 14.6148V15.8994H15.3724C16.3364 15.0048 16.8888 13.6824 16.8888 12.12Z" fill="#4285F4"/>
|
||||
<path d="M13.722 14.6148C13.2624 14.9256 12.6706 15.1074 11.9782 15.1074C10.6408 15.1074 9.50623 14.2038 9.09991 12.9858H7.39685V14.3106C8.2406 15.9894 9.97478 17.1414 11.9782 17.1414C13.3629 17.1414 14.5262 16.6848 15.373 15.8988L13.722 14.6148Z" fill="#34A853"/>
|
||||
<path d="M8.93955 12.003C8.93955 11.661 8.99639 11.3304 9.09992 11.0196V9.69482H7.39686C7.04799 10.389 6.85172 11.1726 6.85172 12.003C6.85172 12.8334 7.04859 13.617 7.39686 14.3112L9.09992 12.9864C8.99639 12.6756 8.93955 12.345 8.93955 12.003Z" fill="#FABB05"/>
|
||||
<path d="M11.9782 8.89801C12.734 8.89801 13.4108 9.15901 13.9452 9.66901L15.4083 8.20321C14.5197 7.37341 13.3611 6.86401 11.9782 6.86401C9.97537 6.86401 8.2406 8.01601 7.39685 9.69481L9.09991 11.0196C9.50623 9.80161 10.6408 8.89801 11.9782 8.89801Z" fill="#E94235"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@ -51,7 +51,9 @@
|
||||
-product-pocket = Pocket
|
||||
|
||||
-brand-apple = Apple
|
||||
-brand-apple-pay = Apple Pay
|
||||
-brand-google = Google
|
||||
-brand-google-pay = Google Pay
|
||||
-brand-paypal = PayPal
|
||||
-brand-name-stripe = Stripe
|
||||
-brand-amex = American Express
|
||||
|
||||
Loading…
Reference in New Issue
Block a user