diff --git a/packages/core/src/payment-integration/create-payment-integration-service.ts b/packages/core/src/payment-integration/create-payment-integration-service.ts index 607cc8ce69..0061a1d19b 100644 --- a/packages/core/src/payment-integration/create-payment-integration-service.ts +++ b/packages/core/src/payment-integration/create-payment-integration-service.ts @@ -45,6 +45,8 @@ import { SubscriptionsActionCreator, SubscriptionsRequestSender } from '../subsc import createPaymentIntegrationSelectors from './create-payment-integration-selectors'; import DefaultPaymentIntegrationService from './default-payment-integration-service'; import PaymentIntegrationStoreProjectionFactory from './payment-integration-store-projection-factory'; +import CouponActionCreator from '../coupon/coupon-action-creator'; +import CouponRequestSender from '../coupon/coupon-request-sender'; export default function createPaymentIntegrationService( store: CheckoutStore, @@ -112,6 +114,10 @@ export default function createPaymentIntegrationService( new StoreCreditRequestSender(requestSender), ); + const applyCouponActionCreator = new CouponActionCreator( + new CouponRequestSender(requestSender), + ); + const spamProtection = createSpamProtection(createScriptLoader()); const spamProtectionRequestSender = new SpamProtectionRequestSender(requestSender); const spamProtectionActionCreator = new SpamProtectionActionCreator( @@ -150,6 +156,7 @@ export default function createPaymentIntegrationService( customerActionCreator, cartRequestSender, storeCreditActionCreator, + applyCouponActionCreator, spamProtectionActionCreator, paymentProviderCustomerActionCreator, shippingCountryActionCreator, diff --git a/packages/core/src/payment-integration/default-payment-integration-service.spec.ts b/packages/core/src/payment-integration/default-payment-integration-service.spec.ts index 9ff4763a57..a8756670b6 100644 --- a/packages/core/src/payment-integration/default-payment-integration-service.spec.ts +++ b/packages/core/src/payment-integration/default-payment-integration-service.spec.ts @@ -41,6 +41,7 @@ import { StoreCreditActionCreator } from '../store-credit'; import DefaultPaymentIntegrationService from './default-payment-integration-service'; import PaymentIntegrationStoreProjectionFactory from './payment-integration-store-projection-factory'; +import CouponActionCreator from '../coupon/coupon-action-creator'; describe('DefaultPaymentIntegrationService', () => { let subject: PaymentIntegrationService; @@ -84,6 +85,7 @@ describe('DefaultPaymentIntegrationService', () => { let customerActionCreator: Pick; let cartRequestSender: CartRequestSender; let storeCreditActionCreator: Pick; + let applyCouponActionCreator: Pick; let paymentProviderCustomerActionCreator: PaymentProviderCustomerActionCreator; let shippingCountryActionCreator: Pick; let remoteCheckoutActionCreator: Pick< @@ -226,6 +228,15 @@ describe('DefaultPaymentIntegrationService', () => { ), }; + applyCouponActionCreator = { + // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + applyCoupon: jest.fn( + async () => () => createAction('APPLY_COUPON'), + ), + }; + spamProtectionActionCreator = { // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -295,6 +306,7 @@ describe('DefaultPaymentIntegrationService', () => { customerActionCreator as CustomerActionCreator, cartRequestSender, storeCreditActionCreator as StoreCreditActionCreator, + applyCouponActionCreator as CouponActionCreator, spamProtectionActionCreator as SpamProtectionActionCreator, paymentProviderCustomerActionCreator, shippingCountryActionCreator as ShippingCountryActionCreator, diff --git a/packages/core/src/payment-integration/default-payment-integration-service.ts b/packages/core/src/payment-integration/default-payment-integration-service.ts index a7f218a201..bdb2bf372a 100644 --- a/packages/core/src/payment-integration/default-payment-integration-service.ts +++ b/packages/core/src/payment-integration/default-payment-integration-service.ts @@ -35,6 +35,7 @@ import { PaymentHumanVerificationHandler, SpamProtectionActionCreator } from '.. import { StoreCreditActionCreator } from '../store-credit'; import PaymentIntegrationStoreProjectionFactory from './payment-integration-store-projection-factory'; +import CouponActionCreator from '../coupon/coupon-action-creator'; export default class DefaultPaymentIntegrationService implements PaymentIntegrationService { private _storeProjection: DataStoreProjection; @@ -54,6 +55,7 @@ export default class DefaultPaymentIntegrationService implements PaymentIntegrat private _customerActionCreator: CustomerActionCreator, private _cartRequestSender: CartRequestSender, private _storeCreditActionCreator: StoreCreditActionCreator, + private _couponActionCreator: CouponActionCreator, private _spamProtectionActionCreator: SpamProtectionActionCreator, private _paymentProviderCustomerActionCreator: PaymentProviderCustomerActionCreator, private _shippingCountryActionCreator: ShippingCountryActionCreator, @@ -215,6 +217,28 @@ export default class DefaultPaymentIntegrationService implements PaymentIntegrat return this._storeProjection.getState(); } + async applyCoupon( + coupon: string, + options?: RequestOptions, + ): Promise { + await this._store.dispatch( + this._couponActionCreator.applyCoupon(coupon, options), + ); + + return this._storeProjection.getState(); + } + + async removeCoupon( + coupon: string, + options?: RequestOptions, + ): Promise { + await this._store.dispatch( + this._couponActionCreator.removeCoupon(coupon, options), + ); + + return this._storeProjection.getState(); + } + async verifyCheckoutSpamProtection(): Promise { const { checkout } = this._store.getState(); const { shouldExecuteSpamCheck } = checkout.getCheckoutOrThrow(); diff --git a/packages/google-pay-integration/src/gateways/google-pay-gateway.ts b/packages/google-pay-integration/src/gateways/google-pay-gateway.ts index a1acd50051..1944842dfd 100644 --- a/packages/google-pay-integration/src/gateways/google-pay-gateway.ts +++ b/packages/google-pay-integration/src/gateways/google-pay-gateway.ts @@ -28,10 +28,10 @@ import { GooglePayFullBillingAddress, GooglePayGatewayParameters, GooglePayInitializationData, - GooglePayMerchantInfo, + GooglePayMerchantInfo, GooglePayPaymentDataRequest, GooglePayRequiredPaymentData, GooglePaySetExternalCheckoutData, - GooglePayTransactionInfo, + GooglePayTransactionInfo, IntermediatePaymentData, ShippingOptionParameters, TotalPriceStatusType, } from '../types'; @@ -142,19 +142,23 @@ export default class GooglePayGateway { CallbackTriggerType.INITIALIZE, CallbackTriggerType.SHIPPING_ADDRESS, CallbackTriggerType.SHIPPING_OPTION, + CallbackTriggerType.OFFER, ]; + const initializationTrigger = [CallbackTriggerType.INITIALIZE]; const addressChangeTriggers = [ CallbackTriggerType.INITIALIZE, CallbackTriggerType.SHIPPING_ADDRESS, ]; const shippingOptionsChangeTriggers = [CallbackTriggerType.SHIPPING_OPTION]; + const offerChangeTriggers = [CallbackTriggerType.OFFER]; return { availableTriggers, initializationTrigger, addressChangeTriggers, shippingOptionsChangeTriggers, + offerChangeTriggers, }; } @@ -311,6 +315,42 @@ export default class GooglePayGateway { } } + getAppliedCoupons(): GooglePayPaymentDataRequest['offerInfo'] { + const state = this._paymentIntegrationService.getState(); + const { coupons } = state.getCheckout() || {}; + + const offers = (coupons || []).map((coupon) => { + const { displayName, code } = coupon; + return { + redemptionCode: code, + description: displayName, + } + }); + + return { + offers, + } + } + + async handleCoupons(offerData: IntermediatePaymentData['offerData']): Promise { + const { redemptionCodes = [] } = offerData || {}; + + if (!redemptionCodes.length) { + const { offers } = this.getAppliedCoupons(); + + const couponsPromise = offers.map((couponCode) => { + return this._paymentIntegrationService.removeCoupon(couponCode.redemptionCode); + }); + + await Promise.all(couponsPromise); + } else { + const code = redemptionCodes[redemptionCodes.length - 1]; + await this._paymentIntegrationService.applyCoupon(code); + } + + return this.getAppliedCoupons(); + } + async handleShippingOptionChange(optionId: string) { if (optionId === 'shipping_option_unselected') { return; diff --git a/packages/google-pay-integration/src/google-pay-button-strategy.ts b/packages/google-pay-integration/src/google-pay-button-strategy.ts index 4d9e50f965..0597530df6 100644 --- a/packages/google-pay-integration/src/google-pay-button-strategy.ts +++ b/packages/google-pay-integration/src/google-pay-button-strategy.ts @@ -193,11 +193,13 @@ export default class GooglePayButtonStrategy implements CheckoutButtonStrategy { callbackTrigger, shippingAddress, shippingOptionData, + offerData, }: IntermediatePaymentData): Promise => { const { availableTriggers, addressChangeTriggers, shippingOptionsChangeTriggers, + offerChangeTriggers, } = this._googlePayPaymentProcessor.getCallbackTriggers(); if (!availableTriggers.includes(callbackTrigger)) { @@ -216,6 +218,10 @@ export default class GooglePayButtonStrategy implements CheckoutButtonStrategy { ); } + if (offerChangeTriggers.includes(callbackTrigger)) { + await this._googlePayPaymentProcessor.handleCoupons(offerData); + } + if (this._buyNowInitializeOptions) { return this._getBuyNowTransactionInfo(availableShippingOptions); } diff --git a/packages/google-pay-integration/src/google-pay-customer-strategy.ts b/packages/google-pay-integration/src/google-pay-customer-strategy.ts index 1da178300f..64476d942a 100644 --- a/packages/google-pay-integration/src/google-pay-customer-strategy.ts +++ b/packages/google-pay-integration/src/google-pay-customer-strategy.ts @@ -112,11 +112,13 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy { callbackTrigger, shippingAddress, shippingOptionData, + offerData, }: IntermediatePaymentData): Promise => { const { availableTriggers, addressChangeTriggers, shippingOptionsChangeTriggers, + offerChangeTriggers, } = this._googlePayPaymentProcessor.getCallbackTriggers(); if (!availableTriggers.includes(callbackTrigger)) { @@ -135,6 +137,10 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy { ); } + const newOfferInfo = offerChangeTriggers.includes(callbackTrigger) + ? await this._googlePayPaymentProcessor.handleCoupons(offerData) + : undefined; + await this._paymentIntegrationService.loadCheckout(); const totalPrice = this._googlePayPaymentProcessor.getTotalPrice(); @@ -152,6 +158,9 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy { ...(availableShippingOptions && { newShippingOptionParameters: availableShippingOptions, }), + ...(newOfferInfo && { + newOfferInfo, + }), }; }, }, @@ -217,6 +226,7 @@ export default class GooglePayCustomerStrategy implements CustomerStrategy { this._googlePayPaymentProcessor.mapToBillingAddressRequestBody(response); const shippingAddress = this._googlePayPaymentProcessor.mapToShippingAddressRequestBody(response); + const siteLink = window.location.pathname === '/embedded-checkout' ? this._paymentIntegrationService.getState().getStoreConfigOrThrow().links.siteLink diff --git a/packages/google-pay-integration/src/google-pay-payment-processor.ts b/packages/google-pay-integration/src/google-pay-payment-processor.ts index 0fce414872..e5116b826e 100644 --- a/packages/google-pay-integration/src/google-pay-payment-processor.ts +++ b/packages/google-pay-integration/src/google-pay-payment-processor.ts @@ -26,7 +26,7 @@ import { GooglePayIsReadyToPayRequest, GooglePaymentsClient, GooglePayPaymentDataRequest, - GooglePayPaymentOptions, + GooglePayPaymentOptions, IntermediatePaymentData, ShippingOptionParameters, } from './types'; @@ -152,6 +152,10 @@ export default class GooglePayPaymentProcessor { await this._gateway.handleShippingOptionChange(optionId); } + async handleCoupons(offerData: IntermediatePaymentData['offerData']): Promise { + return await this._gateway.handleCoupons(offerData); + } + getTotalPrice(): string { return this._gateway.getTotalPrice(); } @@ -244,6 +248,7 @@ export default class GooglePayPaymentProcessor { merchantInfo: this._gateway.getMerchantInfo(), ...(await this._gateway.getRequiredData()), callbackIntents: this._gateway.getCallbackIntents(), + offerInfo: this._gateway.getAppliedCoupons(), }; this._isReadyToPayRequest = { ...this._baseRequest, diff --git a/packages/google-pay-integration/src/google-pay-payment-strategy.ts b/packages/google-pay-integration/src/google-pay-payment-strategy.ts index f5c4396898..6229a54891 100644 --- a/packages/google-pay-integration/src/google-pay-payment-strategy.ts +++ b/packages/google-pay-integration/src/google-pay-payment-strategy.ts @@ -191,11 +191,20 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy { paymentDataCallbacks: { onPaymentDataChanged: async ({ callbackTrigger, + offerData, }: IntermediatePaymentData): Promise => { if (callbackTrigger !== CallbackTriggerType.INITIALIZE) { return; } + const { + offerChangeTriggers, + } = this._googlePayPaymentProcessor.getCallbackTriggers(); + + if (offerChangeTriggers.includes(callbackTrigger)) { + await this._googlePayPaymentProcessor.handleCoupons(offerData); + } + await this._paymentIntegrationService.loadCheckout(); const { getCheckoutOrThrow, getCartOrThrow } = diff --git a/packages/google-pay-integration/src/types.ts b/packages/google-pay-integration/src/types.ts index c8bb1bc494..092e012fb2 100644 --- a/packages/google-pay-integration/src/types.ts +++ b/packages/google-pay-integration/src/types.ts @@ -155,6 +155,12 @@ export interface GooglePayPaymentDataRequest extends GooglePayGatewayBaseRequest allowedCountryCodes?: string[]; phoneNumberRequired?: boolean; }; + offerInfo: { + offers: { + redemptionCode: string, + description: string, + }[]; + }; shippingOptionRequired?: boolean; callbackIntents?: CallbackIntentsType[]; } @@ -176,6 +182,17 @@ export interface NewShippingOptionParameters { newShippingOptionParameters?: ShippingOptionParameters; } +export interface NewOfferData { + newOfferInfo: { + offers: NewOfferInfo[]; + } +} + +export interface NewOfferInfo { + redemptionCode: string, + description: string, +} + export interface GoogleShippingOption { id: string; label?: string; @@ -192,13 +209,16 @@ export interface IntermediatePaymentData { callbackTrigger: CallbackTriggerType; shippingAddress: GooglePayFullBillingAddress; shippingOptionData: GoogleShippingOption; + offerData: { + redemptionCodes: string[]; + }; } export interface GooglePayPaymentOptions { paymentDataCallbacks?: { onPaymentDataChanged( intermediatePaymentData: IntermediatePaymentData, - ): Promise<(NewTransactionInfo & NewShippingOptionParameters) | void>; + ): Promise<(NewTransactionInfo | NewShippingOptionParameters | NewOfferData) | void>; }; } diff --git a/packages/payment-integration-api/src/payment-integration-service.ts b/packages/payment-integration-api/src/payment-integration-service.ts index c28ad8fcfd..8042d5821e 100644 --- a/packages/payment-integration-api/src/payment-integration-service.ts +++ b/packages/payment-integration-api/src/payment-integration-service.ts @@ -77,6 +77,16 @@ export default interface PaymentIntegrationService { options?: RequestOptions, ): Promise; + applyCoupon( + coupon: string, + options?: RequestOptions, + ): Promise; + + removeCoupon( + couponId: string, + options?: RequestOptions, + ): Promise; + createBuyNowCart(body: BuyNowCartRequestBody, options?: RequestOptions): Promise; updatePaymentProviderCustomer( diff --git a/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts b/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts index 5bd6cad0fe..2760f49e3e 100644 --- a/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts +++ b/packages/payment-integrations-test-utils/src/test-utils/payment-integration-service.mock.ts @@ -59,6 +59,7 @@ const state = { getPaymentRedirectUrl: jest.fn(), getPaymentRedirectUrlOrThrow: jest.fn(), isPaymentDataRequired: jest.fn(), + applyCoupon: jest.fn(), }; const createBuyNowCart = jest.fn(() => Promise.resolve(getCart())); @@ -92,6 +93,8 @@ const initializePayment = jest.fn(); const validateCheckout = jest.fn(); const widgetInteraction = jest.fn(); const handle = jest.fn(); +const applyCoupon = jest.fn(); +const removeCoupon = jest.fn(); const PaymentIntegrationServiceMock = jest .fn() @@ -123,6 +126,8 @@ const PaymentIntegrationServiceMock = jest signOutCustomer, selectShippingOption, applyStoreCredit, + applyCoupon, + removeCoupon, verifyCheckoutSpamProtection, updatePaymentProviderCustomer, initializePayment,