diff --git a/packages/google-pay-integration/src/google-pay-payment-initialize-options.ts b/packages/google-pay-integration/src/google-pay-payment-initialize-options.ts index 8b819380d5..5b32338ca2 100644 --- a/packages/google-pay-integration/src/google-pay-payment-initialize-options.ts +++ b/packages/google-pay-integration/src/google-pay-payment-initialize-options.ts @@ -1,3 +1,5 @@ +import { GooglePayButtonColor, GooglePayButtonType } from './types'; + /** * A set of options that are required to initialize the GooglePay payment method * @@ -36,6 +38,25 @@ * }, * }); * ``` + * + * Alternatively, a container-based Google Pay button can be rendered directly + * in the payment step (replacing the Place Order button): + * + * ```js + * service.initializePayment({ + * methodId: 'googlepaybraintree', + * googlepaybraintree: { + * container: 'checkout-payment-continue', + * onInit(renderButton) { + * // Hide Place Order, then render the button once container is in DOM + * renderButton(); + * }, + * onError(error) { + * console.log(error); + * }, + * }, + * }); + * ``` */ export default interface GooglePayPaymentInitializeOptions { /** @@ -50,6 +71,40 @@ export default interface GooglePayPaymentInitializeOptions { */ walletButton?: string; + /** + * The ID of the container element where the Google Pay button will be rendered. + * When provided, a branded Google Pay button is created inside this container. + * Clicking the button opens the Google Pay payment sheet and, on success, submits + * the order and redirects to the order confirmation page directly — no separate + * "Place Order" step is needed. + * + * Either `walletButton` or `container` must be supplied. + */ + container?: string; + + /** + * The color of the Google Pay button rendered into `container`. + * Defaults to `'default'`. + */ + buttonColor?: GooglePayButtonColor; + + /** + * The type/label of the Google Pay button rendered into `container`. + * Defaults to `'pay'`. + */ + buttonType?: GooglePayButtonType; + + /** + * Called after the Google Pay processor is fully initialized, with a + * `renderButton` function that — when invoked — creates the Google Pay + * button inside `container`. Use this callback to control timing: hide + * the Place Order button first, then call `renderButton()` once the + * container element is present in the DOM. + * + * Only relevant when `container` is provided. + */ + onInit?(renderButton: () => void): void; + /** * A callback that gets called when GooglePay fails to initialize or * selects a payment option. 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 9dc24d3c35..a4f3d402a5 100644 --- a/packages/google-pay-integration/src/google-pay-payment-strategy.ts +++ b/packages/google-pay-integration/src/google-pay-payment-strategy.ts @@ -42,6 +42,7 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy { private _clickListener?: (event: MouseEvent) => unknown; private _methodId?: keyof WithGooglePayPaymentInitializeOptions; private _isDeinitializationBlocked = false; + private _isContainerMode = false; constructor( protected _paymentIntegrationService: PaymentIntegrationService, @@ -55,6 +56,7 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy { async initialize( options?: PaymentInitializeOptions & WithGooglePayPaymentInitializeOptions, ): Promise { + console.log('GP initialize', options); if (!options?.methodId || !isGooglePayKey(options.methodId)) { throw new InvalidArgumentError( 'Unable to proceed because "methodId" is not a valid key.', @@ -65,11 +67,11 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy { const googlePayOptions = options[this._getMethodId()]; - if (!googlePayOptions?.walletButton) { + if (!googlePayOptions?.walletButton && !googlePayOptions?.container) { throw new InvalidArgumentError('Unable to proceed without valid options.'); } - const { walletButton, loadingContainerId, ...callbacks } = googlePayOptions; + const { walletButton, loadingContainerId, container, buttonColor, buttonType, onInit, ...callbacks } = googlePayOptions; this._loadingIndicatorContainer = loadingContainerId; @@ -87,7 +89,19 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy { this._getGooglePayClientOptions(paymentMethod.initializationData?.storeCountry), ); - this._addPaymentButton(walletButton, callbacks); + if (container) { + this._isContainerMode = true; + const renderButton = () => + this._addPaymentButtonToContainer(container, buttonColor, buttonType, callbacks.onError); + + if (onInit) { + onInit(renderButton); + } else { + renderButton(); + } + } else { + this._addPaymentButton(walletButton!, callbacks); + } } async execute({ payment }: OrderRequestBody): Promise { @@ -119,13 +133,16 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy { return Promise.resolve(); } - if (this._clickListener) { + if (this._isContainerMode) { + this._paymentButton?.remove(); + } else if (this._clickListener) { this._paymentButton?.removeEventListener('click', this._clickListener); } this._paymentButton = undefined; this._clickListener = undefined; this._methodId = undefined; + this._isContainerMode = false; return Promise.resolve(); } @@ -150,6 +167,55 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy { this._paymentButton.addEventListener('click', this._clickListener); } + protected _addPaymentButtonToContainer( + containerId: string, + buttonColor: GooglePayPaymentInitializeOptions['buttonColor'], + buttonType: GooglePayPaymentInitializeOptions['buttonType'], + onError: GooglePayPaymentInitializeOptions['onError'], + ): void { + this._paymentButton = this._googlePayPaymentProcessor.addPaymentButton(containerId, { + buttonColor: buttonColor ?? 'default', + buttonType: buttonType ?? 'pay', + onClick: this._handleContainerButtonClick(onError), + }); + } + + protected _handleContainerButtonClick( + onError: GooglePayPaymentInitializeOptions['onError'], + ): (event: MouseEvent) => Promise { + return async (event: MouseEvent) => { + event.preventDefault(); + + try { + this._googlePayPaymentProcessor.setShouldRequestShipping(false); + await this._googlePayPaymentProcessor.initializeWidget(); + await this._interactWithPaymentSheetAndPay(); + } catch (error) { + let err: unknown = error; + + this._toggleLoadingIndicator(false); + + if (isGooglePayErrorObject(error)) { + if (error.statusCode === 'CANCELED') { + throw new PaymentMethodCancelledError(); + } + + err = new PaymentMethodFailedError(JSON.stringify(error)); + } + + onError?.( + new PaymentMethodFailedError( + 'An error occurred while requesting your Google Pay payment details.', + ), + ); + + throw err; + } finally { + this._toggleBlockDeinitialization(false); + } + }; + } + protected _handleClick({ onPaymentSelect, onError,