Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { GooglePayButtonColor, GooglePayButtonType } from './types';

/**
* A set of options that are required to initialize the GooglePay payment method
*
Expand Down Expand Up @@ -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 {
/**
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -55,6 +56,7 @@ export default class GooglePayPaymentStrategy implements PaymentStrategy {
async initialize(
options?: PaymentInitializeOptions & WithGooglePayPaymentInitializeOptions,
): Promise<void> {
console.log('GP initialize', options);
if (!options?.methodId || !isGooglePayKey(options.methodId)) {
throw new InvalidArgumentError(
'Unable to proceed because "methodId" is not a valid key.',
Expand All @@ -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;

Expand All @@ -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<void> {
Expand Down Expand Up @@ -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();
}
Expand All @@ -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<void> {
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,
Expand Down