diff --git a/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource-interface.ts b/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource-interface.ts index cb58ce67..52fcb55d 100644 --- a/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource-interface.ts +++ b/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource-interface.ts @@ -1,9 +1,7 @@ -import { DonationPaymentInfo } from '@internetarchive/donation-form-data-models'; import { ApplePaySessionDataSourceDelegate } from './apple-pay-session-datasource-delegate'; export interface ApplePaySessionDataSourceInterface { delegate?: ApplePaySessionDataSourceDelegate; - donationInfo: DonationPaymentInfo; onvalidatemerchant(event: ApplePayJS.ApplePayValidateMerchantEvent): Promise; onpaymentauthorized(event: ApplePayJS.ApplePayPaymentAuthorizedEvent): Promise; oncancel(): Promise; diff --git a/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource.ts b/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource.ts index 2de711da..7b09af7d 100644 --- a/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource.ts +++ b/packages/donation-form/src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource.ts @@ -10,8 +10,8 @@ import { ApplePaySessionDataSourceDelegate } from './apple-pay-session-datasourc export class ApplePaySessionDataSource implements ApplePaySessionDataSourceInterface { delegate?: ApplePaySessionDataSourceDelegate; - donationInfo: DonationPaymentInfo; + private donationInfo: DonationPaymentInfo; private session: ApplePaySession; // eslint-disable-next-line @typescript-eslint/no-explicit-any private applePayInstance: any; diff --git a/packages/donation-form/src/payment-flow-handlers/handlers/applepay-flow-handler.ts b/packages/donation-form/src/payment-flow-handlers/handlers/applepay-flow-handler.ts index 68e1d122..788cbb2b 100644 --- a/packages/donation-form/src/payment-flow-handlers/handlers/applepay-flow-handler.ts +++ b/packages/donation-form/src/payment-flow-handlers/handlers/applepay-flow-handler.ts @@ -68,7 +68,7 @@ export class ApplePayFlowHandler paymentComplete(response: DonationResponse): void { if (response.success) { const successResponse = response.value as SuccessResponse; - if (this.applePayDataSource?.donationInfo.donationType == DonationType.OneTime) { + if (successResponse.donationType == DonationType.OneTime) { this.donationFlowModalManager.showUpsellModal({ oneTimeAmount: successResponse.amount, yesSelected: this.modalYesSelected.bind(this, successResponse), diff --git a/packages/donation-form/test/mocks/mock-apple-pay-authorized-event.ts b/packages/donation-form/test/mocks/mock-apple-pay-authorized-event.ts new file mode 100644 index 00000000..d2ab9ea7 --- /dev/null +++ b/packages/donation-form/test/mocks/mock-apple-pay-authorized-event.ts @@ -0,0 +1,5 @@ +import { MockApplePayPayment } from './payment-clients/mock-applepay-payment'; + +export class MockApplePayAuthorizedEvent extends ApplePayJS.ApplePayPaymentAuthorizedEvent { + readonly payment: ApplePayJS.ApplePayPayment = new MockApplePayPayment(); +} diff --git a/packages/donation-form/test/mocks/mock-apple-pay-session.ts b/packages/donation-form/test/mocks/mock-apple-pay-session.ts new file mode 100644 index 00000000..cc005c54 --- /dev/null +++ b/packages/donation-form/test/mocks/mock-apple-pay-session.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +export class MockApplePaySession implements ApplePaySession { + oncancel: (event: ApplePayJS.Event) => void = () => { + console.log('oncancel'); + }; + onpaymentauthorized: (event: ApplePayJS.ApplePayPaymentAuthorizedEvent) => void = () => { + console.log('onpaymentauthorized'); + }; + onpaymentmethodselected: (event: ApplePayJS.ApplePayPaymentMethodSelectedEvent) => void = () => { + console.log('onpaymentmethodselected'); + }; + onshippingcontactselected: ( + event: ApplePayJS.ApplePayShippingContactSelectedEvent, + ) => void = () => { + console.log('onshippingcontactselected'); + }; + onshippingmethodselected: ( + event: ApplePayJS.ApplePayShippingMethodSelectedEvent, + ) => void = () => { + console.log('onshippingmethodselected'); + }; + onvalidatemerchant: (event: ApplePayJS.ApplePayValidateMerchantEvent) => void = () => { + console.log('onvalidatemerchant'); + }; + abort(): void { + throw new Error('Method not implemented.'); + } + begin(): void { + throw new Error('Method not implemented.'); + } + completeMerchantValidation(merchantSession: any): void { + throw new Error('Method not implemented.'); + } + completePayment(result: number | ApplePayJS.ApplePayPaymentAuthorizationResult): void { + throw new Error('Method not implemented.'); + } + completePaymentMethodSelection( + newTotal: ApplePayJS.ApplePayLineItem, + newLineItems: ApplePayJS.ApplePayLineItem[], + ): void; + completePaymentMethodSelection(update: ApplePayJS.ApplePayPaymentMethodUpdate): void; + completePaymentMethodSelection(newTotal: any, newLineItems?: any): void { + throw new Error('Method not implemented.'); + } + completeShippingContactSelection( + status: number, + newShippingMethods: ApplePayJS.ApplePayShippingMethod[], + newTotal: ApplePayJS.ApplePayLineItem, + newLineItems: ApplePayJS.ApplePayLineItem[], + ): void; + completeShippingContactSelection(update: ApplePayJS.ApplePayShippingContactUpdate): void; + completeShippingContactSelection( + status: any, + newShippingMethods?: any, + newTotal?: any, + newLineItems?: any, + ): void { + throw new Error('Method not implemented.'); + } + completeShippingMethodSelection( + status: number, + newTotal: ApplePayJS.ApplePayLineItem, + newLineItems: ApplePayJS.ApplePayLineItem[], + ): void; + completeShippingMethodSelection(update: ApplePayJS.ApplePayShippingMethodUpdate): void; + completeShippingMethodSelection(status: any, newTotal?: any, newLineItems?: any): void { + throw new Error('Method not implemented.'); + } + addEventListener( + type: string, + listener: EventListener | EventListenerObject | null, + options?: boolean | AddEventListenerOptions, + ): void { + throw new Error('Method not implemented.'); + } + dispatchEvent(event: Event): boolean { + throw new Error('Method not implemented.'); + } + removeEventListener( + type: string, + callback: EventListener | EventListenerObject | null, + options?: boolean | EventListenerOptions, + ): void { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/donation-form/test/mocks/mock-braintree-manager.ts b/packages/donation-form/test/mocks/mock-braintree-manager.ts index c8e1afca..bc26639e 100644 --- a/packages/donation-form/test/mocks/mock-braintree-manager.ts +++ b/packages/donation-form/test/mocks/mock-braintree-manager.ts @@ -18,6 +18,19 @@ import { MockBraintreeClient } from './payment-clients/mock-braintree-client'; import { PaymentProvidersInterface } from '../../src/braintree-manager/payment-providers-interface'; export class MockBraintreeManager implements BraintreeManagerInterface { + submitDonationOptions?: { + nonce: string; + paymentProvider: PaymentProvider; + donationInfo: DonationPaymentInfo; + billingInfo: BillingInfo; + customerInfo: CustomerInfo; + upsellOnetimeTransactionId?: string | undefined; + customerId?: string | undefined; + recaptchaToken?: string | undefined; + bin?: string | undefined; + binName?: string | undefined; + }; + donationSuccessfulOptions?: { successResponse: SuccessResponse; upsellSuccessResponse?: SuccessResponse | undefined; @@ -34,7 +47,9 @@ export class MockBraintreeManager implements BraintreeManagerInterface { this.submitDonationResponse = options?.submitDonationResponse ?? 'success'; } - paymentProviders: PaymentProvidersInterface = new MockPaymentProviders(); + paymentProviders: PaymentProvidersInterface = new MockPaymentProviders({ + braintreeManager: this, + }); instance: PromisedSingleton = new PromisedSingleton({ generator: new Promise(resolve => { resolve(new MockBraintreeClient()); @@ -65,6 +80,8 @@ export class MockBraintreeManager implements BraintreeManagerInterface { throw 'oh no'; } + this.submitDonationOptions = options; + if (this.submitDonationResponse === 'success') { const response = new SuccessResponse({ paymentMethodNonce: options.nonce, diff --git a/packages/donation-form/test/mocks/mock-donation-flow-modal-manager.ts b/packages/donation-form/test/mocks/mock-donation-flow-modal-manager.ts new file mode 100644 index 00000000..b39747aa --- /dev/null +++ b/packages/donation-form/test/mocks/mock-donation-flow-modal-manager.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + SuccessResponse, + PaymentProvider, + DonationPaymentInfo, + BillingInfo, + CustomerInfo, + DonationResponse, +} from '@internetarchive/donation-form-data-models'; +import { UpsellModalCTAMode } from '../../src/modals/upsell-modal-content'; +import { DonationFlowModalManagerInterface } from '../../src/payment-flow-handlers/donation-flow-modal-manager'; + +export class MockDonationFlowModalManager implements DonationFlowModalManagerInterface { + closeModalCalled = false; + showProcessingModalCalled = false; + showThankYouModalOptions?: { + successResponse: SuccessResponse; + upsellSuccessResponse?: SuccessResponse | undefined; + }; + showErrorModalOptions?: { + message: string; + userClosedModalCallback?: (() => void) | undefined; + }; + showUpsellModalOptions?: { + oneTimeAmount: number; + ctaMode?: UpsellModalCTAMode | undefined; + yesSelected?: ((amount: number) => void) | undefined; + noSelected?: (() => void) | undefined; + amountChanged?: ((amount: number) => void) | undefined; + userClosedModalCallback?: (() => void) | undefined; + }; + startDonationSubmissionFlowOptions?: { + nonce: string; + paymentProvider: PaymentProvider; + donationInfo: DonationPaymentInfo; + billingInfo: BillingInfo; + customerInfo: CustomerInfo; + upsellOnetimeTransactionId?: string | undefined; + customerId?: string | undefined; + recaptchaToken?: string | undefined; + bin?: string | undefined; + binName?: string | undefined; + }; + handleSuccessfulDonationResponseDonationInfo?: DonationPaymentInfo; + handleSuccessfulDonationResponseResponse?: SuccessResponse; + + closeModal(): void { + this.closeModalCalled = true; + } + showProcessingModal(): void { + this.showProcessingModalCalled = true; + } + showThankYouModal(options: { + successResponse: SuccessResponse; + upsellSuccessResponse?: SuccessResponse | undefined; + }): void { + this.showThankYouModalOptions = options; + } + showErrorModal(options: { + message: string; + userClosedModalCallback?: (() => void) | undefined; + }): void { + this.showErrorModalOptions = options; + } + async showUpsellModal(options: { + oneTimeAmount: number; + ctaMode?: UpsellModalCTAMode | undefined; + yesSelected?: ((amount: number) => void) | undefined; + noSelected?: (() => void) | undefined; + amountChanged?: ((amount: number) => void) | undefined; + userClosedModalCallback?: (() => void) | undefined; + }): Promise { + this.showUpsellModalOptions = options; + } + async startDonationSubmissionFlow(options: { + nonce: string; + paymentProvider: PaymentProvider; + donationInfo: DonationPaymentInfo; + billingInfo: BillingInfo; + customerInfo: CustomerInfo; + upsellOnetimeTransactionId?: string | undefined; + customerId?: string | undefined; + recaptchaToken?: string | undefined; + bin?: string | undefined; + binName?: string | undefined; + }): Promise { + this.startDonationSubmissionFlowOptions = options; + return undefined; + } + handleSuccessfulDonationResponse( + donationInfo: DonationPaymentInfo, + response: SuccessResponse, + ): void { + this.handleSuccessfulDonationResponseDonationInfo = donationInfo; + this.handleSuccessfulDonationResponseResponse = response; + } +} diff --git a/packages/donation-form/test/mocks/payment-providers/individual-providers/mock-applepay-handler.ts b/packages/donation-form/test/mocks/payment-providers/individual-providers/mock-applepay-handler.ts index 26db459a..c579ef43 100644 --- a/packages/donation-form/test/mocks/payment-providers/individual-providers/mock-applepay-handler.ts +++ b/packages/donation-form/test/mocks/payment-providers/individual-providers/mock-applepay-handler.ts @@ -5,8 +5,19 @@ import { DonationPaymentInfo } from '@internetarchive/donation-form-data-models' import { MockApplePayClient } from '../../payment-clients/mock-applepay-client'; import { ApplePaySessionDataSource } from '../../../../src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource'; import { ApplePayHandlerInterface } from '../../../../src/braintree-manager/payment-providers/apple-pay/apple-pay-interface'; +import { BraintreeManagerInterface } from '../../../../src/braintree-manager/braintree-interfaces'; +import { MockApplePaySession } from '../../payment-clients/mock-applepay-session'; +import { ApplePaySessionDataSourceInterface } from '../../../../src/braintree-manager/payment-providers/apple-pay/apple-pay-session-datasource-interface'; export class MockApplePayHandler implements ApplePayHandlerInterface { + dataSource?: ApplePaySessionDataSourceInterface; + + private braintreeManager: BraintreeManagerInterface; + + constructor(braintreeManager: BraintreeManagerInterface) { + this.braintreeManager = braintreeManager; + } + instance: PromisedSingleton = new PromisedSingleton({ generator: new Promise(resolve => { resolve(new MockApplePayClient()); @@ -17,10 +28,17 @@ export class MockApplePayHandler implements ApplePayHandlerInterface { throw new Error('Method not implemented.'); } - createPaymentRequest( + async createPaymentRequest( e: Event, donationInfo: DonationPaymentInfo, - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + const session = new MockApplePaySession(); + this.dataSource = new ApplePaySessionDataSource({ + donationInfo: donationInfo, + session: session, + applePayInstance: 'foo', + braintreeManager: this.braintreeManager, + }); + return this.dataSource; } } diff --git a/packages/donation-form/test/mocks/payment-providers/mock-payment-providers.ts b/packages/donation-form/test/mocks/payment-providers/mock-payment-providers.ts index be46d959..9d8e4eef 100644 --- a/packages/donation-form/test/mocks/payment-providers/mock-payment-providers.ts +++ b/packages/donation-form/test/mocks/payment-providers/mock-payment-providers.ts @@ -11,6 +11,7 @@ import { ApplePayHandlerInterface } from '../../../src/braintree-manager/payment import { PayPalHandlerInterface } from '../../../src/braintree-manager/payment-providers/paypal/paypal-interface'; import { GooglePayHandlerInterface } from '../../../src/braintree-manager/payment-providers/google-pay-interface'; import { VenmoHandlerInterface } from '../../../src/braintree-manager/payment-providers/venmo-interface'; +import { BraintreeManagerInterface } from '../../../src/braintree-manager/braintree-interfaces'; export class MockPaymentProviders implements PaymentProvidersInterface { creditCardHandler: PromisedSingleton = new PromisedSingleton< @@ -29,7 +30,7 @@ export class MockPaymentProviders implements PaymentProvidersInterface { ApplePayHandlerInterface >({ generator: new Promise(resolve => { - resolve(new MockApplePayHandler()); + resolve(new MockApplePayHandler(this.braintreeManager)); }), }); @@ -57,12 +58,15 @@ export class MockPaymentProviders implements PaymentProvidersInterface { }), }); - constructor(options?: { + constructor(options: { + braintreeManager: BraintreeManagerInterface; mockHostedFieldTokenizePayload?: braintree.HostedFieldsTokenizePayload; }) { + this.braintreeManager = options.braintreeManager; this.mockHostedFieldTokenizePayload = - options?.mockHostedFieldTokenizePayload ?? mockHostedFieldTokenizePayload; + options.mockHostedFieldTokenizePayload ?? mockHostedFieldTokenizePayload; } mockHostedFieldTokenizePayload: braintree.HostedFieldsTokenizePayload; + braintreeManager: BraintreeManagerInterface; } diff --git a/packages/donation-form/test/tests/flow-handlers/handlers/apple-pay-flow-handler.test.ts b/packages/donation-form/test/tests/flow-handlers/handlers/apple-pay-flow-handler.test.ts new file mode 100644 index 00000000..671b358b --- /dev/null +++ b/packages/donation-form/test/tests/flow-handlers/handlers/apple-pay-flow-handler.test.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { expect } from '@open-wc/testing'; +import { MockDonationFlowModalManager } from '../../../mocks/mock-donation-flow-modal-manager'; +import { ApplePayFlowHandler } from '../../../../src/payment-flow-handlers/handlers/applepay-flow-handler'; +import { MockBraintreeManager } from '../../../mocks/mock-braintree-manager'; +import { + DonationPaymentInfo, + DonationResponse, + DonationType, + ErrorResponse, +} from '@internetarchive/donation-form-data-models'; +import { MockApplePayHandler } from '../../../mocks/payment-providers/individual-providers/mock-applepay-handler'; +import { mockSuccessResponse } from '../../../mocks/models/mock-success-response'; + +describe('ApplePay Flow Handler', () => { + it('handles the paymentInitiated event properly', async () => { + const braintreeManager = new MockBraintreeManager(); + const modalFlowManager = new MockDonationFlowModalManager(); + const applePayFlowHandler = new ApplePayFlowHandler({ + braintreeManager: braintreeManager, + donationFlowModalManager: modalFlowManager, + }); + const donationInfo = new DonationPaymentInfo({ + donationType: DonationType.OneTime, + amount: 3.5, + coverFees: false, + }); + const mockEvent = new Event('boop'); + await applePayFlowHandler.paymentInitiated(donationInfo, mockEvent); + expect(modalFlowManager.showProcessingModalCalled).to.be.true; + const handler = (await braintreeManager?.paymentProviders.applePayHandler.get()) as MockApplePayHandler; + const dataSource = handler.dataSource; + expect(dataSource?.delegate).to.not.be.undefined; + }); + + it('shows the upsell modal after a one-time donation completes successfully', async () => { + const braintreeManager = new MockBraintreeManager(); + const modalFlowManager = new MockDonationFlowModalManager(); + const applePayFlowHandler = new ApplePayFlowHandler({ + braintreeManager: braintreeManager, + donationFlowModalManager: modalFlowManager, + }); + const response = new DonationResponse({ success: true, value: mockSuccessResponse }); + expect(modalFlowManager.showUpsellModalOptions).to.be.undefined; + applePayFlowHandler.paymentComplete(response); + expect(modalFlowManager.showUpsellModalOptions).to.not.be.undefined; + }); + + it('shows the thank you modal after a monthly donation completes successfully', async () => { + const braintreeManager = new MockBraintreeManager(); + const modalFlowManager = new MockDonationFlowModalManager(); + const applePayFlowHandler = new ApplePayFlowHandler({ + braintreeManager: braintreeManager, + donationFlowModalManager: modalFlowManager, + }); + const mockMonthlySuccessResponse = mockSuccessResponse; + mockMonthlySuccessResponse.donationType = DonationType.Monthly; + const response = new DonationResponse({ success: true, value: mockMonthlySuccessResponse }); + expect(modalFlowManager.showThankYouModalOptions).to.be.undefined; + applePayFlowHandler.paymentComplete(response); + expect(modalFlowManager.showThankYouModalOptions).to.not.be.undefined; + }); + + it('shows the error modal if the response was unsuccessful', async () => { + const braintreeManager = new MockBraintreeManager(); + const modalFlowManager = new MockDonationFlowModalManager(); + const applePayFlowHandler = new ApplePayFlowHandler({ + braintreeManager: braintreeManager, + donationFlowModalManager: modalFlowManager, + }); + const errorResponse = new ErrorResponse({ message: 'oh no' }); + const response = new DonationResponse({ success: false, value: errorResponse }); + expect(modalFlowManager.showErrorModalOptions).to.be.undefined; + applePayFlowHandler.paymentComplete(response); + expect(modalFlowManager.showErrorModalOptions).to.not.be.undefined; + }); + + it('shows the error modal if the payment fails', async () => { + const braintreeManager = new MockBraintreeManager(); + const modalFlowManager = new MockDonationFlowModalManager(); + const applePayFlowHandler = new ApplePayFlowHandler({ + braintreeManager: braintreeManager, + donationFlowModalManager: modalFlowManager, + }); + applePayFlowHandler.paymentFailed(); + expect(modalFlowManager.showErrorModalOptions).to.not.be.undefined; + }); + + it('closes the modal if the payment is cancelled', async () => { + const braintreeManager = new MockBraintreeManager(); + const modalFlowManager = new MockDonationFlowModalManager(); + const applePayFlowHandler = new ApplePayFlowHandler({ + braintreeManager: braintreeManager, + donationFlowModalManager: modalFlowManager, + }); + applePayFlowHandler.paymentCancelled(); + expect(modalFlowManager.closeModalCalled).to.be.true; + }); +}); diff --git a/packages/donation-form/test/tests/form-elements/payment-selector.test.ts b/packages/donation-form/test/tests/form-elements/payment-selector.test.ts index b7115cd7..f9c0422b 100644 --- a/packages/donation-form/test/tests/form-elements/payment-selector.test.ts +++ b/packages/donation-form/test/tests/form-elements/payment-selector.test.ts @@ -3,6 +3,7 @@ import { PaymentSelector } from '../../../src/form-elements/payment-selector'; import '../../../src/form-elements/payment-selector'; import { MockPaymentProviders } from '../../mocks/payment-providers/mock-payment-providers'; import { promisedSleep } from '../../helpers/promisedSleep'; +import { MockBraintreeManager } from '../../mocks/mock-braintree-manager'; describe('Payment Selector', () => { it('shows Venmo if it is available', async () => { @@ -10,7 +11,8 @@ describe('Payment Selector', () => { `)) as PaymentSelector; - const paymentProviders = new MockPaymentProviders(); + const mockBraintreeManager = new MockBraintreeManager(); + const paymentProviders = new MockPaymentProviders({ braintreeManager: mockBraintreeManager }); el.paymentProviders = paymentProviders; await elementUpdated(el); await promisedSleep(250); diff --git a/packages/donation-form/test/tests/payment-providers/applepay.test.ts b/packages/donation-form/test/tests/payment-providers/applepay.test.ts index 9eb03ff9..8760a99d 100644 --- a/packages/donation-form/test/tests/payment-providers/applepay.test.ts +++ b/packages/donation-form/test/tests/payment-providers/applepay.test.ts @@ -6,6 +6,8 @@ import { MockApplePayClient } from '../../mocks/payment-clients/mock-applepay-cl import { MockApplePaySessionManager } from '../../mocks/payment-clients/mock-applepay-sessionmanager'; import { PromisedSingleton } from '@internetarchive/promised-singleton'; import { MockDonationInfo } from '../../mocks/mock-donation-info'; +import { MockApplePaySessionDataSourceDelegate } from '../../mocks/payment-providers/individual-providers/mock-applepay-datasource-delegate'; +import { MockApplePayPaymentAuthorizedEvent } from '../../mocks/payment-clients/mock-applepay-paymentauthorizedevent'; describe('ApplePayHandler', () => { describe('isAvailable', () => { @@ -75,7 +77,13 @@ describe('ApplePayHandler', () => { }); const event = new Event('boop'); const datasource = await handler.createPaymentRequest(event, new MockDonationInfo()); - expect(datasource.donationInfo.amount).to.equal(new MockDonationInfo().amount); + const delegate = new MockApplePaySessionDataSourceDelegate(); + datasource.delegate = delegate; + + const paymentAuthorizedEvent = new MockApplePayPaymentAuthorizedEvent(); + expect(braintreeManager.submitDonationOptions).to.be.undefined; + await datasource.onpaymentauthorized(paymentAuthorizedEvent); + expect(braintreeManager.submitDonationOptions).to.not.be.undefined; }); }); });