🔀 Merge branch 'trunk'

# Conflicts:
#	modules/ppcp-applepay/resources/js/ApplepayButton.js
#	modules/ppcp-applepay/resources/js/boot-admin.js
#	modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js
#	modules/ppcp-googlepay/resources/js/GooglepayButton.js
#	modules/ppcp-googlepay/resources/js/boot-admin.js
This commit is contained in:
Philipp Stracker 2024-09-06 15:19:17 +02:00
commit a128228c86
No known key found for this signature in database
303 changed files with 21512 additions and 5378 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" width="92px" height="50px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="160 160 774 422" enable-background="new 0 0 1094 742" xml:space="preserve">
<path id="Base_1_" fill="#FFFFFF" d="M722.7,170h-352c-110,0-200,90-200,200l0,0c0,110,90,200,200,200h352c110,0,200-90,200-200l0,0
C922.7,260,832.7,170,722.7,170z"/>
<path id="Outline" fill="#3C4043" d="M722.7,186.2c24.7,0,48.7,4.9,71.3,14.5c21.9,9.3,41.5,22.6,58.5,39.5
c16.9,16.9,30.2,36.6,39.5,58.5c9.6,22.6,14.5,46.6,14.5,71.3s-4.9,48.7-14.5,71.3c-9.3,21.9-22.6,41.5-39.5,58.5
c-16.9,16.9-36.6,30.2-58.5,39.5c-22.6,9.6-46.6,14.5-71.3,14.5h-352c-24.7,0-48.7-4.9-71.3-14.5c-21.9-9.3-41.5-22.6-58.5-39.5
c-16.9-16.9-30.2-36.6-39.5-58.5c-9.6-22.6-14.5-46.6-14.5-71.3s4.9-48.7,14.5-71.3c9.3-21.9,22.6-41.5,39.5-58.5
c16.9-16.9,36.6-30.2,58.5-39.5c22.6-9.6,46.6-14.5,71.3-14.5L722.7,186.2 M722.7,170h-352c-110,0-200,90-200,200l0,0
c0,110,90,200,200,200h352c110,0,200-90,200-200l0,0C922.7,260,832.7,170,722.7,170L722.7,170z"/>
<g id="G_Pay_Lockup_1_">
<g id="Pay_Typeface_3_">
<path id="Letter_p_3_" fill="#3C4043" d="M529.3,384.2v60.5h-19.2V295.3H561c12.9,0,23.9,4.3,32.9,12.9
c9.2,8.6,13.8,19.1,13.8,31.5c0,12.7-4.6,23.2-13.8,31.7c-8.9,8.5-19.9,12.7-32.9,12.7h-31.7V384.2z M529.3,313.7v52.1h32.1
c7.6,0,14-2.6,19-7.7c5.1-5.1,7.7-11.3,7.7-18.3c0-6.9-2.6-13-7.7-18.1c-5-5.3-11.3-7.9-19-7.9h-32.1V313.7z"/>
<path id="Letter_a_3_" fill="#3C4043" d="M657.9,339.1c14.2,0,25.4,3.8,33.6,11.4c8.2,7.6,12.3,18,12.3,31.2v63h-18.3v-14.2h-0.8
c-7.9,11.7-18.5,17.5-31.7,17.5c-11.3,0-20.7-3.3-28.3-10s-11.4-15-11.4-25c0-10.6,4-19,12-25.2c8-6.3,18.7-9.4,32-9.4
c11.4,0,20.8,2.1,28.1,6.3v-4.4c0-6.7-2.6-12.3-7.9-17c-5.3-4.7-11.5-7-18.6-7c-10.7,0-19.2,4.5-25.4,13.6l-16.9-10.6
C625.9,345.8,639.7,339.1,657.9,339.1z M633.1,413.3c0,5,2.1,9.2,6.4,12.5c4.2,3.3,9.2,5,14.9,5c8.1,0,15.3-3,21.6-9
s9.5-13,9.5-21.1c-6-4.7-14.3-7.1-25-7.1c-7.8,0-14.3,1.9-19.5,5.6C635.7,403.1,633.1,407.8,633.1,413.3z"/>
<path id="Letter_y_3_" fill="#3C4043" d="M808.2,342.4l-64,147.2h-19.8l23.8-51.5L706,342.4h20.9l30.4,73.4h0.4l29.6-73.4H808.2z"
/>
</g>
<g id="G_Mark_1_">
<path id="Blue_500" fill="#4285F4" d="M452.93,372c0-6.26-0.56-12.25-1.6-18.01h-80.48v33L417.2,387
c-1.88,10.98-7.93,20.34-17.2,26.58v21.41h27.59C443.7,420.08,452.93,398.04,452.93,372z"/>
<path id="Green_500_1_" fill="#34A853" d="M400.01,413.58c-7.68,5.18-17.57,8.21-29.14,8.21c-22.35,0-41.31-15.06-48.1-35.36
h-28.46v22.08c14.1,27.98,43.08,47.18,76.56,47.18c23.14,0,42.58-7.61,56.73-20.71L400.01,413.58z"/>
<path id="Yellow_500_1_" fill="#FABB05" d="M320.09,370.05c0-5.7,0.95-11.21,2.68-16.39v-22.08h-28.46
c-5.83,11.57-9.11,24.63-9.11,38.47s3.29,26.9,9.11,38.47l28.46-22.08C321.04,381.26,320.09,375.75,320.09,370.05z"/>
<path id="Red_500" fill="#E94235" d="M370.87,318.3c12.63,0,23.94,4.35,32.87,12.85l24.45-24.43
c-14.85-13.83-34.21-22.32-57.32-22.32c-33.47,0-62.46,19.2-76.56,47.18l28.46,22.08C329.56,333.36,348.52,318.3,370.87,318.3z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -4,7 +4,7 @@
"description": "Googlepay module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
// Used in various places to mark fields for the preview button.
$apm_name = 'GooglePay';
@ -72,7 +72,7 @@ return array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/googlepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/googlepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Google Pay', 'woocommerce-paypal-payments' )
),
@ -117,7 +117,7 @@ return array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/googlepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/googlepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Google Pay', 'woocommerce-paypal-payments' )
),
@ -166,7 +166,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'default' => 'plain',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): GooglepayModule {
return new GooglepayModule();
};

View file

@ -1,3 +1,10 @@
/* Front end display */
.ppcp-button-apm .gpay-card-info-container-fill .gpay-card-info-container {
outline-offset: -1px;
border-radius: var(--apm-button-border-radius);
}
/* Admin preview */
.ppcp-button-googlepay {
min-height: 40px;
@ -18,3 +25,7 @@
min-width: 0 !important;
}
}
#ppc-button-ppcp-googlepay {
display: none;
}

View file

@ -4,7 +4,7 @@ import CheckoutActionHandler from '../../../../ppcp-button/resources/js/modules/
import FormValidator from '../../../../ppcp-button/resources/js/modules/Helper/FormValidator';
class CheckoutHandler extends BaseHandler {
transactionInfo() {
validateForm() {
return new Promise( async ( resolve, reject ) => {
try {
const spinner = new Spinner();
@ -23,7 +23,7 @@ class CheckoutHandler extends BaseHandler {
: null;
if ( ! formValidator ) {
resolve( super.transactionInfo() );
resolve();
return;
}
@ -42,7 +42,7 @@ class CheckoutHandler extends BaseHandler {
reject();
} else {
resolve( super.transactionInfo() );
resolve();
}
} );
} catch ( error ) {

View file

@ -0,0 +1,102 @@
import { GooglePayStorage } from '../Helper/GooglePayStorage';
import {
getWooCommerceCustomerDetails,
setPayerData,
} from '../../../../ppcp-button/resources/js/modules/Helper/PayerData';
const CHECKOUT_FORM_SELECTOR = 'form.woocommerce-checkout';
export class CheckoutBootstrap {
/**
* @type {GooglePayStorage}
*/
#storage;
/**
* @type {HTMLFormElement|null}
*/
#checkoutForm;
/**
* @param {GooglePayStorage} storage
*/
constructor( storage ) {
this.#storage = storage;
this.#checkoutForm = CheckoutBootstrap.getCheckoutForm();
}
/**
* Indicates if the current page contains a checkout form.
*
* @return {boolean} True if a checkout form is present.
*/
static isPageWithCheckoutForm() {
return null !== CheckoutBootstrap.getCheckoutForm();
}
/**
* Retrieves the WooCommerce checkout form element.
*
* @return {HTMLFormElement|null} The form, or null if not a checkout page.
*/
static getCheckoutForm() {
return document.querySelector( CHECKOUT_FORM_SELECTOR );
}
/**
* Returns the WooCommerce checkout form element.
*
* @return {HTMLFormElement|null} The form, or null if not a checkout page.
*/
get checkoutForm() {
return this.#checkoutForm;
}
/**
* Initializes the checkout process.
*
* @throws {Error} If called on a page without a checkout form.
*/
init() {
if ( ! this.#checkoutForm ) {
throw new Error(
'Checkout form not found. Cannot initialize CheckoutBootstrap.'
);
}
this.#populateCheckoutFields();
}
/**
* Populates checkout fields with stored or customer data.
*/
#populateCheckoutFields() {
const loggedInData = getWooCommerceCustomerDetails();
if ( loggedInData ) {
// If customer is logged in, we use the details from the customer profile.
return;
}
const billingData = this.#storage.getPayer();
if ( ! billingData ) {
return;
}
setPayerData( billingData, true );
this.checkoutForm.addEventListener(
'submit',
this.#onFormSubmit.bind( this )
);
}
/**
* Clean-up when checkout form is submitted.
*
* Immediately removes the payer details from the localStorage.
*/
#onFormSubmit() {
this.#storage.clearPayer();
}
}

View file

@ -1,242 +1,412 @@
import ContextHandlerFactory from './Context/ContextHandlerFactory';
import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import {
combineStyles,
combineWrapperIds,
} from '../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData';
import moduleStorage from './Helper/GooglePayStorage';
class GooglepayButton {
constructor( context, externalHandler, buttonConfig, ppcpConfig ) {
apmButtonsInit( ppcpConfig );
/**
* Plugin-specific styling.
*
* Note that most properties of this object do not apply to the Google Pay button.
*
* @typedef {Object} PPCPStyle
* @property {string} shape - Outline shape.
* @property {?number} height - Button height in pixel.
*/
this.isInitialized = false;
/**
* Style options that are defined by the Google Pay SDK and are required to render the button.
*
* @typedef {Object} GooglePayStyle
* @property {string} type - Defines the button label.
* @property {string} color - Button color
* @property {string} language - The locale; an empty string will apply the user-agent's language.
*/
this.context = context;
this.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
/**
* Google Pay JS SDK
*
* @see https://developers.google.com/pay/api/web/reference/request-objects
* @typedef {Object} GooglePaySDK
* @property {typeof PaymentsClient} PaymentsClient - Main API client for payment actions.
*/
this.paymentsClient = null;
/**
* The Payments Client class, generated by the Google Pay SDK.
*
* @see https://developers.google.com/pay/api/web/reference/client
* @typedef {Object} PaymentsClient
* @property {Function} createButton - The convenience method is used to
* generate a Google Pay payment button styled with the latest Google Pay branding for
* insertion into a webpage.
* @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest)
* method to determine a user's ability to return a form of payment from the Google Pay API.
* @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment
* sheet that allows selection of a payment method and optionally configured parameters
* @property {Function} onPaymentAuthorized - This method is called when a payment is
* authorized in the payment sheet.
* @property {Function} onPaymentDataChanged - This method handles payment data changes
* in the payment sheet such as shipping address and shipping options.
*/
this.contextHandler = ContextHandlerFactory.create(
this.context,
this.buttonConfig,
this.ppcpConfig,
this.externalHandler
/**
* This object describes the transaction details.
*
* @see https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo
* @typedef {Object} TransactionInfo
* @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code.
* @property {string} countryCode - Optional. required for EEA countries,
* @property {string} transactionId - Optional. A unique ID that identifies a facilitation
* attempt. Highly encouraged for troubleshooting.
* @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price
* used:
* @property {string} totalPrice - Required. Total monetary value of the transaction with an
* optional decimal precision of two decimal places.
* @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet
* (e.g. subtotals, sales taxes, shipping charges, discounts etc.).
* @property {string} totalPriceLabel - Optional. Custom label for the total price within the
* display items.
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the
* Google Pay payment sheet.
*/
function payerDataFromPaymentResponse( response ) {
const raw = response?.paymentMethodData?.info?.billingAddress;
return {
email_address: response?.email,
name: {
given_name: raw.name.split( ' ' )[ 0 ], // Assuming first name is the first part
surname: raw.name.split( ' ' ).slice( 1 ).join( ' ' ), // Assuming last name is the rest
},
address: {
country_code: raw.countryCode,
address_line_1: raw.address1,
address_line_2: raw.address2,
admin_area_1: raw.administrativeArea,
admin_area_2: raw.locality,
postal_code: raw.postalCode,
},
};
}
class GooglepayButton extends PaymentButton {
/**
* @inheritDoc
*/
static methodId = PaymentMethods.GOOGLEPAY;
/**
* @inheritDoc
*/
static cssClass = 'google-pay';
/**
* Client reference, provided by the Google Pay JS SDK.
*/
#paymentsClient = null;
/**
* Details about the processed transaction, provided to the Google SDK.
*
* @type {?TransactionInfo}
*/
#transactionInfo = null;
googlePayConfig = null;
/**
* @inheritDoc
*/
static getWrappers( buttonConfig, ppcpConfig ) {
return combineWrapperIds(
buttonConfig?.button?.wrapper || '',
buttonConfig?.button?.mini_cart_wrapper || '',
ppcpConfig?.button?.wrapper || '',
'ppc-button-googlepay-container',
'ppc-button-ppcp-googlepay'
);
}
/**
* @inheritDoc
*/
static getStyles( buttonConfig, ppcpConfig ) {
const styles = combineStyles(
ppcpConfig?.button || {},
buttonConfig?.button || {}
);
this.log = function () {
if ( this.buttonConfig.is_debug ) {
//console.log('[GooglePayButton]', ...arguments);
if ( 'buy' === styles.MiniCart.type ) {
styles.MiniCart.type = 'pay';
}
return styles;
}
constructor(
context,
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
) {
// Disable debug output in the browser console:
// buttonConfig.is_debug = false;
super(
context,
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
);
this.init = this.init.bind( this );
this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this );
this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this );
this.onButtonClick = this.onButtonClick.bind( this );
this.log( 'Create instance' );
}
/**
* @inheritDoc
*/
get requiresShipping() {
return super.requiresShipping && this.buttonConfig.shipping?.enabled;
}
/**
* The Google Pay API.
*
* @return {?GooglePaySDK} API for the Google Pay JS SDK, or null when SDK is not ready yet.
*/
get googlePayApi() {
return window.google?.payments?.api;
}
/**
* The Google Pay PaymentsClient instance created by this button.
* @see https://developers.google.com/pay/api/web/reference/client
*
* @return {?PaymentsClient} The SDK object, or null when SDK is not ready yet.
*/
get paymentsClient() {
return this.#paymentsClient;
}
/**
* Details about the processed transaction.
*
* This object defines the price that is charged, and text that is displayed inside the
* payment sheet.
*
* @return {?TransactionInfo} The TransactionInfo object.
*/
get transactionInfo() {
return this.#transactionInfo;
}
/**
* Assign the new transaction details to the payment button.
*
* @param {TransactionInfo} newTransactionInfo - Transaction details.
*/
set transactionInfo( newTransactionInfo ) {
this.#transactionInfo = newTransactionInfo;
this.refresh();
}
/**
* @inheritDoc
*/
validateConfiguration( silent = false ) {
const validEnvs = [ 'PRODUCTION', 'TEST' ];
const isInvalid = ( ...args ) => {
if ( ! silent ) {
this.error( ...args );
}
return false;
};
}
init( config ) {
if ( this.isInitialized ) {
return;
}
this.isInitialized = true;
if ( ! this.validateConfig() ) {
return;
if ( ! validEnvs.includes( this.buttonConfig.environment ) ) {
return isInvalid(
'Invalid environment:',
this.buttonConfig.environment
);
}
if ( ! this.contextHandler.validateContext() ) {
return;
// Preview buttons only need a valid environment.
if ( this.isPreview ) {
return true;
}
this.googlePayConfig = config;
this.allowedPaymentMethods = config.allowedPaymentMethods;
this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
this.initClient();
this.initEventHandlers();
this.paymentsClient
.isReadyToPay(
this.buildReadyToPayRequest(
this.allowedPaymentMethods,
config
)
)
.then( ( response ) => {
if ( response.result ) {
this.addButton( this.baseCardPaymentMethod );
}
} )
.catch( function ( err ) {
console.error( err );
} );
}
reinit() {
if ( ! this.googlePayConfig ) {
return;
return isInvalid(
'No API configuration - missing configure() call?'
);
}
this.isInitialized = false;
this.init( this.googlePayConfig );
}
validateConfig() {
if (
[ 'PRODUCTION', 'TEST' ].indexOf(
this.buttonConfig.environment
) === -1
) {
console.error(
'[GooglePayButton] Invalid environment.',
this.buttonConfig.environment
if ( ! this.transactionInfo ) {
return isInvalid(
'No transactionInfo - missing configure() call?'
);
return false;
}
if ( ! this.contextHandler ) {
console.error(
'[GooglePayButton] Invalid context handler.',
this.contextHandler
);
return false;
if ( ! typeof this.contextHandler?.validateContext() ) {
return isInvalid( 'Invalid context handler.', this.contextHandler );
}
return true;
}
/**
* Returns configurations relative to this button context.
* Configures the button instance. Must be called before the initial `init()`.
*
* @param {Object} apiConfig - API configuration.
* @param {Object} transactionInfo - Transaction details; required before "init" call.
*/
contextConfig() {
const config = {
wrapper: this.buttonConfig.button.wrapper,
ppcpStyle: this.ppcpConfig.button.style,
buttonStyle: this.buttonConfig.button.style,
ppcpButtonWrapper: this.ppcpConfig.button.wrapper,
};
configure( apiConfig, transactionInfo ) {
this.googlePayConfig = apiConfig;
this.#transactionInfo = transactionInfo;
if ( this.context === 'mini-cart' ) {
config.wrapper = this.buttonConfig.button.mini_cart_wrapper;
config.ppcpStyle = this.ppcpConfig.button.mini_cart_style;
config.buttonStyle = this.buttonConfig.button.mini_cart_style;
config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper;
// Handle incompatible types.
if ( config.buttonStyle.type === 'buy' ) {
config.buttonStyle.type = 'pay';
}
}
if (
[ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1
) {
config.ppcpButtonWrapper =
'#express-payment-method-ppcp-gateway-paypal';
}
return config;
this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods;
this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
}
initClient() {
const callbacks = {
onPaymentAuthorized: this.onPaymentAuthorized.bind( this ),
};
if (
this.buttonConfig.shipping.enabled &&
this.contextHandler.shippingAllowed()
) {
callbacks.onPaymentDataChanged =
this.onPaymentDataChanged.bind( this );
init() {
// Use `reinit()` to force a full refresh of an initialized button.
if ( this.isInitialized ) {
return;
}
this.paymentsClient = new google.payments.api.PaymentsClient( {
// Stop, if configuration is invalid.
if ( ! this.validateConfiguration() ) {
return;
}
super.init();
this.#paymentsClient = this.createPaymentsClient();
if ( ! this.isPresent ) {
this.log( 'Payment wrapper not found', this.wrapperId );
return;
}
if ( ! this.paymentsClient ) {
this.log( 'Could not initialize the payments client' );
return;
}
this.paymentsClient
.isReadyToPay(
this.buildReadyToPayRequest(
this.allowedPaymentMethods,
this.googlePayConfig
)
)
.then( ( response ) => {
this.log( 'PaymentsClient.isReadyToPay response:', response );
this.isEligible = !! response.result;
} )
.catch( ( err ) => {
this.error( err );
this.isEligible = false;
} );
}
reinit() {
// Missing (invalid) configuration indicates, that the first `init()` call did not happen yet.
if ( ! this.validateConfiguration( true ) ) {
return;
}
super.reinit();
this.init();
}
/**
* Provides an object with relevant paymentDataCallbacks for the current button instance.
*
* @return {Object} An object containing callbacks for the current scope & configuration.
*/
preparePaymentDataCallbacks() {
const callbacks = {};
// We do not attach any callbacks to preview buttons.
if ( this.isPreview ) {
return callbacks;
}
callbacks.onPaymentAuthorized = this.onPaymentAuthorized;
if ( this.requiresShipping ) {
callbacks.onPaymentDataChanged = this.onPaymentDataChanged;
}
return callbacks;
}
createPaymentsClient() {
if ( ! this.googlePayApi ) {
return null;
}
const callbacks = this.preparePaymentDataCallbacks();
/**
* Consider providing merchant info here:
*
* @see https://developers.google.com/pay/api/web/reference/request-objects#PaymentOptions
*/
return new this.googlePayApi.PaymentsClient( {
environment: this.buttonConfig.environment,
// add merchant info maybe
paymentDataCallbacks: callbacks,
} );
}
initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
if ( wrapper === ppcpButtonWrapper ) {
throw new Error(
`[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
);
}
const syncButtonVisibility = () => {
const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
setEnabled(
wrapper,
! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
);
};
jQuery( document ).on(
'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
( ev, data ) => {
if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
syncButtonVisibility();
}
}
);
syncButtonVisibility();
}
buildReadyToPayRequest( allowedPaymentMethods, baseRequest ) {
this.log( 'Ready To Pay request', baseRequest, allowedPaymentMethods );
return Object.assign( {}, baseRequest, {
allowedPaymentMethods,
} );
}
/**
* Add a Google Pay purchase button
* @param baseCardPaymentMethod
* Creates the payment button and calls `this.insertButton()` to make the button visible in the
* correct wrapper.
*/
addButton( baseCardPaymentMethod ) {
this.log( 'addButton', this.context );
addButton() {
if ( ! this.paymentsClient ) {
return;
}
const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig();
const baseCardPaymentMethod = this.baseCardPaymentMethod;
const { color, type, language } = this.style;
this.waitForWrapper( wrapper, () => {
const $wrapper = jQuery( wrapper );
$wrapper.removeClass( 'ppcp-button-rect ppcp-button-pill' );
$wrapper.addClass( 'ppcp-button-' + ppcpStyle.shape );
if ( ppcpStyle.height ) {
$wrapper.css( 'height', `${ ppcpStyle.height }px` );
}
const button = this.paymentsClient.createButton( {
onClick: this.onButtonClick.bind( this ),
allowedPaymentMethods: [ baseCardPaymentMethod ],
buttonColor: buttonStyle.color || 'black',
buttonType: buttonStyle.type || 'pay',
buttonLocale: buttonStyle.language || 'en',
buttonSizeMode: 'fill',
} );
$wrapper.append( button );
/**
* @see https://developers.google.com/pay/api/web/reference/client#createButton
*/
const button = this.paymentsClient.createButton( {
onClick: this.onButtonClick,
allowedPaymentMethods: [ baseCardPaymentMethod ],
buttonColor: color || 'black',
buttonType: type || 'pay',
buttonLocale: language || 'en',
buttonSizeMode: 'fill',
} );
}
waitForWrapper( selector, callback, delay = 100, timeout = 2000 ) {
const startTime = Date.now();
const interval = setInterval( () => {
const el = document.querySelector( selector );
const timeElapsed = Date.now() - startTime;
if ( el ) {
clearInterval( interval );
callback( el );
} else if ( timeElapsed > timeout ) {
clearInterval( interval );
}
}, delay );
this.insertButton( button );
}
//------------------------
@ -246,53 +416,78 @@ class GooglepayButton {
/**
* Show Google Pay payment sheet when Google Pay payment button is clicked
*/
async onButtonClick() {
this.log( 'onButtonClick', this.context );
onButtonClick() {
this.log( 'onButtonClick' );
const paymentDataRequest = await this.paymentDataRequest();
this.log(
'onButtonClick: paymentDataRequest',
paymentDataRequest,
this.context
);
const initiatePaymentRequest = () => {
window.ppcpFundingSource = 'googlepay';
const paymentDataRequest = this.paymentDataRequest();
window.ppcpFundingSource = 'googlepay'; // Do this on another place like on create order endpoint handler.
this.log(
'onButtonClick: paymentDataRequest',
paymentDataRequest,
this.context
);
this.paymentsClient.loadPaymentData( paymentDataRequest );
return this.paymentsClient.loadPaymentData( paymentDataRequest );
};
const validateForm = () => {
if ( 'function' !== typeof this.contextHandler.validateForm ) {
return Promise.resolve();
}
return this.contextHandler.validateForm().catch( ( error ) => {
this.error( 'Form validation failed:', error );
throw error;
} );
};
const getTransactionInfo = () => {
if ( 'function' !== typeof this.contextHandler.transactionInfo ) {
return Promise.resolve();
}
return this.contextHandler
.transactionInfo()
.then( ( transactionInfo ) => {
this.transactionInfo = transactionInfo;
} )
.catch( ( error ) => {
this.error( 'Failed to get transaction info:', error );
throw error;
} );
};
validateForm()
.then( getTransactionInfo )
.then( initiatePaymentRequest );
}
async paymentDataRequest() {
paymentDataRequest() {
const baseRequest = {
apiVersion: 2,
apiVersionMinor: 0,
};
const googlePayConfig = this.googlePayConfig;
const paymentDataRequest = Object.assign( {}, baseRequest );
paymentDataRequest.allowedPaymentMethods =
googlePayConfig.allowedPaymentMethods;
paymentDataRequest.transactionInfo =
await this.contextHandler.transactionInfo();
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
const useShippingCallback = this.requiresShipping;
const callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
if (
this.buttonConfig.shipping.enabled &&
this.contextHandler.shippingAllowed()
) {
paymentDataRequest.callbackIntents = [
'SHIPPING_ADDRESS',
'SHIPPING_OPTION',
'PAYMENT_AUTHORIZATION',
];
paymentDataRequest.shippingAddressRequired = true;
paymentDataRequest.shippingAddressParameters =
this.shippingAddressParameters();
paymentDataRequest.shippingOptionRequired = true;
} else {
paymentDataRequest.callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
if ( useShippingCallback ) {
callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' );
}
return paymentDataRequest;
return {
...baseRequest,
allowedPaymentMethods: this.googlePayConfig.allowedPaymentMethods,
transactionInfo: this.transactionInfo,
merchantInfo: this.googlePayConfig.merchantInfo,
callbackIntents,
emailRequired: true,
shippingAddressRequired: useShippingCallback,
shippingOptionRequired: useShippingCallback,
shippingAddressParameters: this.shippingAddressParameters(),
};
}
//------------------------
@ -307,47 +502,54 @@ class GooglepayButton {
}
onPaymentDataChanged( paymentData ) {
this.log( 'onPaymentDataChanged', this.context );
this.log( 'paymentData', paymentData );
this.log( 'onPaymentDataChanged', paymentData );
return new Promise( async ( resolve, reject ) => {
const paymentDataRequestUpdate = {};
try {
const paymentDataRequestUpdate = {};
const updatedData = await new UpdatePaymentData(
this.buttonConfig.ajax.update_payment_data
).update( paymentData );
const transactionInfo = await this.contextHandler.transactionInfo();
const updatedData = await new UpdatePaymentData(
this.buttonConfig.ajax.update_payment_data
).update( paymentData );
const transactionInfo = this.transactionInfo;
this.log( 'onPaymentDataChanged:updatedData', updatedData );
this.log( 'onPaymentDataChanged:transactionInfo', transactionInfo );
this.log( 'onPaymentDataChanged:updatedData', updatedData );
this.log(
'onPaymentDataChanged:transactionInfo',
transactionInfo
);
updatedData.country_code = transactionInfo.countryCode;
updatedData.currency_code = transactionInfo.currencyCode;
updatedData.total_str = transactionInfo.totalPrice;
updatedData.country_code = transactionInfo.countryCode;
updatedData.currency_code = transactionInfo.currencyCode;
updatedData.total_str = transactionInfo.totalPrice;
// Handle unserviceable address.
if ( ! updatedData.shipping_options?.shippingOptions?.length ) {
paymentDataRequestUpdate.error =
this.unserviceableShippingAddressError();
resolve( paymentDataRequestUpdate );
return;
}
switch ( paymentData.callbackTrigger ) {
case 'INITIALIZE':
case 'SHIPPING_ADDRESS':
paymentDataRequestUpdate.newShippingOptionParameters =
updatedData.shipping_options;
paymentDataRequestUpdate.newTransactionInfo =
this.calculateNewTransactionInfo( updatedData );
break;
case 'SHIPPING_OPTION':
paymentDataRequestUpdate.newTransactionInfo =
this.calculateNewTransactionInfo( updatedData );
break;
}
// Handle unserviceable address.
if ( ! updatedData.shipping_options?.shippingOptions?.length ) {
paymentDataRequestUpdate.error =
this.unserviceableShippingAddressError();
resolve( paymentDataRequestUpdate );
return;
} catch ( error ) {
this.error( 'Error during onPaymentDataChanged:', error );
reject( error );
}
switch ( paymentData.callbackTrigger ) {
case 'INITIALIZE':
case 'SHIPPING_ADDRESS':
paymentDataRequestUpdate.newShippingOptionParameters =
updatedData.shipping_options;
paymentDataRequestUpdate.newTransactionInfo =
this.calculateNewTransactionInfo( updatedData );
break;
case 'SHIPPING_OPTION':
paymentDataRequestUpdate.newTransactionInfo =
this.calculateNewTransactionInfo( updatedData );
break;
}
resolve( paymentDataRequestUpdate );
} );
}
@ -373,84 +575,111 @@ class GooglepayButton {
//------------------------
onPaymentAuthorized( paymentData ) {
this.log( 'onPaymentAuthorized', this.context );
this.log( 'onPaymentAuthorized', paymentData );
return this.processPayment( paymentData );
}
async processPayment( paymentData ) {
this.log( 'processPayment', this.context );
this.logGroup( 'processPayment' );
return new Promise( async ( resolve, reject ) => {
try {
const id = await this.contextHandler.createOrder();
const payer = payerDataFromPaymentResponse( paymentData );
this.log( 'processPayment: createOrder', id, this.context );
const paymentError = ( reason ) => {
this.error( reason );
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
.confirmOrder( {
orderId: id,
paymentMethodData: paymentData.paymentMethodData,
} );
return this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
reason
);
};
this.log(
'processPayment: confirmOrder',
confirmOrderResponse,
this.context
);
const checkPayPalApproval = async ( orderId ) => {
const confirmationData = {
orderId,
paymentMethodData: paymentData.paymentMethodData,
};
/** Capture the Order on the Server */
if ( confirmOrderResponse.status === 'APPROVED' ) {
let approveFailed = false;
await this.contextHandler.approveOrder(
{
orderID: id,
},
{
// actions mock object.
restart: () =>
new Promise( ( resolve, reject ) => {
approveFailed = true;
resolve();
} ),
order: {
get: () =>
new Promise( ( resolve, reject ) => {
resolve( null );
} ),
},
}
);
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
.confirmOrder( confirmationData );
if ( ! approveFailed ) {
resolve( this.processPaymentResponse( 'SUCCESS' ) );
} else {
resolve(
this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
'FAILED TO APPROVE'
)
);
}
} else {
resolve(
this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
'TRANSACTION FAILED'
)
);
this.log( 'confirmOrder', confirmOrderResponse );
return 'APPROVED' === confirmOrderResponse?.status;
};
/**
* This approval mainly confirms that the orderID is valid.
*
* It's still needed because this handler redirects to the checkout page if the server-side
* approval was successful.
*
* @param {string} orderID
*/
const approveOrderServerSide = async ( orderID ) => {
let isApproved = true;
this.log( 'approveOrder', orderID );
await this.contextHandler.approveOrder(
{ orderID, payer },
{
restart: () =>
new Promise( ( resolve ) => {
isApproved = false;
resolve();
} ),
order: {
get: () =>
new Promise( ( resolve ) => {
resolve( null );
} ),
},
}
} catch ( err ) {
resolve(
this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
err.message
)
);
);
return isApproved;
};
const processPaymentPromise = async ( resolve ) => {
const id = await this.contextHandler.createOrder();
this.log( 'createOrder', id );
const isApprovedByPayPal = await checkPayPalApproval( id );
if ( ! isApprovedByPayPal ) {
resolve( paymentError( 'TRANSACTION FAILED' ) );
return;
}
// This must be the last step in the process, as it initiates a redirect.
const success = await approveOrderServerSide( id );
if ( success ) {
resolve( this.processPaymentResponse( 'SUCCESS' ) );
} else {
resolve( paymentError( 'FAILED TO APPROVE' ) );
}
};
const addBillingDataToSession = () => {
moduleStorage.setPayer( payer );
setPayerData( payer );
};
return new Promise( async ( resolve ) => {
try {
addBillingDataToSession();
await processPaymentPromise( resolve );
} catch ( err ) {
resolve( paymentError( err.message ) );
}
this.logGroup();
} );
}
@ -466,7 +695,7 @@ class GooglepayButton {
};
}
this.log( 'processPaymentResponse', response, this.context );
this.log( 'processPaymentResponse', response );
return response;
}

View file

@ -1,39 +1,92 @@
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import GooglepayButton from './GooglepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
class GooglepayManager {
constructor( buttonConfig, ppcpConfig ) {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.googlePayConfig = null;
this.transactionInfo = null;
this.contextHandler = null;
this.buttons = [];
buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => {
const button = new GooglepayButton(
buttonModuleWatcher.watchContextBootstrap( async ( bootstrap ) => {
this.contextHandler = ContextHandlerFactory.create(
bootstrap.context,
buttonConfig,
ppcpConfig,
bootstrap.handler
);
const button = GooglepayButton.createButton(
bootstrap.context,
bootstrap.handler,
buttonConfig,
ppcpConfig
ppcpConfig,
this.contextHandler
);
this.buttons.push( button );
if ( this.googlePayConfig ) {
button.init( this.googlePayConfig );
const initButton = () => {
button.configure( this.googlePayConfig, this.transactionInfo );
button.init();
};
// Initialize button only if googlePayConfig and transactionInfo are already fetched.
if ( this.googlePayConfig && this.transactionInfo ) {
initButton();
} else {
await this.init();
if ( this.googlePayConfig && this.transactionInfo ) {
initButton();
}
}
} );
}
init() {
( async () => {
// Gets GooglePay configuration of the PayPal merchant.
this.googlePayConfig = await paypal.Googlepay().config();
for ( const button of this.buttons ) {
button.init( this.googlePayConfig );
async init() {
try {
if ( ! this.googlePayConfig ) {
// Gets GooglePay configuration of the PayPal merchant.
this.googlePayConfig = await paypal.Googlepay().config();
}
} )();
if ( ! this.transactionInfo ) {
this.transactionInfo = await this.fetchTransactionInfo();
}
if ( ! this.googlePayConfig ) {
console.error( 'No GooglePayConfig received during init' );
} else if ( ! this.transactionInfo ) {
console.error( 'No transactionInfo found during init' );
} else {
for ( const button of this.buttons ) {
button.configure(
this.googlePayConfig,
this.transactionInfo
);
button.init();
}
}
} catch ( error ) {
console.error( 'Error during initialization:', error );
}
}
async fetchTransactionInfo() {
try {
if ( ! this.contextHandler ) {
throw new Error( 'ContextHandler is not initialized' );
}
return await this.contextHandler.transactionInfo();
} catch ( error ) {
console.error( 'Error fetching transaction info:', error );
throw error;
}
}
reinit() {

View file

@ -0,0 +1,78 @@
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
import GooglepayButton from './GooglepayButton';
/**
* A single GooglePay preview button instance.
*/
export default class GooglePayPreviewButton extends PreviewButton {
/**
* Instance of the preview button.
*
* @type {?PaymentButton}
*/
#button = null;
constructor( args ) {
super( args );
this.selector = `${ args.selector }GooglePay`;
this.defaultAttributes = {
button: {
style: {
type: 'pay',
color: 'black',
language: 'en',
},
},
};
}
createNewWrapper() {
const element = super.createNewWrapper();
element.addClass( 'ppcp-button-googlepay' );
return element;
}
createButton( buttonConfig ) {
const contextHandler = ContextHandlerFactory.create(
'preview',
buttonConfig,
this.ppcpConfig,
null
);
if ( ! this.#button ) {
/* Intentionally using `new` keyword, instead of the `.createButton()` factory,
* as the factory is designed to only create a single button per context, while a single
* page can contain multiple instances of a preview button.
*/
this.#button = new GooglepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig,
contextHandler
);
}
this.#button.configure( this.apiConfig, null );
this.#button.applyButtonStyles( buttonConfig, this.ppcpConfig );
this.#button.reinit();
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
*
* @param {Object} buttonConfig
* @param {Object} ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button && buttonConfig.button ) {
Object.assign( buttonConfig.button.style, ppcpConfig.button.style );
}
}
}

View file

@ -0,0 +1,31 @@
import { LocalStorage } from '../../../../ppcp-button/resources/js/modules/Helper/LocalStorage';
export class GooglePayStorage extends LocalStorage {
static PAYER = 'payer';
static PAYER_TTL = 900; // 15 minutes in seconds
constructor() {
super( 'ppcp-googlepay' );
}
getPayer() {
return this.get( GooglePayStorage.PAYER );
}
setPayer( data ) {
/*
* The payer details are deleted on successful checkout, or after the TTL is reached.
* This helps to remove stale data from the browser, in case the customer chooses to
* use a different method to complete the purchase.
*/
this.set( GooglePayStorage.PAYER, data, GooglePayStorage.PAYER_TTL );
}
clearPayer() {
this.clear( GooglePayStorage.PAYER );
}
}
const moduleStorage = new GooglePayStorage();
export default moduleStorage;

View file

@ -1,28 +1,62 @@
/**
* Initialize the GooglePay module in the front end.
* In some cases, this module is loaded when the `window.PayPalCommerceGateway` object is not
* present. In that case, the page does not contain a Google Pay button, but some other logic
* that is related to Google Pay (e.g., the CheckoutBootstrap module)
*
* @file
*/
import { loadCustomScript } from '@paypal/paypal-js';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
import GooglepayManager from './GooglepayManager';
import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper';
import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap';
import moduleStorage from './Helper/GooglePayStorage';
( function ( { buttonConfig, ppcpConfig, jQuery } ) {
let manager;
( function ( { buttonConfig, ppcpConfig = {} } ) {
const context = ppcpConfig.context;
const bootstrap = function () {
manager = new GooglepayManager( buttonConfig, ppcpConfig );
manager.init();
};
setupButtonEvents( function () {
if ( manager ) {
manager.reinit();
function bootstrapPayButton() {
if ( ! buttonConfig || ! ppcpConfig ) {
return;
}
} );
const manager = new GooglepayManager( buttonConfig, ppcpConfig );
manager.init();
setupButtonEvents( function () {
manager.reinit();
} );
}
function bootstrapCheckout() {
if ( context && ! [ 'continuation', 'checkout' ].includes( context ) ) {
// Context must be missing/empty, or "continuation"/"checkout" to proceed.
return;
}
if ( ! CheckoutBootstrap.isPageWithCheckoutForm() ) {
return;
}
const checkoutBootstrap = new CheckoutBootstrap( moduleStorage );
checkoutBootstrap.init();
}
function bootstrap() {
bootstrapPayButton();
bootstrapCheckout();
}
document.addEventListener( 'DOMContentLoaded', () => {
if (
typeof buttonConfig === 'undefined' ||
typeof ppcpConfig === 'undefined'
) {
// No PayPal buttons present on this page.
if ( ! buttonConfig || ! ppcpConfig ) {
/*
* No PayPal buttons present on this page, but maybe a bootstrap module needs to be
* initialized. Skip loading the SDK or gateway configuration, and directly initialize
* the module.
*/
bootstrap();
return;
}
@ -52,5 +86,4 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
} )( {
buttonConfig: window.wc_ppcp_googlepay,
ppcpConfig: window.PayPalCommerceGateway,
jQuery: window.jQuery,
} );

View file

@ -25,23 +25,24 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// If GooglePay can be configured.
'googlepay.eligible' => static function ( ContainerInterface $container ): bool {
'googlepay.eligible' => static function ( ContainerInterface $container ): bool {
$apm_applies = $container->get( 'googlepay.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country_currency();
return $apm_applies->for_country() && $apm_applies->for_currency();
},
'googlepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
'googlepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
$container->get( 'googlepay.supported-country-currency-matrix' ),
$container->get( 'googlepay.supported-countries' ),
$container->get( 'googlepay.supported-currencies' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
// If GooglePay is configured and onboarded.
'googlepay.available' => static function ( ContainerInterface $container ): bool {
'googlepay.available' => static function ( ContainerInterface $container ): bool {
if ( apply_filters( 'woocommerce_paypal_payments_googlepay_validate_product_status', true ) ) {
$status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $status instanceof ApmProductStatus );
@ -54,14 +55,14 @@ return array(
},
// We assume it's a referral if we can check product status without API request failures.
'googlepay.is_referral' => static function ( ContainerInterface $container ): bool {
'googlepay.is_referral' => static function ( ContainerInterface $container ): bool {
$status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $status instanceof ApmProductStatus );
return ! $status->has_request_failure();
},
'googlepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
'googlepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
return new AvailabilityNotice(
$container->get( 'googlepay.helpers.apm-product-status' ),
$container->get( 'wcgateway.is-wc-gateways-list-page' ),
@ -69,7 +70,7 @@ return array(
);
},
'googlepay.helpers.apm-product-status' => SingletonDecorator::make(
'googlepay.helpers.apm-product-status' => SingletonDecorator::make(
static function( ContainerInterface $container ): ApmProductStatus {
return new ApmProductStatus(
$container->get( 'wcgateway.settings' ),
@ -81,772 +82,93 @@ return array(
),
/**
* The matrix which countries and currency combinations can be used for GooglePay.
* The list of which countries can be used for GooglePay.
*/
'googlepay.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
'googlepay.supported-countries' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for GooglePay.
* Returns which countries can be used for GooglePay.
*/
return apply_filters(
'woocommerce_paypal_payments_googlepay_supported_country_currency_matrix',
'woocommerce_paypal_payments_googlepay_supported_countries',
// phpcs:disable Squiz.Commenting.InlineComment
array(
'AU' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'AT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BG' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'CA' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'CY' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'CZ' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'DK' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'EE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'FI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'FR' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'DE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'GR' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'HU' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LV' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LU' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'MT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'NO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'NL' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'PL' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'PT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'RO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'SK' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'SI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'ES' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'SE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'GB' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'US' => array(
'AUD',
'CAD',
'EUR',
'GBP',
'JPY',
'USD',
),
'AU', // Australia
'AT', // Austria
'BE', // Belgium
'BG', // Bulgaria
'CA', // Canada
'CN', // China
'CY', // Cyprus
'CZ', // Czech Republic
'DK', // Denmark
'EE', // Estonia
'FI', // Finland
'FR', // France
'DE', // Germany
'GR', // Greece
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
'LV', // Latvia
'LI', // Liechtenstein
'LT', // Lithuania
'LU', // Luxembourg
'MT', // Malta
'NL', // Netherlands
'NO', // Norway
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SK', // Slovakia
'SI', // Slovenia
'ES', // Spain
'SE', // Sweden
'US', // United States
'GB', // United Kingdom
)
// phpcs:enable Squiz.Commenting.InlineComment
);
},
'googlepay.button' => static function ( ContainerInterface $container ): ButtonInterface {
/**
* The list of which currencies can be used for GooglePay.
*/
'googlepay.supported-currencies' => static function ( ContainerInterface $container ) : array {
/**
* Returns which currencies can be used for GooglePay.
*/
return apply_filters(
'woocommerce_paypal_payments_googlepay_supported_currencies',
// phpcs:disable Squiz.Commenting.InlineComment
array(
'AUD', // Australian Dollar
'BRL', // Brazilian Real
'CAD', // Canadian Dollar
'CHF', // Swiss Franc
'CZK', // Czech Koruna
'DKK', // Danish Krone
'EUR', // Euro
'GBP', // British Pound Sterling
'HKD', // Hong Kong Dollar
'HUF', // Hungarian Forint
'ILS', // Israeli New Shekel
'JPY', // Japanese Yen
'MXN', // Mexican Peso
'NOK', // Norwegian Krone
'NZD', // New Zealand Dollar
'PHP', // Philippine Peso
'PLN', // Polish Zloty
'SEK', // Swedish Krona
'SGD', // Singapore Dollar
'THB', // Thai Baht
'TWD', // New Taiwan Dollar
'USD', // United States Dollar
)
// phpcs:enable Squiz.Commenting.InlineComment
);
},
'googlepay.button' => static function ( ContainerInterface $container ): ButtonInterface {
return new Button(
$container->get( 'googlepay.url' ),
$container->get( 'googlepay.sdk_url' ),
@ -860,7 +182,7 @@ return array(
);
},
'googlepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
'googlepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
return new BlocksPaymentMethod(
'ppcp-googlepay',
$container->get( 'googlepay.url' ),
@ -870,7 +192,7 @@ return array(
);
},
'googlepay.url' => static function ( ContainerInterface $container ): string {
'googlepay.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
@ -881,26 +203,26 @@ return array(
);
},
'googlepay.sdk_url' => static function ( ContainerInterface $container ): string {
'googlepay.sdk_url' => static function ( ContainerInterface $container ): string {
return 'https://pay.google.com/gp/p/js/pay.js';
},
'googlepay.endpoint.update-payment-data' => static function ( ContainerInterface $container ): UpdatePaymentDataEndpoint {
'googlepay.endpoint.update-payment-data' => static function ( ContainerInterface $container ): UpdatePaymentDataEndpoint {
return new UpdatePaymentDataEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'googlepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
'googlepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY';
},
'googlepay.enable-url-live' => static function ( ContainerInterface $container ): string {
'googlepay.enable-url-live' => static function ( ContainerInterface $container ): string {
return 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY';
},
'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
@ -938,5 +260,15 @@ return array(
esc_html( $button_text )
);
},
'googlepay.wc-gateway' => static function ( ContainerInterface $container ): GooglePayGateway {
return new GooglePayGateway(
$container->get( 'wcgateway.order-processor' ),
$container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'session.handler' ),
$container->get( 'googlepay.url' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -13,7 +13,9 @@ use Exception;
use Psr\Log\LoggerInterface;
use WC_Countries;
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
@ -25,6 +27,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class Button implements ButtonInterface {
use ContextTrait;
/**
* The URL to the module.
*
@ -286,6 +290,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
$this->hide_gateway_until_eligible();
},
21
);
@ -299,6 +304,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
$this->hide_gateway_until_eligible();
},
21
);
@ -331,6 +337,23 @@ class Button implements ButtonInterface {
<?php
}
/**
* Outputs an inline CSS style that hides the Google Pay gateway (on Classic Checkout).
* The style is removed by `PaymentButton.js` once the eligibility of the payment method
* is confirmed.
*
* @return void
*/
protected function hide_gateway_until_eligible() : void {
?>
<style data-hide-gateway='<?php echo esc_attr( GooglePayGateway::ID ); ?>'>
.wc_payment_method.payment_method_ppcp-googlepay {
display: none;
}
</style>
<?php
}
/**
* Enqueues scripts/styles.
*/
@ -409,7 +432,7 @@ class Button implements ButtonInterface {
*/
public function script_data(): array {
$shipping = array(
'enabled' => $this->settings->has( 'googlepay_button_shipping_enabled' )
'enabled' => $this->settings->has( 'googlepay_button_shipping_enabled' )
? boolval( $this->settings->get( 'googlepay_button_shipping_enabled' ) )
: false,
'configured' => wc_shipping_enabled() && wc_get_shipping_method_count( false, true ) > 0,
@ -421,19 +444,23 @@ class Button implements ButtonInterface {
$is_enabled = $this->settings->has( 'googlepay_button_enabled' ) && $this->settings->get( 'googlepay_button_enabled' );
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$is_wc_gateway_enabled = isset( $available_gateways[ GooglePayGateway::ID ] );
return array(
'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION',
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'is_enabled' => $is_enabled,
'sdk_url' => $this->sdk_url,
'button' => array(
'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION',
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'is_enabled' => $is_enabled,
'is_wc_gateway_enabled' => $is_wc_gateway_enabled,
'sdk_url' => $this->sdk_url,
'button' => array(
'wrapper' => '#ppc-button-googlepay-container',
'style' => $this->button_styles_for_context( 'cart' ), // For now use cart. Pass the context if necessary.
'mini_cart_wrapper' => '#ppc-button-googlepay-container-minicart',
'mini_cart_style' => $this->button_styles_for_context( 'mini-cart' ),
),
'shipping' => $shipping,
'ajax' => array(
'shipping' => $shipping,
'ajax' => array(
'update_payment_data' => array(
'endpoint' => \WC_AJAX::get_endpoint( UpdatePaymentDataEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( UpdatePaymentDataEndpoint::nonce() ),

View file

@ -0,0 +1,254 @@
<?php
/**
* The Google Pay Payment Gateway
*
* @package WooCommerce\PayPalCommerce\Googlepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\Messages;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class GooglePayGateway
*/
class GooglePayGateway extends WC_Payment_Gateway {
use ProcessPaymentTrait;
const ID = 'ppcp-googlepay';
/**
* The processor for orders.
*
* @var OrderProcessor
*/
protected $order_processor;
/**
* The function return the PayPal checkout URL for the given order ID.
*
* @var callable(string):string
*/
private $paypal_checkout_url_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* The Session Handler.
*
* @var SessionHandler
*/
protected $session_handler;
/**
* The URL to the module.
*
* @var string
*/
private $module_url;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* GooglePayGateway constructor.
*
* @param OrderProcessor $order_processor The Order Processor.
* @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SessionHandler $session_handler The Session Handler.
* @param string $module_url The URL to the module.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
OrderProcessor $order_processor,
callable $paypal_checkout_url_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider,
SessionHandler $session_handler,
string $module_url,
LoggerInterface $logger
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'Google Pay (via PayPal) ', 'woocommerce-paypal-payments' );
$this->method_description = __( 'The separate payment gateway with the Google Pay button. If disabled, the button is included in the PayPal gateway.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'Google Pay', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->module_url = $module_url;
$this->icon = esc_url( $this->module_url ) . 'assets/images/googlepay.svg';
$this->init_form_fields();
$this->init_settings();
$this->order_processor = $order_processor;
$this->paypal_checkout_url_factory = $paypal_checkout_url_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
$this->session_handler = $session_handler;
$this->logger = $logger;
add_action(
'woocommerce_update_options_payment_gateways_' . $this->id,
array(
$this,
'process_admin_options',
)
);
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable Google Pay payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Process payment for a WooCommerce order.
*
* @param int $order_id The WooCommerce order id.
*
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return $this->handle_payment_failure(
null,
new GatewayGenericException( new Exception( 'WC order was not found.' ) )
);
}
/**
* If the WC_Order is paid through the approved webhook.
*/
//phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) {
return $this->handle_payment_success( $wc_order );
}
//phpcs:enable WordPress.Security.NonceVerification.Recommended
do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order );
try {
try {
$this->order_processor->process( $wc_order );
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );
return $this->handle_payment_success( $wc_order );
} catch ( PayPalOrderMissingException $exc ) {
$order = $this->order_processor->create_order( $wc_order );
return array(
'result' => 'success',
'redirect' => ( $this->paypal_checkout_url_factory )( $order->id() ),
);
}
} catch ( PayPalApiException $error ) {
return $this->handle_payment_failure(
$wc_order,
new Exception(
Messages::generic_payment_error_message() . ' ' . $error->getMessage(),
$error->getCode(),
$error
)
);
} catch ( Exception $error ) {
return $this->handle_payment_failure( $wc_order, $error );
}
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -10,35 +10,43 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class GooglepayModule
*/
class GooglepayModule implements ModuleInterface {
class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
// Clears product status when appropriate.
add_action(
@ -92,11 +100,21 @@ class GooglepayModule implements ModuleInterface {
static function () use ( $c, $button ) {
$smart_button = $c->get( 'button.smart-button' );
assert( $smart_button instanceof SmartButtonInterface );
if ( $smart_button->should_load_ppcp_script() ) {
$button->enqueue();
return;
}
/*
* Checkout page, but no PPCP scripts were loaded. Most likely in continuation mode.
* Need to enqueue some Google Pay scripts to populate the billing form with details
* provided by Google Pay.
*/
if ( is_checkout() ) {
$button->enqueue();
}
if ( has_block( 'woocommerce/checkout' ) || has_block( 'woocommerce/cart' ) ) {
/**
* Should add this to the ButtonInterface.
@ -159,13 +177,47 @@ class GooglepayModule implements ModuleInterface {
},
1
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
add_filter(
'woocommerce_payment_gateways',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
static function ( $methods ) use ( $c ): array {
if ( ! is_array( $methods ) ) {
return $methods;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( $settings->has( 'googlepay_button_enabled' ) && $settings->get( 'googlepay_button_enabled' ) ) {
$googlepay_gateway = $c->get( 'googlepay.wc-gateway' );
assert( $googlepay_gateway instanceof WC_Payment_Gateway );
$methods[] = $googlepay_gateway;
}
return $methods;
}
);
add_action(
'woocommerce_review_order_after_submit',
function () {
echo '<div id="ppc-button-' . esc_attr( GooglePayGateway::ID ) . '"></div>';
}
);
add_action(
'woocommerce_pay_order_after_submit',
function () {
echo '<div id="ppc-button-' . esc_attr( GooglePayGateway::ID ) . '"></div>';
}
);
return true;
}
}

View file

@ -16,11 +16,18 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for GooglePay.
* The list of which countries can be used for GooglePay.
*
* @var array
*/
private $allowed_country_currency_matrix;
private $allowed_countries;
/**
* The list of which currencies can be used for GooglePay.
*
* @var array
*/
private $allowed_currencies;
/**
* 3-letter currency code of the shop.
@ -39,30 +46,39 @@ class ApmApplies {
/**
* DccApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for GooglePay.
* @param array $allowed_countries The list of which countries can be used for GooglePay.
* @param array $allowed_currencies The list of which currencies can be used for GooglePay.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
array $allowed_country_currency_matrix,
array $allowed_countries,
array $allowed_currencies,
string $currency,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
$this->allowed_countries = $allowed_countries;
$this->allowed_currencies = $allowed_currencies;
$this->currency = $currency;
$this->country = $country;
}
/**
* Returns whether GooglePay can be used in the current country and the current currency used.
* Returns whether GooglePay can be used in the current country used.
*
* @return bool
*/
public function for_country_currency(): bool {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
public function for_country(): bool {
return in_array( $this->country, $this->allowed_countries, true );
}
/**
* Returns whether GooglePay can be used in the current currency used.
*
* @return bool
*/
public function for_currency(): bool {
return in_array( $this->currency, $this->allowed_currencies, true );
}
}