Merge pull request #1719 from woocommerce/PCP-2006-google-pay-settings-improvements

Google Pay Settings improvements (2006)
This commit is contained in:
Emili Castells 2023-10-17 14:24:57 +02:00 committed by GitHub
commit c07e8eaa87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1784 additions and 243 deletions

View file

@ -39,7 +39,7 @@ return function ( string $root_dir ): iterable {
if ( apply_filters(
//phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.googlepay_enabled',
getenv( 'PCP_GOOGLEPAY_ENABLED' ) === '1'
getenv( 'PCP_GOOGLEPAY_ENABLED' ) !== '0'
) ) {
$modules[] = ( require "$modules_dir/ppcp-googlepay/module.php" )();
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
@ -120,7 +121,8 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.factory.sellerstatus' ),
$container->get( 'api.partner_merchant_id' ),
$container->get( 'api.merchant_id' )
$container->get( 'api.merchant_id' ),
$container->get( 'api.helper.failure-registry' )
);
},
'api.factory.sellerstatus' => static function ( ContainerInterface $container ) : SellerStatusFactory {
@ -846,6 +848,10 @@ return array(
$purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
return new OrderTransient( $cache, $purchase_unit_sanitizer );
},
'api.helper.failure-registry' => static function( ContainerInterface $container ): FailureRegistry {
$cache = new Cache( 'ppcp-paypal-api-status-cache' );
return new FailureRegistry( $cache );
},
'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(
static function( ContainerInterface $container ): PurchaseUnitSanitizer {
$settings = $container->get( 'wcgateway.settings' );

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
@ -81,6 +82,18 @@ class ApiModule implements ModuleInterface {
10,
2
);
add_action(
'woocommerce_paypal_payments_clear_apm_product_status',
function () use ( $c ) {
$failure_registry = $c->has( 'api.helper.failure-registry' ) ? $c->get( 'api.helper.failure-registry' ) : null;
if ( $failure_registry instanceof FailureRegistry ) {
$failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY );
}
},
10,
2
);
}
/**

View file

@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
/**
* Class PartnersEndpoint
@ -65,6 +66,13 @@ class PartnersEndpoint {
*/
private $merchant_id;
/**
* The failure registry.
*
* @var FailureRegistry
*/
private $failure_registry;
/**
* PartnersEndpoint constructor.
*
@ -74,6 +82,7 @@ class PartnersEndpoint {
* @param SellerStatusFactory $seller_status_factory The seller status factory.
* @param string $partner_id The partner ID.
* @param string $merchant_id The merchant ID.
* @param FailureRegistry $failure_registry The API failure registry.
*/
public function __construct(
string $host,
@ -81,7 +90,8 @@ class PartnersEndpoint {
LoggerInterface $logger,
SellerStatusFactory $seller_status_factory,
string $partner_id,
string $merchant_id
string $merchant_id,
FailureRegistry $failure_registry
) {
$this->host = $host;
$this->bearer = $bearer;
@ -89,6 +99,7 @@ class PartnersEndpoint {
$this->seller_status_factory = $seller_status_factory;
$this->partner_id = $partner_id;
$this->merchant_id = $merchant_id;
$this->failure_registry = $failure_registry;
}
/**
@ -140,9 +151,15 @@ class PartnersEndpoint {
'response' => $response,
)
);
// Register the failure on api failure registry.
$this->failure_registry->add_failure( FailureRegistry::SELLER_STATUS_KEY );
throw $error;
}
$this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY );
$status = $this->seller_status_factory->from_paypal_reponse( $json );
return $status;
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Failure registry.
*
* This class is used to remember API failures.
* Mostly to prevent multiple failed API requests.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
/**
* Class FailureRegistry
*/
class FailureRegistry {
const CACHE_KEY = 'failure_registry';
const CACHE_TIMEOUT = 60 * 60 * 24; // DAY_IN_SECONDS, if necessary we can increase this.
const SELLER_STATUS_KEY = 'seller_status';
/**
* The Cache.
*
* @var Cache
*/
private $cache;
/**
* FailureRegistry constructor.
*
* @param Cache $cache The Cache.
*/
public function __construct( Cache $cache ) {
$this->cache = $cache;
}
/**
* Returns if there was a failure within a given timeframe.
*
* @param string $key The cache key.
* @param int $seconds The timeframe in seconds.
* @return bool
*/
public function has_failure_in_timeframe( string $key, int $seconds ): bool {
$cache_key = $this->cache_key( $key );
$failure_time = $this->cache->get( $cache_key );
if ( ! $failure_time ) {
return false;
}
$expiration = $failure_time + $seconds;
return $expiration > time();
}
/**
* Registers a failure.
*
* @param string $key The cache key.
* @return void
*/
public function add_failure( string $key ) {
$cache_key = $this->cache_key( $key );
$this->cache->set( $cache_key, time(), self::CACHE_TIMEOUT );
}
/**
* Clear a given failure.
*
* @param string $key The cache key.
* @return void
*/
public function clear_failures( string $key ) {
$cache_key = $this->cache_key( $key );
if ( $this->cache->has( $cache_key ) ) {
$this->cache->delete( $cache_key );
}
}
/**
* Build cache key.
*
* @param string $key The cache key.
* @return string
*/
private function cache_key( string $key ): string {
return implode( '_', array( self::CACHE_KEY, $key ) );
}
}

View file

@ -17,11 +17,11 @@ use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
/**
* Class OrderHelper
* Class OrderTransient
*/
class OrderTransient {
const CACHE_KEY = 'order_transient';
const CACHE_TIMEOUT = DAY_IN_SECONDS; // If necessary we can increase this.
const CACHE_TIMEOUT = 60 * 60 * 24; // DAY_IN_SECONDS, if necessary we can increase this.
/**
* The Cache.

View file

@ -13,10 +13,20 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
// Eligibility check.
if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) {
return $fields;
}
$is_available = $container->get( 'applepay.enabled' );
$is_referral = $container->get( 'applepay.is_referral' );
$insert_after = function ( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
$index = array_search( $key, $keys, true );
@ -27,9 +37,27 @@ return array(
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) {
// Connection tab fields.
$fields = $insert_after(
$fields,
'ppcp_dcc_status',
array(
'applepay_status' => array(
'title' => __( 'Apple Pay Payments', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => $container->get( 'applepay.settings.connection.status-text' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
),
)
);
if ( ! $is_available && $is_referral ) {
$connection_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-connection#field-credentials_feature_onboarding_heading' );
$connection_link = '<a href="' . $connection_url . '" target="_blank">';
$connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">';
return $insert_after(
$fields,
'allow_card_button_gateway',
@ -57,7 +85,7 @@ return array(
array(
$display_manager
->rule()
->condition_element( 'applepay_button_enabled', '1' )
->condition_is_true( false )
->action_enable( 'applepay_button_enabled' )
->to_array(),
)

View file

@ -31,10 +31,16 @@ class ApplepayButton {
this.updated_contact_info = []
this.selectedShippingMethod = []
this.nonce = document.getElementById('woocommerce-process-checkout-nonce').value
this.log = function() {
if ( this.buttonConfig.is_debug ) {
console.log('[ApplePayButton]', ...arguments);
}
}
}
init(config) {
console.log('[ApplePayButton] init', config);
this.log('init', config);
if (this.isInitialized) {
return;
}
@ -53,12 +59,12 @@ class ApplepayButton {
const id = "#apple-" + this.buttonConfig.button.wrapper;
if (this.context === 'mini-cart') {
document.querySelector(id_minicart).addEventListener('click', (evt) => {
document.querySelector(id_minicart)?.addEventListener('click', (evt) => {
evt.preventDefault();
this.onButtonClick();
});
} else {
document.querySelector(id).addEventListener('click', (evt) => {
document.querySelector(id)?.addEventListener('click', (evt) => {
evt.preventDefault();
this.onButtonClick();
});
@ -150,7 +156,10 @@ class ApplepayButton {
const language = this.buttonConfig.button.lang;
const color = this.buttonConfig.button.color;
const id = "apple-" + wrapper;
if (appleContainer) {
appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`;
}
jQuery('#' + wrapper).addClass('ppcp-button-' + shape);
jQuery(wrapper).append(appleContainer);
@ -180,7 +189,7 @@ class ApplepayButton {
console.error(error);
}
const session = this.applePaySession(paymentDataRequest)
console.log("session", session)
this.log("session", session)
const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ?
new FormValidator(
PayPalCommerceGateway.ajax.validate_checkout.endpoint,
@ -242,7 +251,7 @@ class ApplepayButton {
//------------------------
onvalidatemerchant(session) {
console.log("onvalidatemerchant")
this.log("onvalidatemerchant")
return (applePayValidateMerchantEvent) => {
paypal.Applepay().validateMerchant({
validationUrl: applePayValidateMerchantEvent.validationURL
@ -259,7 +268,7 @@ class ApplepayButton {
'woocommerce-process-checkout-nonce': this.nonce,
}
})
console.log('validated')
this.log('validated')
})
.catch(validateError => {
console.error(validateError);
@ -279,7 +288,7 @@ class ApplepayButton {
}
onshippingmethodselected(session) {
const ajax_url = this.buttonConfig.ajax_url
console.log('[ApplePayButton] onshippingmethodselected');
this.log('onshippingmethodselected');
return (event) => {
const data = this.getShippingMethodData(event);
jQuery.ajax({
@ -291,7 +300,7 @@ class ApplepayButton {
if (applePayShippingMethodUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
}
console.log('shipping method update response', response, applePayShippingMethodUpdate)
this.log('shipping method update response', response, applePayShippingMethodUpdate)
this.selectedShippingMethod = event.shippingMethod
//order the response shipping methods, so that the selected shipping method is the first one
let orderedShippingMethods = response.newShippingMethods.sort((a, b) => {
@ -316,10 +325,10 @@ class ApplepayButton {
}
onshippingcontactselected(session) {
const ajax_url = this.buttonConfig.ajax_url
console.log('[ApplePayButton] onshippingcontactselected', ajax_url, session)
this.log('[ApplePayButton] onshippingcontactselected', ajax_url, session)
return (event) => {
const data = this.getShippingContactData(event);
console.log('shipping contact selected', data, event)
this.log('shipping contact selected', data, event)
jQuery.ajax({
url: ajax_url,
method: 'POST',
@ -327,7 +336,7 @@ class ApplepayButton {
success: (applePayShippingContactUpdate, textStatus, jqXHR) => {
let response = applePayShippingContactUpdate.data
this.updated_contact_info = event.shippingContact
console.log('shipping contact update response', response, applePayShippingContactUpdate, this.updated_contact_info)
this.log('shipping contact update response', response, applePayShippingContactUpdate, this.updated_contact_info)
if (applePayShippingContactUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
}
@ -421,7 +430,7 @@ class ApplepayButton {
let createOrderInPayPal = actionHandler.createOrder()
const processInWooAndCapture = async (data) => {
try {
console.log('processInWooAndCapture', data)
this.log('processInWooAndCapture', data)
const billingContact = data.billing_contact
const shippingContact = data.shipping_contact
jQuery.ajax({
@ -443,7 +452,7 @@ class ApplepayButton {
complete: (jqXHR, textStatus) => {
},
success: (authorizationResult, textStatus, jqXHR) => {
console.log('success authorizationResult', authorizationResult)
this.log('success authorizationResult', authorizationResult)
if (authorizationResult.result === "success") {
redirectionUrl = authorizationResult.redirect;
//session.completePayment(ApplePaySession.STATUS_SUCCESS)
@ -453,18 +462,18 @@ class ApplepayButton {
}
},
error: (jqXHR, textStatus, errorThrown) => {
console.log('error authorizationResult', errorThrown)
this.log('error authorizationResult', errorThrown)
session.completePayment(ApplePaySession.STATUS_FAILURE)
console.warn(textStatus, errorThrown)
session.abort()
},
})
} catch (error) {
console.log(error) // handle error
this.log(error) // handle error
}
}
createOrderInPayPal([], []).then((orderId) => {
console.log('createOrderInPayPal', orderId)
this.log('createOrderInPayPal', orderId)
paypal.Applepay().confirmOrder(
{
orderId: orderId,
@ -488,31 +497,31 @@ class ApplepayButton {
}
);
}).catch((error) => {
console.log(error)
console.error(error)
session.abort()
})
};*/
}
/* onPaymentAuthorized(paymentData) {
console.log('[ApplePayButton] onPaymentAuthorized', this.context);
this.log('[ApplePayButton] onPaymentAuthorized', this.context);
return this.processPayment(paymentData);
}
async processPayment(paymentData) {
console.log('[ApplePayButton] processPayment', this.context);
this.log('[ApplePayButton] processPayment', this.context);
return new Promise(async (resolve, reject) => {
try {
let id = await this.contextHandler.createOrder();
console.log('[ApplePayButton] processPayment: createOrder', id, this.context);
this.log('[ApplePayButton] processPayment: createOrder', id, this.context);
const confirmOrderResponse = await paypal.Applepay().confirmOrder({
orderId: id,
paymentMethodData: paymentData.paymentMethodData
});
console.log('[ApplePayButton] processPayment: confirmOrder', confirmOrderResponse, this.context);
this.log('[ApplePayButton] processPayment: confirmOrder', confirmOrderResponse, this.context);
/!** Capture the Order on the Server *!/
if (confirmOrderResponse.status === "APPROVED") {
@ -554,7 +563,7 @@ class ApplepayButton {
}
}
console.log('[ApplePayButton] processPaymentResponse', response, this.context);
this.log('processPaymentResponse', response, this.context);
return response;
}*/

View file

@ -8,7 +8,7 @@ class ApplepayManager {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.ApplePayConfig = null;
console.log('Applepay manager', ppcpConfig, buttonConfig)
//console.log('Applepay manager', ppcpConfig, buttonConfig)
this.buttons = [];
buttonModuleWatcher.watchContextBootstrap((bootstrap) => {

View file

@ -14,13 +14,13 @@ if (typeof window.PayPalCommerceGateway === 'undefined') {
window.PayPalCommerceGateway = ppcpConfig;
}
console.log('ppcpData', ppcpData);
console.log('ppcpConfig', ppcpConfig);
console.log('buttonData', buttonData);
console.log('buttonConfig', buttonConfig);
//console.log('ppcpData', ppcpData);
//console.log('ppcpConfig', ppcpConfig);
//console.log('buttonData', buttonData);
//console.log('buttonConfig', buttonConfig);
const ApplePayComponent = () => {
console.log('ApplePayComponent render');
//console.log('ApplePayComponent render');
const [bootstrapped, setBootstrapped] = useState(false);
const [paypalLoaded, setPaypalLoaded] = useState(false);

View file

@ -25,7 +25,7 @@ import ApplepayManager from "./ApplepayManager";
}
const isMiniCart = ppcpConfig.mini_cart_buttons_enabled;
const isButton = jQuery('#' + buttonConfig.button.wrapper).length > 0;
console.log('isbutton' ,isButton, buttonConfig.button.wrapper)
//console.log('isbutton' ,isButton, buttonConfig.button.wrapper)
// If button wrapper is not present then there is no need to load the scripts.
// minicart loads later?
if (!isMiniCart && !isButton) {

View file

@ -16,6 +16,9 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
use WooCommerce\PayPalCommerce\Applepay\Assets\DataToAppleButtonScripts;
use WooCommerce\PayPalCommerce\Applepay\Assets\BlocksPaymentMethod;
use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -36,12 +39,21 @@ return array(
'applepay.status-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-apple-status-cache' );
},
// We assume it's a referral if we can check product status without API request failures.
'applepay.is_referral' => static function ( ContainerInterface $container ): bool {
$status = $container->get( 'applepay.apple-product-status' );
assert( $status instanceof AppleProductStatus );
return ! $status->has_request_failure();
},
'applepay.apple-product-status' => static function( ContainerInterface $container ): AppleProductStatus {
return new AppleProductStatus(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
$container->get( 'applepay.status-cache' ),
$container->get( 'onboarding.state' )
$container->get( 'onboarding.state' ),
$container->get( 'api.helper.failure-registry' )
);
},
'applepay.enabled' => static function ( ContainerInterface $container ): bool {
@ -116,4 +128,52 @@ return array(
)
);
},
'applepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY';
},
'applepay.enable-url-live' => static function ( ContainerInterface $container ): string {
return 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY';
},
'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
}
$product_status = $container->get( 'applepay.apple-product-status' );
assert( $product_status instanceof AppleProductStatus );
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
$enabled = $product_status->apple_is_active();
$enabled_status_text = esc_html__( 'Status: Available', 'woocommerce-paypal-payments' );
$disabled_status_text = esc_html__( 'Status: Not yet enabled', 'woocommerce-paypal-payments' );
$button_text = $enabled
? esc_html__( 'Settings', 'woocommerce-paypal-payments' )
: esc_html__( 'Enable Apple Pay', 'woocommerce-paypal-payments' );
$enable_url = $environment->current_environment_is( Environment::PRODUCTION )
? $container->get( 'applepay.enable-url-live' )
: $container->get( 'applepay.enable-url-sandbox' );
$button_url = $enabled
? admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway#field-alternative_payment_methods' )
: $enable_url;
return sprintf(
'<p>%1$s %2$s</p><p><a target="%3$s" href="%4$s" class="button">%5$s</a></p>',
$enabled ? $enabled_status_text : $disabled_status_text,
$enabled ? '<span class="dashicons dashicons-yes"></span>' : '<span class="dashicons dashicons-no"></span>',
$enabled ? '_self' : '_blank',
esc_url( $button_url ),
esc_html( $button_text )
);
},
);

View file

@ -147,6 +147,19 @@ class ApplePayButton implements ButtonInterface {
*/
public function initialize(): void {
add_filter( 'ppcp_onboarding_options', array( $this, 'add_apple_onboarding_option' ), 10, 1 );
add_filter(
'ppcp_partner_referrals_option',
function ( array $option ): array {
if ( $option['valid'] ) {
return $option;
}
if ( $option['field'] === 'ppcp-onboarding-apple' ) {
$option['valid'] = true;
$option['value'] = ( $option['value'] ? '1' : '' );
}
return $option;
}
);
add_filter(
'ppcp_partner_referrals_data',
function ( array $data ): array {
@ -165,7 +178,6 @@ class ApplePayButton implements ButtonInterface {
$data['products'][0] = 'PAYMENT_METHODS';
}
$data['capabilities'][] = 'APPLE_PAY';
$nonce = $data['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['seller_nonce'];
$data['operations'][] = array(
'operation' => 'API_INTEGRATION',
'api_integration_preference' => array(
@ -177,7 +189,6 @@ class ApplePayButton implements ButtonInterface {
'PAYMENT',
'REFUND',
),
'seller_nonce' => $nonce,
),
),
),
@ -196,6 +207,10 @@ class ApplePayButton implements ButtonInterface {
* @return string
*/
public function add_apple_onboarding_option( $options ): string {
if ( ! apply_filters( 'woocommerce_paypal_payments_apple_pay_onboarding_option', false ) ) {
return $options;
}
$checked = '';
try {
$onboard_with_apple = $this->settings->get( 'ppcp-onboarding-apple' );
@ -206,7 +221,7 @@ class ApplePayButton implements ButtonInterface {
$checked = '';
}
return $options . '<li><label><input type="checkbox" id="ppcp-onboarding-apple" ' . $checked . '> ' .
return $options . '<li><label><input type="checkbox" id="ppcp-onboarding-apple" ' . $checked . ' data-onboarding-option="ppcp-onboarding-apple"> ' .
__( 'Onboard with ApplePay', 'woocommerce-paypal-payments' ) . '
</label></li>';

View file

@ -11,8 +11,8 @@ namespace WooCommerce\PayPalCommerce\Applepay\Assets;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -36,6 +36,14 @@ class AppleProductStatus {
* @var bool|null
*/
private $current_status_cache;
/**
* If there was a request failure.
*
* @var bool
*/
private $has_request_failure = false;
/**
* The settings.
*
@ -57,6 +65,13 @@ class AppleProductStatus {
*/
private $onboarding_state;
/**
* The API failure registry
*
* @var FailureRegistry
*/
private $api_failure_registry;
/**
* PayUponInvoiceProductStatus constructor.
*
@ -64,17 +79,20 @@ class AppleProductStatus {
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param Cache $cache The cache.
* @param State $onboarding_state The onboarding state.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
Settings $settings,
PartnersEndpoint $partners_endpoint,
Cache $cache,
State $onboarding_state
State $onboarding_state,
FailureRegistry $api_failure_registry
) {
$this->settings = $settings;
$this->partners_endpoint = $partners_endpoint;
$this->cache = $cache;
$this->onboarding_state = $onboarding_state;
$this->api_failure_registry = $api_failure_registry;
}
/**
@ -99,9 +117,17 @@ class AppleProductStatus {
return true;
}
// Check API failure registry to prevent multiple failed API requests.
if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) {
$this->has_request_failure = true;
$this->current_status_cache = false;
return $this->current_status_cache;
}
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( Throwable $error ) {
$this->has_request_failure = true;
$this->current_status_cache = false;
return false;
}
@ -124,4 +150,14 @@ class AppleProductStatus {
$this->current_status_cache = false;
return false;
}
/**
* Returns if there was a request failure.
*
* @return bool
*/
public function has_request_failure(): bool {
return $this->has_request_failure;
}
}

View file

@ -127,6 +127,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',
@ -180,6 +181,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',

View file

@ -27,6 +27,7 @@ class WidgetBuilder {
setPaypal(paypal) {
this.paypal = paypal;
jQuery(document).trigger('ppcp-paypal-loaded', paypal);
}
registerButtons(wrapper, options) {
@ -177,4 +178,5 @@ class WidgetBuilder {
}
}
export default new WidgetBuilder();
window.widgetBuilder = window.widgetBuilder || new WidgetBuilder();
export default window.widgetBuilder;

View file

@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Product;
use WC_Product_Variation;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
@ -1038,6 +1039,10 @@ class SmartButton implements SmartButtonInterface {
'funding_sources_without_redirect' => $this->funding_sources_without_redirect,
);
if ( 'pay-now' === $this->context() ) {
$localize['pay_now'] = $this->pay_now_script_data();
}
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
$localize['button']['mini_cart_style']['tagline'] = false;
}
@ -1058,6 +1063,32 @@ class SmartButton implements SmartButtonInterface {
return $localize;
}
/**
* Returns pay-now payment data.
*
* @return array
*/
private function pay_now_script_data(): array {
$order_id = $this->get_order_pay_id();
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'] ?? '';
$currency_code = get_woocommerce_currency();
$wc_order = wc_get_order( $order_id );
if ( ! $wc_order instanceof WC_Order ) {
return array();
}
$total = (float) $wc_order->get_total( 'numeric' );
return array(
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
);
}
/**
* If we can find the payer data for a current customer, we will return it.
*

View file

@ -76,7 +76,7 @@ class CartScriptParamsEndpoint implements EndpointInterface {
// Shop settings.
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'];
$shop_country_code = $base_location['country'] ?? '';
$currency_code = get_woocommerce_currency();
wp_send_json_success(

View file

@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\Googlepay\Helper\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
@ -24,6 +25,9 @@ return array(
return $fields;
}
$is_available = $container->get( 'googlepay.available' );
$is_referral = $container->get( 'googlepay.is_referral' );
$insert_after = function( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
$index = array_search( $key, $keys, true );
@ -35,6 +39,66 @@ return array(
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
// Connection tab fields.
$fields = $insert_after(
$fields,
'ppcp_dcc_status',
array(
'googlepay_status' => array(
'title' => __( 'Google Pay Payments', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => $container->get( 'googlepay.settings.connection.status-text' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
),
)
);
if ( ! $is_available && $is_referral ) {
$connection_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-connection#field-credentials_feature_onboarding_heading' );
$connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">';
return $insert_after(
$fields,
'allow_card_button_gateway',
array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => array( 'ppcp-disabled-checkbox' ),
'label' => __( 'Enable Google Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">'
. sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Your PayPal account %1$srequires additional permissions%2$s to enable Google Pay.', 'woocommerce-paypal-payments' ),
$connection_link,
'</a>'
)
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
array(
$display_manager
->rule()
->condition_is_true( false )
->action_enable( 'googlepay_button_enabled' )
->to_array(),
)
),
),
),
)
);
}
// Standard Payments tab fields.
return $insert_after(
$fields,
'allow_card_button_gateway',

View file

@ -10,6 +10,10 @@ class BaseHandler {
this.externalHandler = externalHandler;
}
shippingAllowed() {
return true;
}
transactionInfo() {
return new Promise((resolve, reject) => {

View file

@ -2,6 +2,10 @@ import BaseHandler from "./BaseHandler";
class CheckoutBlockHandler extends BaseHandler{
shippingAllowed() {
return false;
}
createOrder() {
return this.externalHandler.createOrder();
}

View file

@ -6,6 +6,10 @@ import FormValidator from "../../../../ppcp-button/resources/js/modules/Helper/F
class CheckoutHandler extends BaseHandler {
shippingAllowed() {
return false;
}
transactionInfo() {
return new Promise(async (resolve, reject) => {

View file

@ -4,6 +4,8 @@ import CheckoutHandler from "./CheckoutHandler";
import CartBlockHandler from "./CartBlockHandler";
import CheckoutBlockHandler from "./CheckoutBlockHandler";
import MiniCartHandler from "./MiniCartHandler";
import PayNowHandler from "./PayNowHandler";
import PreviewHandler from "./PreviewHandler";
class ContextHandlerFactory {
@ -14,14 +16,17 @@ class ContextHandlerFactory {
case 'cart':
return new CartHandler(buttonConfig, ppcpConfig, externalActionHandler);
case 'checkout':
case 'pay-now': // same as checkout
return new CheckoutHandler(buttonConfig, ppcpConfig, externalActionHandler);
case 'pay-now':
return new PayNowHandler(buttonConfig, ppcpConfig, externalActionHandler);
case 'mini-cart':
return new MiniCartHandler(buttonConfig, ppcpConfig, externalActionHandler);
case 'cart-block':
return new CartBlockHandler(buttonConfig, ppcpConfig, externalActionHandler);
case 'checkout-block':
return new CheckoutBlockHandler(buttonConfig, ppcpConfig, externalActionHandler);
case 'preview':
return new PreviewHandler(buttonConfig, ppcpConfig, externalActionHandler);
}
}
}

View file

@ -0,0 +1,35 @@
import Spinner from "../../../../ppcp-button/resources/js/modules/Helper/Spinner";
import BaseHandler from "./BaseHandler";
import CheckoutActionHandler
from "../../../../ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler";
class PayNowHandler extends BaseHandler {
shippingAllowed() {
return false;
}
transactionInfo() {
return new Promise(async (resolve, reject) => {
const data = this.ppcpConfig['pay_now'];
resolve({
countryCode: data.country_code,
currencyCode: data.currency_code,
totalPriceStatus: 'FINAL',
totalPrice: data.total_str
});
});
}
actionHandler() {
return new CheckoutActionHandler(
this.ppcpConfig,
this.errorHandler(),
new Spinner()
);
}
}
export default PayNowHandler;

View file

@ -0,0 +1,31 @@
import BaseHandler from "./BaseHandler";
class CartHandler extends BaseHandler {
constructor(buttonConfig, ppcpConfig, externalHandler) {
super(buttonConfig, ppcpConfig, externalHandler);
}
transactionInfo() {
throw new Error('Transaction info fail. This is just a preview.');
}
createOrder() {
throw new Error('Create order fail. This is just a preview.');
}
approveOrder(data, actions) {
throw new Error('Approve order fail. This is just a preview.');
}
actionHandler() {
throw new Error('Action handler fail. This is just a preview.');
}
errorHandler() {
throw new Error('Error handler fail. This is just a preview.');
}
}
export default CartHandler;

View file

@ -1,6 +1,8 @@
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 widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import UpdatePaymentData from "./Helper/UpdatePaymentData";
class GooglepayButton {
@ -21,7 +23,11 @@ class GooglepayButton {
this.externalHandler
);
console.log('[GooglePayButton] new Button', this);
this.log = function() {
if ( this.buttonConfig.is_debug ) {
console.log('[GooglePayButton]', ...arguments);
}
}
}
init(config) {
@ -54,6 +60,15 @@ class GooglepayButton {
});
}
reinit() {
if (!this.googlePayConfig) {
return;
}
this.isInitialized = false;
this.init(this.googlePayConfig);
}
validateConfig() {
if ( ['PRODUCTION', 'TEST'].indexOf(this.buttonConfig.environment) === -1) {
console.error('[GooglePayButton] Invalid environment.', this.buttonConfig.environment);
@ -99,13 +114,18 @@ class GooglepayButton {
}
initClient() {
const callbacks = {
onPaymentAuthorized: this.onPaymentAuthorized.bind(this)
}
if ( this.buttonConfig.shipping.enabled && this.contextHandler.shippingAllowed() ) {
callbacks['onPaymentDataChanged'] = this.onPaymentDataChanged.bind(this);
}
this.paymentsClient = new google.payments.api.PaymentsClient({
environment: this.buttonConfig.environment,
// add merchant info maybe
paymentDataCallbacks: {
//onPaymentDataChanged: onPaymentDataChanged,
onPaymentAuthorized: this.onPaymentAuthorized.bind(this),
}
paymentDataCallbacks: callbacks
});
}
@ -137,10 +157,11 @@ class GooglepayButton {
* Add a Google Pay purchase button
*/
addButton(baseCardPaymentMethod) {
console.log('[GooglePayButton] addButton', this.context);
this.log('addButton', this.context);
const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig();
this.waitForWrapper(wrapper, () => {
jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape);
const button =
@ -152,7 +173,25 @@ class GooglepayButton {
buttonLocale: buttonStyle.language || 'en',
buttonSizeMode: 'fill',
});
jQuery(wrapper).append(button);
});
}
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);
console.error('Waiting for wrapper timed out.', selector);
}
}, delay);
}
//------------------------
@ -163,10 +202,10 @@ class GooglepayButton {
* Show Google Pay payment sheet when Google Pay payment button is clicked
*/
async onButtonClick() {
console.log('[GooglePayButton] onButtonClick', this.context);
this.log('onButtonClick', this.context);
const paymentDataRequest = await this.paymentDataRequest();
console.log('[GooglePayButton] onButtonClick: paymentDataRequest', paymentDataRequest, this.context);
this.log('onButtonClick: paymentDataRequest', paymentDataRequest, this.context);
window.ppcpFundingSource = 'googlepay'; // Do this on another place like on create order endpoint handler.
@ -184,35 +223,111 @@ class GooglepayButton {
paymentDataRequest.allowedPaymentMethods = googlePayConfig.allowedPaymentMethods;
paymentDataRequest.transactionInfo = await this.contextHandler.transactionInfo();
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
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'];
}
return paymentDataRequest;
}
//------------------------
// Shipping processing
//------------------------
shippingAddressParameters() {
return {
allowedCountryCodes: this.buttonConfig.shipping.countries,
phoneNumberRequired: true
};
}
onPaymentDataChanged(paymentData) {
this.log('onPaymentDataChanged', this.context);
this.log('paymentData', paymentData);
return new Promise(async (resolve, reject) => {
let paymentDataRequestUpdate = {};
const updatedData = await (new UpdatePaymentData(this.buttonConfig.ajax.update_payment_data)).update(paymentData);
const transactionInfo = await this.contextHandler.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;
// Handle unserviceable address.
if(!updatedData.shipping_options || !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;
}
resolve(paymentDataRequestUpdate);
});
}
unserviceableShippingAddressError() {
return {
reason: "SHIPPING_ADDRESS_UNSERVICEABLE",
message: "Cannot ship to the selected address",
intent: "SHIPPING_ADDRESS"
};
}
calculateNewTransactionInfo(updatedData) {
return {
countryCode: updatedData.country_code,
currencyCode: updatedData.currency_code,
totalPriceStatus: 'FINAL',
totalPrice: updatedData.total_str
};
}
//------------------------
// Payment process
//------------------------
onPaymentAuthorized(paymentData) {
console.log('[GooglePayButton] onPaymentAuthorized', this.context);
this.log('onPaymentAuthorized', this.context);
return this.processPayment(paymentData);
}
async processPayment(paymentData) {
console.log('[GooglePayButton] processPayment', this.context);
this.log('processPayment', this.context);
return new Promise(async (resolve, reject) => {
try {
let id = await this.contextHandler.createOrder();
console.log('[GooglePayButton] processPayment: createOrder', id, this.context);
this.log('processPayment: createOrder', id, this.context);
const confirmOrderResponse = await paypal.Googlepay().confirmOrder({
const confirmOrderResponse = await widgetBuilder.paypal.Googlepay().confirmOrder({
orderId: id,
paymentMethodData: paymentData.paymentMethodData
});
console.log('[GooglePayButton] processPayment: confirmOrder', confirmOrderResponse, this.context);
this.log('processPayment: confirmOrder', confirmOrderResponse, this.context);
/** Capture the Order on the Server */
if (confirmOrderResponse.status === "APPROVED") {
@ -259,7 +374,7 @@ class GooglepayButton {
}
}
console.log('[GooglePayButton] processPaymentResponse', response, this.context);
this.log('processPaymentResponse', response, this.context);
return response;
}

View file

@ -38,6 +38,12 @@ class GooglepayManager {
})();
}
reinit() {
for (const button of this.buttons) {
button.reinit();
}
}
}
export default GooglepayManager;

View file

@ -0,0 +1,38 @@
class UpdatePaymentData {
constructor(config) {
this.config = config;
}
update(paymentData) {
return new Promise((resolve, reject) => {
fetch(
this.config.endpoint,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.nonce,
paymentData: paymentData,
})
}
)
.then(result => result.json())
.then(result => {
if (!result.success) {
return;
}
resolve(result.data);
});
});
}
}
export default UpdatePaymentData;

View file

@ -0,0 +1,146 @@
import {loadCustomScript} from "@paypal/paypal-js";
import GooglepayButton from "./GooglepayButton";
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
(function ({
buttonConfig,
jQuery
}) {
let googlePayConfig;
let buttonQueue = [];
let activeButtons = {};
let bootstrapped = false;
// React to PayPal config changes.
jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => {
if (bootstrapped) {
createButton(ppcpConfig);
} else {
buttonQueue.push({
ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig))
});
}
});
// React to GooglePay config changes.
jQuery([
'#ppcp-googlepay_button_enabled',
'#ppcp-googlepay_button_type',
'#ppcp-googlepay_button_color',
'#ppcp-googlepay_button_language',
'#ppcp-googlepay_button_shipping_enabled'
].join(',')).on('change', () => {
for (const [selector, ppcpConfig] of Object.entries(activeButtons)) {
createButton(ppcpConfig);
}
});
// Maybe we can find a more elegant reload method when transitioning from styling modes.
jQuery([
'#ppcp-smart_button_enable_styling_per_location'
].join(',')).on('change', () => {
setTimeout(() => {
for (const [selector, ppcpConfig] of Object.entries(activeButtons)) {
createButton(ppcpConfig);
}
}, 100);
});
const applyConfigOptions = function (buttonConfig) {
buttonConfig.button = buttonConfig.button || {};
buttonConfig.button.style = buttonConfig.button.style || {};
buttonConfig.button.style.type = jQuery('#ppcp-googlepay_button_type').val();
buttonConfig.button.style.color = jQuery('#ppcp-googlepay_button_color').val();
buttonConfig.button.style.language = jQuery('#ppcp-googlepay_button_language').val();
}
const createButton = function (ppcpConfig) {
const selector = ppcpConfig.button.wrapper + 'GooglePay';
if (!jQuery('#ppcp-googlepay_button_enabled').is(':checked')) {
jQuery(selector).remove();
return;
}
buttonConfig = JSON.parse(JSON.stringify(buttonConfig));
buttonConfig.button.wrapper = selector;
applyConfigOptions(buttonConfig);
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-googlepay"></div>`;
if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement);
} else {
jQuery(selector).replaceWith(wrapperElement);
}
const button = new GooglepayButton(
'preview',
null,
buttonConfig,
ppcpConfig,
);
button.init(googlePayConfig);
activeButtons[selector] = ppcpConfig;
}
const bootstrap = async function () {
if (!widgetBuilder.paypal) {
return;
}
googlePayConfig = await widgetBuilder.paypal.Googlepay().config();
// We need to set bootstrapped here otherwise googlePayConfig may not be set.
bootstrapped = true;
let options;
while (options = buttonQueue.pop()) {
createButton(options.ppcpConfig);
}
};
document.addEventListener(
'DOMContentLoaded',
() => {
if (typeof (buttonConfig) === 'undefined') {
console.error('PayPal button could not be configured.');
return;
}
let paypalLoaded = false;
let googlePayLoaded = false;
const tryToBoot = () => {
if (!bootstrapped && paypalLoaded && googlePayLoaded) {
bootstrap();
}
}
// Load GooglePay SDK
loadCustomScript({ url: buttonConfig.sdk_url }).then(() => {
googlePayLoaded = true;
tryToBoot();
});
// Wait for PayPal to be loaded externally
if (typeof widgetBuilder.paypal !== 'undefined') {
paypalLoaded = true;
tryToBoot();
}
jQuery(document).on('ppcp-paypal-loaded', () => {
paypalLoaded = true;
tryToBoot();
});
},
);
})({
buttonConfig: window.wc_ppcp_googlepay_admin,
jQuery: window.jQuery
});

View file

@ -8,11 +8,17 @@ import GooglepayManager from "./GooglepayManager";
jQuery
}) {
let manager;
const bootstrap = function () {
const manager = new GooglepayManager(buttonConfig, ppcpConfig);
manager = new GooglepayManager(buttonConfig, ppcpConfig);
manager.init();
};
jQuery(document.body).on('updated_cart_totals updated_checkout', () => {
manager.reinit();
});
document.addEventListener(
'DOMContentLoaded',
() => {
@ -20,12 +26,7 @@ import GooglepayManager from "./GooglepayManager";
(typeof (buttonConfig) === 'undefined') ||
(typeof (ppcpConfig) === 'undefined')
) {
console.error('PayPal button could not be configured.');
return;
}
// If button wrapper is not present then there is no need to load the scripts.
if (!jQuery(buttonConfig.button.wrapper).length) {
// No PayPal buttons present on this page.
return;
}

View file

@ -11,10 +11,15 @@ namespace WooCommerce\PayPalCommerce\Googlepay;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodTypeInterface;
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Googlepay\Assets\BlocksPaymentMethod;
use WooCommerce\PayPalCommerce\Googlepay\Assets\Button;
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
@ -35,9 +40,9 @@ return array(
);
},
// If GooglePay is configured.
// If GooglePay is configured and onboarded.
'googlepay.available' => static function ( ContainerInterface $container ): bool {
if ( apply_filters( 'woocommerce_paypal_payments_googlepay_validate_product_status', false ) ) {
if ( apply_filters( 'woocommerce_paypal_payments_googlepay_validate_product_status', true ) ) {
$status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $status instanceof ApmProductStatus );
/**
@ -48,13 +53,32 @@ return array(
return true;
},
'googlepay.helpers.apm-product-status' => static function( ContainerInterface $container ): ApmProductStatus {
// We assume it's a referral if we can check product status without API request failures.
'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 {
return new AvailabilityNotice(
$container->get( 'googlepay.helpers.apm-product-status' ),
$container->get( 'wcgateway.is-wc-gateways-list-page' ),
$container->get( 'wcgateway.is-ppcp-settings-page' )
);
},
'googlepay.helpers.apm-product-status' => SingletonDecorator::make(
static function( ContainerInterface $container ): ApmProductStatus {
return new ApmProductStatus(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
$container->get( 'onboarding.state' )
$container->get( 'onboarding.state' ),
$container->get( 'api.helper.failure-registry' )
);
},
}
),
/**
* The matrix which countries and currency combinations can be used for GooglePay.
@ -153,4 +177,58 @@ return array(
return 'https://pay.google.com/gp/p/js/pay.js';
},
'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 {
return 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY';
},
'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 {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
}
$product_status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $product_status instanceof ApmProductStatus );
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
$enabled = $product_status->is_active();
$enabled_status_text = esc_html__( 'Status: Available', 'woocommerce-paypal-payments' );
$disabled_status_text = esc_html__( 'Status: Not yet enabled', 'woocommerce-paypal-payments' );
$button_text = $enabled
? esc_html__( 'Settings', 'woocommerce-paypal-payments' )
: esc_html__( 'Enable Google Pay', 'woocommerce-paypal-payments' );
$enable_url = $environment->current_environment_is( Environment::PRODUCTION )
? $container->get( 'googlepay.enable-url-live' )
: $container->get( 'googlepay.enable-url-sandbox' );
$button_url = $enabled
? admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway#field-alternative_payment_methods' )
: $enable_url;
return sprintf(
'<p>%1$s %2$s</p><p><a target="%3$s" href="%4$s" class="button">%5$s</a></p>',
$enabled ? $enabled_status_text : $disabled_status_text,
$enabled ? '<span class="dashicons dashicons-yes"></span>' : '<span class="dashicons dashicons-no"></span>',
$enabled ? '_self' : '_blank',
esc_url( $button_url ),
esc_html( $button_text )
);
},
);

View file

@ -11,7 +11,9 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Assets;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Countries;
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
@ -127,6 +129,7 @@ class Button implements ButtonInterface {
*/
public function initialize(): void {
add_filter( 'ppcp_onboarding_options', array( $this, 'add_onboarding_options' ), 10, 1 );
add_filter( 'ppcp_partner_referrals_option', array( $this, 'filter_partner_referrals_option' ), 10, 1 );
add_filter( 'ppcp_partner_referrals_data', array( $this, 'add_partner_referrals_data' ), 10, 1 );
}
@ -139,6 +142,10 @@ class Button implements ButtonInterface {
* @psalm-suppress MissingClosureParamType
*/
public function add_onboarding_options( $options ): string {
if ( ! apply_filters( 'woocommerce_paypal_payments_google_pay_onboarding_option', false ) ) {
return $options;
}
$checked = '';
try {
$onboard_with_google = $this->settings->get( 'ppcp-onboarding-google' );
@ -150,11 +157,28 @@ class Button implements ButtonInterface {
}
return $options
. '<li><label><input type="checkbox" id="ppcp-onboarding-google" ' . $checked . '> '
. '<li><label><input type="checkbox" id="ppcp-onboarding-google" ' . $checked . ' data-onboarding-option="ppcp-onboarding-google"> '
. __( 'Onboard with GooglePay', 'woocommerce-paypal-payments' )
. '</label></li>';
}
/**
* Filters a partner referrals option.
*
* @param array $option The option data.
* @return array
*/
public function filter_partner_referrals_option( array $option ): array {
if ( $option['valid'] ) {
return $option;
}
if ( $option['field'] === 'ppcp-onboarding-google' ) {
$option['valid'] = true;
$option['value'] = ( $option['value'] ? '1' : '' );
}
return $option;
}
/**
* Adds to partner referrals data.
*
@ -180,9 +204,6 @@ class Button implements ButtonInterface {
}
$data['capabilities'][] = 'GOOGLE_PAY';
$nonce = $data['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['seller_nonce'];
$data['operations'][] = array(
'operation' => 'API_INTEGRATION',
'api_integration_preference' => array(
@ -194,7 +215,6 @@ class Button implements ButtonInterface {
'PAYMENT',
'REFUND',
),
'seller_nonce' => $nonce,
),
),
),
@ -358,14 +378,53 @@ class Button implements ButtonInterface {
);
}
/**
* Enqueues scripts/styles for admin.
*/
public function enqueue_admin(): void {
wp_register_style(
'wc-ppcp-googlepay-admin',
untrailingslashit( $this->module_url ) . '/assets/css/styles.css',
array(),
$this->version
);
wp_enqueue_style( 'wc-ppcp-googlepay-admin' );
wp_register_script(
'wc-ppcp-googlepay-admin',
untrailingslashit( $this->module_url ) . '/assets/js/boot-admin.js',
array(),
$this->version,
true
);
wp_enqueue_script( 'wc-ppcp-googlepay-admin' );
wp_localize_script(
'wc-ppcp-googlepay-admin',
'wc_ppcp_googlepay_admin',
$this->script_data()
);
}
/**
* The configuration for the smart buttons.
*
* @return array
*/
public function script_data(): array {
$shipping = array(
'enabled' => $this->settings->has( 'googlepay_button_shipping_enabled' )
? boolval( $this->settings->get( 'googlepay_button_shipping_enabled' ) )
: false,
);
if ( $shipping['enabled'] ) {
$shipping['countries'] = array_keys( $this->wc_countries()->get_shipping_countries() );
}
return array(
'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION',
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'sdk_url' => $this->sdk_url,
'button' => array(
'wrapper' => '#ppc-button-googlepay-container',
@ -373,6 +432,13 @@ class Button implements ButtonInterface {
'mini_cart_wrapper' => '#ppc-button-googlepay-container-minicart',
'mini_cart_style' => $this->button_styles_for_context( 'mini-cart' ),
),
'shipping' => $shipping,
'ajax' => array(
'update_payment_data' => array(
'endpoint' => \WC_AJAX::get_endpoint( UpdatePaymentDataEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( UpdatePaymentDataEndpoint::nonce() ),
),
),
);
}
@ -404,4 +470,12 @@ class Button implements ButtonInterface {
return $values;
}
/**
* Returns a WC_Countries instance to check shipping
*
* @return WC_Countries
*/
private function wc_countries(): WC_Countries {
return new WC_Countries();
}
}

View file

@ -0,0 +1,219 @@
<?php
/**
* Endpoint to update payment data like shipping method and address.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay\Endpoint;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
/**
* Class UpdatePaymentDataEndpoint
*/
class UpdatePaymentDataEndpoint {
const ENDPOINT = 'ppc-googlepay-update-payment-data';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* UpdatePaymentDataEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws RuntimeException When a validation fails.
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
// Validate payment data.
if ( ! isset( $data['paymentData'] ) ) {
throw new RuntimeException(
__( 'No paymentData provided.', 'woocommerce-paypal-payments' )
);
}
$payment_data = $data['paymentData'];
// Set context as cart.
if ( is_callable( 'wc_maybe_define_constant' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
$this->update_addresses( $payment_data );
$this->update_shipping_method( $payment_data );
WC()->cart->calculate_shipping();
WC()->cart->calculate_fees();
WC()->cart->calculate_totals();
$total = (float) WC()->cart->get_total( 'numeric' );
// Shop settings.
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'];
$currency_code = get_woocommerce_currency();
wp_send_json_success(
array(
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
'shipping_options' => $this->get_shipping_options(),
)
);
return true;
} catch ( Throwable $error ) {
$this->logger->error( "UpdatePaymentDataEndpoint execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
wp_send_json_error();
return false;
}
}
/**
* Returns the array of available shipping methods.
*
* @return array
*/
public function get_shipping_options(): array {
$shipping_options = array();
$calculated_packages = WC()->shipping->calculate_shipping(
WC()->cart->get_shipping_packages()
);
if ( ! isset( $calculated_packages[0] ) && ! isset( $calculated_packages[0]['rates'] ) ) {
return array();
}
foreach ( $calculated_packages[0]['rates'] as $rate ) {
/**
* The shipping rate.
*
* @var \WC_Shipping_Rate $rate
*/
$shipping_options[] = array(
'id' => $rate->get_id(),
'label' => $rate->get_label(),
'description' => html_entity_decode(
wp_strip_all_tags(
wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) )
)
),
);
}
if ( ! isset( $shipping_options[0] ) ) {
return array();
}
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
return array(
'defaultSelectedOptionId' => ( $chosen_shipping_methods[0] ?? null ) ? $chosen_shipping_methods[0] : $shipping_options[0]['id'],
'shippingOptions' => $shipping_options,
);
}
/**
* Update addresses.
*
* @param array $payment_data The payment data.
* @return void
*/
private function update_addresses( array $payment_data ): void {
if ( ! in_array( $payment_data['callbackTrigger'] ?? '', array( 'SHIPPING_ADDRESS', 'INITIALIZE' ), true ) ) {
return;
}
/**
* The shipping methods.
*
* @var \WC_Customer|null $customer
*/
$customer = WC()->customer;
if ( ! $customer ) {
return;
}
$customer->set_billing_postcode( $payment_data['shippingAddress']['postalCode'] ?? '' );
$customer->set_billing_country( $payment_data['shippingAddress']['countryCode'] ?? '' );
$customer->set_billing_state( '' );
$customer->set_billing_city( $payment_data['shippingAddress']['locality'] ?? '' );
$customer->set_shipping_postcode( $payment_data['shippingAddress']['postalCode'] ?? '' );
$customer->set_shipping_country( $payment_data['shippingAddress']['countryCode'] ?? '' );
$customer->set_shipping_state( '' );
$customer->set_shipping_city( $payment_data['shippingAddress']['locality'] ?? '' );
// Save the data.
$customer->save();
WC()->session->set( 'customer', WC()->customer->get_data() );
}
/**
* Update shipping method.
*
* @param array $payment_data The payment data.
* @return void
*/
private function update_shipping_method( array $payment_data ): void {
$rate_id = $payment_data['shippingOptionData']['id'];
$calculated_packages = WC()->shipping->calculate_shipping(
WC()->cart->get_shipping_packages()
);
if ( $rate_id && isset( $calculated_packages[0]['rates'][ $rate_id ] ) ) {
WC()->session->set( 'chosen_shipping_methods', array( $rate_id ) );
}
}
}

View file

@ -11,7 +11,9 @@ namespace WooCommerce\PayPalCommerce\Googlepay;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
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;
@ -37,18 +39,39 @@ class GooglepayModule implements ModuleInterface {
*/
public function run( ContainerInterface $c ): void {
// Clears product status when appropriate.
add_action(
'woocommerce_paypal_payments_clear_apm_product_status',
function( Settings $settings = null ) use ( $c ): void {
$apm_status = $c->get( 'googlepay.helpers.apm-product-status' );
assert( $apm_status instanceof ApmProductStatus );
$apm_status->clear( $settings );
}
);
// Check if the module is applicable, correct country, currency, ... etc.
if ( ! $c->get( 'googlepay.eligible' ) ) {
return;
}
// Load the button handler.
$button = $c->get( 'googlepay.button' );
assert( $button instanceof ButtonInterface );
$button->initialize();
if ( ! $c->get( 'googlepay.available' ) ) {
// Show notice if there are product availability issues.
$availability_notice = $c->get( 'googlepay.availability_notice' );
assert( $availability_notice instanceof AvailabilityNotice );
$availability_notice->execute();
// Check if this merchant can activate / use the buttons.
// We allow non referral merchants as they can potentially still use GooglePay, we just have no way of checking the capability.
if ( ( ! $c->get( 'googlepay.available' ) ) && $c->get( 'googlepay.is_referral' ) ) {
return;
}
// Initializes button rendering.
add_action(
'wp',
static function () use ( $c, $button ) {
@ -59,6 +82,7 @@ class GooglepayModule implements ModuleInterface {
}
);
// Enqueue frontend scripts.
add_action(
'wp_enqueue_scripts',
static function () use ( $c, $button ) {
@ -66,6 +90,23 @@ class GooglepayModule implements ModuleInterface {
}
);
// Enqueue backend scripts.
add_action(
'admin_enqueue_scripts',
static function () use ( $c, $button ) {
if ( ! is_admin() ) {
return;
}
/**
* Should add this to the ButtonInterface.
*
* @psalm-suppress UndefinedInterfaceMethod
*/
$button->enqueue_admin();
}
);
// Registers buttons on blocks pages.
add_action(
'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c, $button ): void {
@ -75,21 +116,26 @@ class GooglepayModule implements ModuleInterface {
}
);
// Clear product status handling.
// Adds GooglePay component to the backend button preview settings.
add_action(
'woocommerce_paypal_payments_clear_apm_product_status',
function( Settings $settings = null ) use ( $c ): void {
$apm_status = $c->get( 'googlepay.helpers.apm-product-status' );
assert( $apm_status instanceof ApmProductStatus );
if ( ! $settings instanceof Settings ) {
$settings = null;
'woocommerce_paypal_payments_admin_gateway_settings',
function( array $settings ) use ( $c, $button ): array {
if ( is_array( $settings['components'] ) ) {
$settings['components'][] = 'googlepay';
}
$apm_status->clear( $settings );
return $settings;
}
);
// Initialize AJAX endpoints.
add_action(
'wc_ajax_' . UpdatePaymentDataEndpoint::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'googlepay.endpoint.update-payment-data' );
assert( $endpoint instanceof UpdatePaymentDataEndpoint );
$endpoint->handle_request();
}
);
}
/**

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -32,6 +33,13 @@ class ApmProductStatus {
*/
private $current_status = null;
/**
* If there was a request failure.
*
* @var bool
*/
private $has_request_failure = false;
/**
* The settings.
*
@ -53,21 +61,31 @@ class ApmProductStatus {
*/
private $onboarding_state;
/**
* The API failure registry
*
* @var FailureRegistry
*/
private $api_failure_registry;
/**
* ApmProductStatus constructor.
*
* @param Settings $settings The Settings.
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param State $onboarding_state The onboarding state.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
Settings $settings,
PartnersEndpoint $partners_endpoint,
State $onboarding_state
State $onboarding_state,
FailureRegistry $api_failure_registry
) {
$this->settings = $settings;
$this->partners_endpoint = $partners_endpoint;
$this->onboarding_state = $onboarding_state;
$this->api_failure_registry = $api_failure_registry;
}
/**
@ -76,33 +94,47 @@ class ApmProductStatus {
* @return bool
*/
public function is_active() : bool {
if ( $this->onboarding_state->current_state() < State::STATE_ONBOARDED ) {
// If not onboarded then makes no sense to check status.
if ( ! $this->is_onboarded() ) {
return false;
}
// If status was already checked on this request return the same result.
if ( null !== $this->current_status ) {
return $this->current_status;
}
// Check if status was checked on previous requests.
if ( $this->settings->has( self::SETTINGS_KEY ) && ( $this->settings->get( self::SETTINGS_KEY ) ) ) {
$this->current_status = wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) );
return $this->current_status;
}
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( Throwable $error ) {
// It may be a transitory error, don't persist the status.
// Check API failure registry to prevent multiple failed API requests.
if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) {
$this->has_request_failure = true;
$this->current_status = false;
return $this->current_status;
}
// Request seller status via PayPal API.
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( Throwable $error ) {
$this->has_request_failure = true;
$this->current_status = false;
return $this->current_status;
}
// Check the seller status for the intended capability.
foreach ( $seller_status->products() as $product ) {
if ( $product->name() !== 'PAYMENT_METHODS' ) {
continue;
}
if ( in_array( self::CAPABILITY_NAME, $product->capabilities(), true ) ) {
// Capability found, persist status and return true.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
@ -111,6 +143,7 @@ class ApmProductStatus {
}
}
// Capability not found, persist status and return false.
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED );
$this->settings->persist();
@ -118,6 +151,24 @@ class ApmProductStatus {
return $this->current_status;
}
/**
* Returns if the seller is onboarded.
*
* @return bool
*/
public function is_onboarded(): bool {
return $this->onboarding_state->current_state() >= State::STATE_ONBOARDED;
}
/**
* Returns if there was a request failure.
*
* @return bool
*/
public function has_request_failure(): bool {
return $this->has_request_failure;
}
/**
* Clears the persisted result to force a recheck.
*
@ -132,8 +183,10 @@ class ApmProductStatus {
$this->current_status = null;
if ( $settings->has( self::SETTINGS_KEY ) ) {
$settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED );
$settings->persist();
}
}
}

View file

@ -0,0 +1,159 @@
<?php
/**
* Adds availability notice if applicable.
*
* @package WooCommerce\PayPalCommerce\Googlepay\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
/**
* Class AvailabilityNotice
*/
class AvailabilityNotice {
/**
* The product status handler.
*
* @var ApmProductStatus
*/
private $product_status;
/**
* Indicates if we're on the WooCommerce gateways list page.
*
* @var bool
*/
private $is_wc_gateways_list_page;
/**
* Indicates if we're on a PPCP Settings page.
*
* @var bool
*/
private $is_ppcp_settings_page;
/**
* Class ApmProductStatus constructor.
* @param ApmProductStatus $product_status The product status handler.
* @param bool $is_wc_gateways_list_page Indicates if we're on the WooCommerce gateways list page.
* @param bool $is_ppcp_settings_page Indicates if we're on a PPCP Settings page.
*/
public function __construct(
ApmProductStatus $product_status,
bool $is_wc_gateways_list_page,
bool $is_ppcp_settings_page
) {
$this->product_status = $product_status;
$this->is_wc_gateways_list_page = $is_wc_gateways_list_page;
$this->is_ppcp_settings_page = $is_ppcp_settings_page;
}
/**
* Adds availability notice if applicable.
*
* @return void
*/
public function execute(): void {
if ( ! $this->should_display() ) {
return;
}
// We need to check is active before checking failure requests, otherwise failure status won't be set.
$is_active = $this->product_status->is_active();
if ( $this->product_status->has_request_failure() ) {
$this->add_seller_status_failure_notice();
} elseif ( ! $is_active ) {
$this->add_not_available_notice();
}
}
/**
* Whether the message should display.
*
* @return bool
*/
protected function should_display(): bool {
if ( ! $this->product_status->is_onboarded() ) {
return false;
}
if ( ! $this->is_wc_gateways_list_page && ! $this->is_ppcp_settings_page ) {
return false;
}
return true;
}
/**
* Adds seller status failure notice.
*
* @return void
*/
private function add_seller_status_failure_notice(): void {
add_filter(
Repository::NOTICES_FILTER,
/**
* Adds seller status notice.
*
* @param array $notices The notices.
* @return array
*
* @psalm-suppress MissingClosureParamType
*/
static function ( $notices ): array {
$message = sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__(
'<p>Notice: We could not determine your PayPal seller status to list your available features. Disconnect and reconnect your PayPal account through our %1$sonboarding process%2$s to resolve this.</p><p>Don\'t worry if you cannot use the %1$sonboarding process%2$s; most functionalities available to your account should work.</p>',
'woocommerce-paypal-payments'
),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#connect-paypal-account" target="_blank">',
'</a>'
);
// Name the key so it can be overridden in other modules.
$notices['error_product_status'] = new Message( $message, 'warning', true, 'ppcp-notice-wrapper' );
return $notices;
}
);
}
/**
* Adds not available notice.
*
* @return void
*/
private function add_not_available_notice(): void {
add_filter(
Repository::NOTICES_FILTER,
/**
* Adds GooglePay not available notice.
*
* @param array $notices The notices.
* @return array
*
* @psalm-suppress MissingClosureParamType
*/
static function ( $notices ): array {
$message = sprintf(
__(
'Google Pay is not available on your PayPal seller account.',
'woocommerce-paypal-payments'
)
);
$notices[] = new Message( $message, 'warning', true, 'ppcp-notice-wrapper' );
return $notices;
}
);
}
}

View file

@ -11,6 +11,7 @@ module.exports = {
entry: {
'boot': path.resolve('./resources/js/boot.js'),
'boot-block': path.resolve('./resources/js/boot-block.js'),
'boot-admin': path.resolve('./resources/js/boot-admin.js'),
"styles": path.resolve('./resources/css/styles.scss')
},
output: {

View file

@ -13,10 +13,7 @@ const ppcp_onboarding = {
reload: function() {
const buttons = document.querySelectorAll(ppcp_onboarding.BUTTON_SELECTOR);
if (0 === buttons.length) {
return;
}
if (buttons.length > 0) {
// Add event listeners to buttons preventing link clicking if PayPal init failed.
buttons.forEach(
(element) => {
@ -70,27 +67,48 @@ const ppcp_onboarding = {
},
1000
);
}
const onboard_pui = document.querySelector('#ppcp-onboarding-pui');
const $onboarding_inputs = function () {
return jQuery('*[data-onboarding-option]');
};
const onboarding_options = function () {
let options = {};
$onboarding_inputs().each((index, el) => {
const opt = jQuery(el).data('onboardingOption');
options[opt] = el.checked;
});
return options;
}
const disable_onboarding_options = function () {
$onboarding_inputs().each((index, el) => {
el.setAttribute('disabled', 'disabled');
});
}
const enable_onboarding_options = function () {
$onboarding_inputs().each((index, el) => {
el.removeAttribute('disabled');
});
}
const update_onboarding_options = function () {
const spinner = '<span class="spinner is-active" style="float: none;"></span>';
onboard_pui?.addEventListener('click', (event) => {
event.preventDefault();
onboard_pui.setAttribute('disabled', 'disabled');
disable_onboarding_options();
buttons.forEach((element) => {
element.removeAttribute('href');
element.setAttribute('disabled', 'disabled');
jQuery(spinner).insertAfter(element);
});
fetch(PayPalCommerceGatewayOnboarding.pui_endpoint, {
fetch(PayPalCommerceGatewayOnboarding.update_signup_links_endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: PayPalCommerceGatewayOnboarding.pui_nonce,
checked: onboard_pui.checked
nonce: PayPalCommerceGatewayOnboarding.update_signup_links_nonce,
settings: onboarding_options()
})
}).then((res)=>{
return res.json();
@ -99,7 +117,6 @@ const ppcp_onboarding = {
alert('Could not update signup buttons: ' + JSON.stringify(data));
return;
}
buttons.forEach((element) => {
for (let [key, value] of Object.entries(data.data.signup_links)) {
key = 'connect-to' + key.replace(/-/g, '');
@ -110,9 +127,13 @@ const ppcp_onboarding = {
}
}
});
onboard_pui.removeAttribute('disabled');
enable_onboarding_options();
});
}
$onboarding_inputs().on('click', (event) => {
event.preventDefault();
update_onboarding_options();
});
})
},
loginSeller: function(env, authCode, sharedId) {

View file

@ -19,7 +19,10 @@ document.addEventListener(
if (! toggleElement.checked) {
group.forEach( (elementToHide) => {
document.querySelector(elementToHide).style.display = 'none';
const element = document.querySelector(elementToHide);
if (element) {
element.style.display = 'none';
}
})
}
toggleElement.addEventListener(
@ -27,7 +30,10 @@ document.addEventListener(
(event) => {
if (! event.target.checked) {
group.forEach( (elementToHide) => {
document.querySelector(elementToHide).style.display = 'none';
const element = document.querySelector(elementToHide);
if (element) {
element.style.display = 'none';
}
});
return;

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\PayUponInvoiceEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController;
@ -187,8 +187,8 @@ return array(
$logger
);
},
'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : PayUponInvoiceEndpoint {
return new PayUponInvoiceEndpoint(
'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint {
return new UpdateSignupLinksEndpoint(
$container->get( 'wcgateway.settings' ),
$container->get( 'button.request-data' ),
$container->get( 'onboarding.signup-link-cache' ),

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding\Assets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -155,8 +156,8 @@ class OnboardingAssets {
'error_messages' => array(
'no_credentials' => __( 'API credentials must be entered to save the settings.', 'woocommerce-paypal-payments' ),
),
'pui_endpoint' => \WC_AJAX::get_endpoint( 'ppc-pui' ),
'pui_nonce' => wp_create_nonce( 'ppc-pui' ),
'update_signup_links_endpoint' => \WC_AJAX::get_endpoint( UpdateSignupLinksEndpoint::ENDPOINT ),
'update_signup_links_nonce' => wp_create_nonce( UpdateSignupLinksEndpoint::ENDPOINT ),
);
}

View file

@ -14,14 +14,17 @@ use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
/**
* Class PayUponInvoiceEndpoint
* Class UpdateSignupLinksEndpoint
*/
class PayUponInvoiceEndpoint implements EndpointInterface {
class UpdateSignupLinksEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-update-signup-links';
/**
* The settings.
@ -66,7 +69,7 @@ class PayUponInvoiceEndpoint implements EndpointInterface {
protected $logger;
/**
* PayUponInvoiceEndpoint constructor.
* UpdateSignupLinksEndpoint constructor.
*
* @param Settings $settings The settings.
* @param RequestData $request_data The request data.
@ -97,7 +100,7 @@ class PayUponInvoiceEndpoint implements EndpointInterface {
* @return string
*/
public static function nonce(): string {
return 'ppc-pui';
return self::ENDPOINT;
}
/**
@ -116,13 +119,26 @@ class PayUponInvoiceEndpoint implements EndpointInterface {
try {
$data = $this->request_data->read_request( $this->nonce() );
$this->settings->set( 'ppcp-onboarding-pui', $data['checked'] );
foreach ( $data['settings'] ?? array() as $field => $value ) {
$option = apply_filters(
'ppcp_partner_referrals_option',
array(
'field' => $field,
'value' => $value,
'valid' => false,
)
);
if ( $option['valid'] ) {
$this->settings->set( $field, $value );
}
}
$this->settings->persist();
foreach ( $this->signup_link_ids as $key ) {
if ( $this->signup_link_cache->has( $key ) ) {
$this->signup_link_cache->delete( $key );
}
( new OnboardingUrl( $this->signup_link_cache, $key, get_current_user_id() ) )->delete();
}
foreach ( $this->signup_link_ids as $key ) {

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
@ -96,7 +97,7 @@ class OnboardingModule implements ModuleInterface {
);
add_action(
'wc_ajax_ppc-pui',
'wc_ajax_' . UpdateSignupLinksEndpoint::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'onboarding.endpoint.pui' );
$endpoint->handle_request();

View file

@ -95,7 +95,7 @@ class OnboardingOptionsRenderer {
$checked = '';
}
return '<li><label><input type="checkbox" id="ppcp-onboarding-pui" ' . $checked . '> ' .
return '<li><label><input type="checkbox" id="ppcp-onboarding-pui" ' . $checked . ' data-onboarding-option="ppcp-onboarding-pui"> ' .
__( 'Onboard with Pay upon Invoice', 'woocommerce-paypal-payments' ) . '
</label></li>';
}

View file

@ -16,7 +16,7 @@ document.addEventListener(
jQuery( '*[data-ppcp-display]' ).each( (index, el) => {
const rules = jQuery(el).data('ppcpDisplay');
console.log('rules', rules);
// console.log('rules', rules);
for (const rule of rules) {
displayManager.addRule(rule);

View file

@ -14,11 +14,11 @@ class DisplayManager {
addRule(ruleConfig) {
const updateStatus = () => {
this.ruleStatus[ruleConfig.key] = this.rules[ruleConfig.key].status;
console.log('ruleStatus', this.ruleStatus);
//console.log('ruleStatus', this.ruleStatus);
}
this.rules[ruleConfig.key] = new Rule(ruleConfig, updateStatus.bind(this));
console.log('Rule', this.rules[ruleConfig.key]);
//console.log('Rule', this.rules[ruleConfig.key]);
}
register() {

View file

@ -15,14 +15,14 @@ class Rule {
const condition = ConditionFactory.make(conditionConfig, updateStatus);
this.conditions[condition.key] = condition;
console.log('Condition', condition);
//console.log('Condition', condition);
}
for (const actionConfig of this.config.actions) {
const action = ActionFactory.make(actionConfig);
this.actions[action.key] = action;
console.log('Action', action);
//console.log('Action', action);
}
}

View file

@ -87,6 +87,7 @@ document.addEventListener(
try {
renderer.render({});
jQuery(document).trigger('ppcp_paypal_render_preview', settings);
} catch (err) {
console.error(err);
}
@ -113,7 +114,7 @@ document.addEventListener(
'client-id': PayPalCommerceGatewaySettings.client_id,
'currency': PayPalCommerceGatewaySettings.currency,
'integration-date': PayPalCommerceGatewaySettings.integration_date,
'components': ['buttons', 'funding-eligibility', 'messages'],
'components': PayPalCommerceGatewaySettings.components,
'enable-funding': ['venmo', 'paylater'],
};

View file

@ -1021,7 +1021,8 @@ return array(
$partner_endpoint,
$container->get( 'dcc.status-cache' ),
$container->get( 'api.helpers.dccapplies' ),
$container->get( 'onboarding.state' )
$container->get( 'onboarding.state' ),
$container->get( 'api.helper.failure-registry' )
);
},
@ -1101,7 +1102,8 @@ return array(
$container->get( 'wcgateway.settings' ),
$container->get( 'api.endpoint.partners' ),
$container->get( 'pui.status-cache' ),
$container->get( 'onboarding.state' )
$container->get( 'onboarding.state' ),
$container->get( 'api.helper.failure-registry' )
);
},
'wcgateway.pay-upon-invoice' => static function ( ContainerInterface $container ): PayUponInvoice {

View file

@ -211,6 +211,8 @@ class SettingsPageAssets {
wp_localize_script(
'ppcp-gateway-settings',
'PayPalCommerceGatewaySettings',
apply_filters(
'woocommerce_paypal_payments_admin_gateway_settings',
array(
'is_subscriptions_plugin_active' => $this->subscription_helper->plugin_is_active(),
'client_id' => $this->client_id,
@ -221,6 +223,8 @@ class SettingsPageAssets {
'is_pay_later_button_enabled' => $this->is_pay_later_button_enabled,
'disabled_sources' => $this->disabled_sources,
'all_funding_sources' => $this->all_funding_sources,
'components' => array( 'buttons', 'funding-eligibility', 'messages' ),
)
)
);
}

View file

@ -142,6 +142,20 @@ class PayUponInvoice {
$this->settings->persist();
}
add_filter(
'ppcp_partner_referrals_option',
function ( array $option ): array {
if ( $option['valid'] ) {
return $option;
}
if ( $option['field'] === 'ppcp-onboarding-pui' ) {
$option['valid'] = true;
$option['value'] = ( $option['value'] ? '1' : '' );
}
return $option;
}
);
add_filter(
'ppcp_partner_referrals_data',
function ( array $data ): array {

View file

@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -37,6 +38,14 @@ class DCCProductStatus {
* @var bool|null
*/
private $current_status_cache;
/**
* If there was a request failure.
*
* @var bool
*/
private $has_request_failure = false;
/**
* The settings.
*
@ -65,6 +74,13 @@ class DCCProductStatus {
*/
private $onboarding_state;
/**
* The API failure registry
*
* @var FailureRegistry
*/
private $api_failure_registry;
/**
* DccProductStatus constructor.
*
@ -73,19 +89,22 @@ class DCCProductStatus {
* @param Cache $cache The cache.
* @param DccApplies $dcc_applies The dcc applies helper.
* @param State $onboarding_state The onboarding state.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
Settings $settings,
PartnersEndpoint $partners_endpoint,
Cache $cache,
DccApplies $dcc_applies,
State $onboarding_state
State $onboarding_state,
FailureRegistry $api_failure_registry
) {
$this->settings = $settings;
$this->partners_endpoint = $partners_endpoint;
$this->cache = $cache;
$this->dcc_applies = $dcc_applies;
$this->onboarding_state = $onboarding_state;
$this->api_failure_registry = $api_failure_registry;
}
/**
@ -111,9 +130,17 @@ class DCCProductStatus {
return true;
}
// Check API failure registry to prevent multiple failed API requests.
if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) {
$this->has_request_failure = true;
$this->current_status_cache = false;
return $this->current_status_cache;
}
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( Throwable $error ) {
$this->has_request_failure = true;
$this->current_status_cache = false;
return false;
}
@ -149,4 +176,14 @@ class DCCProductStatus {
$this->current_status_cache = false;
return false;
}
/**
* Returns if there was a request failure.
*
* @return bool
*/
public function has_request_failure(): bool {
return $this->has_request_failure;
}
}

View file

@ -13,6 +13,7 @@ use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -36,6 +37,14 @@ class PayUponInvoiceProductStatus {
* @var bool|null
*/
private $current_status_cache;
/**
* If there was a request failure.
*
* @var bool
*/
private $has_request_failure = false;
/**
* The settings.
*
@ -57,6 +66,13 @@ class PayUponInvoiceProductStatus {
*/
private $onboarding_state;
/**
* The API failure registry
*
* @var FailureRegistry
*/
private $api_failure_registry;
/**
* PayUponInvoiceProductStatus constructor.
*
@ -64,17 +80,20 @@ class PayUponInvoiceProductStatus {
* @param PartnersEndpoint $partners_endpoint The Partner Endpoint.
* @param Cache $cache The cache.
* @param State $onboarding_state The onboarding state.
* @param FailureRegistry $api_failure_registry The API failure registry.
*/
public function __construct(
Settings $settings,
PartnersEndpoint $partners_endpoint,
Cache $cache,
State $onboarding_state
State $onboarding_state,
FailureRegistry $api_failure_registry
) {
$this->settings = $settings;
$this->partners_endpoint = $partners_endpoint;
$this->cache = $cache;
$this->onboarding_state = $onboarding_state;
$this->api_failure_registry = $api_failure_registry;
}
/**
@ -99,9 +118,17 @@ class PayUponInvoiceProductStatus {
return true;
}
// Check API failure registry to prevent multiple failed API requests.
if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) {
$this->has_request_failure = true;
$this->current_status_cache = false;
return $this->current_status_cache;
}
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( Throwable $error ) {
$this->has_request_failure = true;
$this->current_status_cache = false;
return false;
}
@ -136,4 +163,14 @@ class PayUponInvoiceProductStatus {
$this->current_status_cache = false;
return false;
}
/**
* Returns if there was a request failure.
*
* @return bool
*/
public function has_request_failure(): bool {
return $this->has_request_failure;
}
}

View file

@ -456,7 +456,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'description' => __( 'If you use your PayPal account with more than one installation, please use a distinct prefix to separate those installations. Please use only English letters and "-", "_" characters.', 'woocommerce-paypal-payments' ),
'maxlength' => 15,
'custom_attributes' => array(
'pattern' => '[a-zA-Z_-]+',
'pattern' => '[a-zA-Z_\\-]+',
),
'default' => ( static function (): string {
$site_url = get_site_url( get_current_blog_id() );

View file

@ -264,6 +264,11 @@
<code>DAY_IN_SECONDS</code>
</UndefinedConstant>
</file>
<file src="modules/ppcp-api-client/src/Helper/FailureRegistry.php">
<UndefinedConstant occurrences="1">
<code>DAY_IN_SECONDS</code>
</UndefinedConstant>
</file>
<file src="modules/ppcp-button/services.php">
<PossiblyFalseArgument occurrences="1">
<code>realpath( __FILE__ )</code>