Merge branch 'trunk' into issue-174-vaulting-pay-later-ux

This commit is contained in:
Danae Millan 2022-04-13 13:54:52 -04:00
commit b488c909eb
199 changed files with 9684 additions and 2687 deletions

View file

@ -25,6 +25,9 @@ class Repository implements RepositoryInterface {
*/
public function current_message(): array {
return array_filter(
/**
* Returns the list of admin messages.
*/
(array) apply_filters(
self::NOTICES_FILTER,
array()

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
@ -25,15 +26,20 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ApplicationContextFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\MoneyFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentSourceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerReceivableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
@ -42,37 +48,39 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'api.host' => function( ContainerInterface $container ) : string {
'api.host' => function( ContainerInterface $container ) : string {
return PAYPAL_API_URL;
},
'api.paypal-host' => function( ContainerInterface $container ) : string {
'api.paypal-host' => function( ContainerInterface $container ) : string {
return PAYPAL_API_URL;
},
'api.partner_merchant_id' => static function () : string {
'api.partner_merchant_id' => static function () : string {
return '';
},
'api.merchant_email' => function () : string {
'api.merchant_email' => function () : string {
return '';
},
'api.merchant_id' => function () : string {
'api.merchant_id' => function () : string {
return '';
},
'api.key' => static function (): string {
'api.key' => static function (): string {
return '';
},
'api.secret' => static function (): string {
'api.secret' => static function (): string {
return '';
},
'api.prefix' => static function (): string {
'api.prefix' => static function (): string {
return 'WC-';
},
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$cache = new Cache( 'ppcp-paypal-bearer' );
$key = $container->get( 'api.key' );
$secret = $container->get( 'api.secret' );
@ -88,7 +96,7 @@ return array(
$settings
);
},
'api.endpoint.partners' => static function ( ContainerInterface $container ) : PartnersEndpoint {
'api.endpoint.partners' => static function ( ContainerInterface $container ) : PartnersEndpoint {
return new PartnersEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
@ -98,19 +106,21 @@ return array(
$container->get( 'api.merchant_id' )
);
},
'api.factory.sellerstatus' => static function ( ContainerInterface $container ) : SellerStatusFactory {
'api.factory.sellerstatus' => static function ( ContainerInterface $container ) : SellerStatusFactory {
return new SellerStatusFactory();
},
'api.endpoint.payment-token' => static function ( ContainerInterface $container ) : PaymentTokenEndpoint {
'api.endpoint.payment-token' => static function ( ContainerInterface $container ) : PaymentTokenEndpoint {
return new PaymentTokenEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.payment-token' ),
$container->get( 'api.factory.payment-token-action-links' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.prefix' )
$container->get( 'api.repository.customer' ),
$container->get( 'api.repository.paypal-request-id' )
);
},
'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint {
'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint {
return new WebhookEndpoint(
$container->get( 'api.host' ),
@ -120,27 +130,27 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.partner-referrals' => static function ( ContainerInterface $container ) : PartnerReferrals {
'api.endpoint.partner-referrals' => static function ( ContainerInterface $container ) : PartnerReferrals {
return new PartnerReferrals(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.identity-token' => static function ( ContainerInterface $container ) : IdentityToken {
'api.endpoint.identity-token' => static function ( ContainerInterface $container ) : IdentityToken {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$prefix = $container->get( 'api.prefix' );
$settings = $container->get( 'wcgateway.settings' );
$customer_repository = $container->get( 'api.repository.customer' );
return new IdentityToken(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$logger,
$prefix
$settings,
$customer_repository
);
},
'api.endpoint.payments' => static function ( ContainerInterface $container ): PaymentsEndpoint {
'api.endpoint.payments' => static function ( ContainerInterface $container ): PaymentsEndpoint {
$authorizations_factory = $container->get( 'api.factory.authorization' );
$capture_factory = $container->get( 'api.factory.capture' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
@ -153,7 +163,7 @@ return array(
$logger
);
},
'api.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSeller {
'api.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSeller {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new LoginSeller(
@ -162,7 +172,7 @@ return array(
$logger
);
},
'api.endpoint.order' => static function ( ContainerInterface $container ): OrderEndpoint {
'api.endpoint.order' => static function ( ContainerInterface $container ): OrderEndpoint {
$order_factory = $container->get( 'api.factory.order' );
$patch_collection_factory = $container->get( 'api.factory.patch-collection-factory' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
@ -189,47 +199,68 @@ return array(
$subscription_helper
);
},
'api.repository.paypal-request-id' => static function( ContainerInterface $container ) : PayPalRequestIdRepository {
'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint {
return new BillingAgreementsEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.repository.paypal-request-id' => static function( ContainerInterface $container ) : PayPalRequestIdRepository {
return new PayPalRequestIdRepository();
},
'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository {
'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository {
$settings = $container->get( 'wcgateway.settings' );
return new ApplicationContextRepository( $settings );
},
'api.repository.partner-referrals-data' => static function ( ContainerInterface $container ) : PartnerReferralsData {
'api.repository.partner-referrals-data' => static function ( ContainerInterface $container ) : PartnerReferralsData {
$merchant_email = $container->get( 'api.merchant_email' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
return new PartnerReferralsData( $merchant_email, $dcc_applies );
return new PartnerReferralsData( $dcc_applies );
},
'api.repository.cart' => static function ( ContainerInterface $container ): CartRepository {
'api.repository.cart' => static function ( ContainerInterface $container ): CartRepository {
$factory = $container->get( 'api.factory.purchase-unit' );
return new CartRepository( $factory );
},
'api.repository.payee' => static function ( ContainerInterface $container ): PayeeRepository {
'api.repository.payee' => static function ( ContainerInterface $container ): PayeeRepository {
$merchant_email = $container->get( 'api.merchant_email' );
$merchant_id = $container->get( 'api.merchant_id' );
return new PayeeRepository( $merchant_email, $merchant_id );
},
'api.factory.application-context' => static function ( ContainerInterface $container ) : ApplicationContextFactory {
'api.repository.customer' => static function( ContainerInterface $container ): CustomerRepository {
$prefix = $container->get( 'api.prefix' );
return new CustomerRepository( $prefix );
},
'api.repository.order' => static function( ContainerInterface $container ): OrderRepository {
return new OrderRepository(
$container->get( 'api.endpoint.order' )
);
},
'api.factory.application-context' => static function ( ContainerInterface $container ) : ApplicationContextFactory {
return new ApplicationContextFactory();
},
'api.factory.payment-token' => static function ( ContainerInterface $container ) : PaymentTokenFactory {
'api.factory.payment-token' => static function ( ContainerInterface $container ) : PaymentTokenFactory {
return new PaymentTokenFactory();
},
'api.factory.webhook' => static function ( ContainerInterface $container ): WebhookFactory {
'api.factory.payment-token-action-links' => static function ( ContainerInterface $container ) : PaymentTokenActionLinksFactory {
return new PaymentTokenActionLinksFactory();
},
'api.factory.webhook' => static function ( ContainerInterface $container ): WebhookFactory {
return new WebhookFactory();
},
'api.factory.webhook-event' => static function ( ContainerInterface $container ): WebhookEventFactory {
'api.factory.webhook-event' => static function ( ContainerInterface $container ): WebhookEventFactory {
return new WebhookEventFactory();
},
'api.factory.capture' => static function ( ContainerInterface $container ): CaptureFactory {
'api.factory.capture' => static function ( ContainerInterface $container ): CaptureFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new CaptureFactory( $amount_factory );
return new CaptureFactory(
$amount_factory,
$container->get( 'api.factory.seller-receivable-breakdown' )
);
},
'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory {
'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory {
$amount_factory = $container->get( 'api.factory.amount' );
$payee_repository = $container->get( 'api.repository.payee' );
@ -249,34 +280,43 @@ return array(
$prefix
);
},
'api.factory.patch-collection-factory' => static function ( ContainerInterface $container ): PatchCollectionFactory {
'api.factory.patch-collection-factory' => static function ( ContainerInterface $container ): PatchCollectionFactory {
return new PatchCollectionFactory();
},
'api.factory.payee' => static function ( ContainerInterface $container ): PayeeFactory {
'api.factory.payee' => static function ( ContainerInterface $container ): PayeeFactory {
return new PayeeFactory();
},
'api.factory.item' => static function ( ContainerInterface $container ): ItemFactory {
return new ItemFactory();
'api.factory.item' => static function ( ContainerInterface $container ): ItemFactory {
return new ItemFactory(
$container->get( 'api.shop.currency' )
);
},
'api.factory.shipping' => static function ( ContainerInterface $container ): ShippingFactory {
'api.factory.shipping' => static function ( ContainerInterface $container ): ShippingFactory {
$address_factory = $container->get( 'api.factory.address' );
return new ShippingFactory( $address_factory );
},
'api.factory.amount' => static function ( ContainerInterface $container ): AmountFactory {
'api.factory.amount' => static function ( ContainerInterface $container ): AmountFactory {
$item_factory = $container->get( 'api.factory.item' );
return new AmountFactory( $item_factory );
return new AmountFactory(
$item_factory,
$container->get( 'api.factory.money' ),
$container->get( 'api.shop.currency' )
);
},
'api.factory.payer' => static function ( ContainerInterface $container ): PayerFactory {
'api.factory.money' => static function ( ContainerInterface $container ): MoneyFactory {
return new MoneyFactory();
},
'api.factory.payer' => static function ( ContainerInterface $container ): PayerFactory {
$address_factory = $container->get( 'api.factory.address' );
return new PayerFactory( $address_factory );
},
'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory {
'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory {
return new AddressFactory();
},
'api.factory.payment-source' => static function ( ContainerInterface $container ): PaymentSourceFactory {
'api.factory.payment-source' => static function ( ContainerInterface $container ): PaymentSourceFactory {
return new PaymentSourceFactory();
},
'api.factory.order' => static function ( ContainerInterface $container ): OrderFactory {
'api.factory.order' => static function ( ContainerInterface $container ): OrderFactory {
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' );
$application_context_repository = $container->get( 'api.repository.application-context' );
@ -290,15 +330,340 @@ return array(
$payment_source_factory
);
},
'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory {
'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory {
$authorizations_factory = $container->get( 'api.factory.authorization' );
$capture_factory = $container->get( 'api.factory.capture' );
return new PaymentsFactory( $authorizations_factory, $capture_factory );
},
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
return new AuthorizationFactory();
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies();
'api.factory.exchange-rate' => static function ( ContainerInterface $container ): ExchangeRateFactory {
return new ExchangeRateFactory();
},
'api.factory.platform-fee' => static function ( ContainerInterface $container ): PlatformFeeFactory {
return new PlatformFeeFactory(
$container->get( 'api.factory.money' ),
$container->get( 'api.factory.payee' )
);
},
'api.factory.seller-receivable-breakdown' => static function ( ContainerInterface $container ): SellerReceivableBreakdownFactory {
return new SellerReceivableBreakdownFactory(
$container->get( 'api.factory.money' ),
$container->get( 'api.factory.exchange-rate' ),
$container->get( 'api.factory.platform-fee' )
);
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies(
$container->get( 'api.dcc-supported-country-currency-matrix' ),
$container->get( 'api.dcc-supported-country-card-matrix' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
'api.shop.currency' => static function ( ContainerInterface $container ) : string {
$currency = get_woocommerce_currency();
if ( $currency ) {
return $currency;
}
$currency = get_option( 'woocommerce_currency' );
if ( ! $currency ) {
return 'NO_CURRENCY'; // Unlikely to happen.
}
return $currency;
},
'api.shop.country' => static function ( ContainerInterface $container ) : string {
$location = wc_get_base_location();
return $location['country'];
},
'api.shop.is-psd2-country' => static function ( ContainerInterface $container ) : bool {
return in_array(
$container->get( 'api.shop.country' ),
$container->get( 'api.psd2-countries' ),
true
);
},
'api.shop.is-currency-supported' => static function ( ContainerInterface $container ) : bool {
return in_array(
$container->get( 'api.shop.currency' ),
$container->get( 'api.supported-currencies' ),
true
);
},
/**
* Currencies supported by PayPal.
*
* From https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies/
*/
'api.supported-currencies' => static function ( ContainerInterface $container ) : array {
return array(
'AUD',
'BRL',
'CAD',
'CNY',
'CZK',
'DKK',
'EUR',
'HKD',
'HUF',
'ILS',
'JPY',
'MYR',
'MXN',
'TWD',
'NZD',
'NOK',
'PHP',
'PLN',
'GBP',
'RUB',
'SGD',
'SEK',
'CHF',
'THB',
'USD',
);
},
/**
* The matrix which countries and currency combinations can be used for DCC.
*/
'api.dcc-supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for DCC.
*/
return apply_filters(
'woocommerce_paypal_payments_supported_country_currency_matrix',
array(
'AU' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'DE' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'ES' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'FR' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'GB' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'IT' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'US' => array(
'AUD',
'CAD',
'EUR',
'GBP',
'JPY',
'USD',
),
'CA' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
)
);
},
/**
* Which countries support which credit cards. Empty credit card arrays mean no restriction on currency.
*/
'api.dcc-supported-country-card-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries support which credit cards. Empty credit card arrays mean no restriction on currency.
*/
return apply_filters(
'woocommerce_paypal_payments_supported_country_card_matrix',
array(
'AU' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'AUD' ),
),
'DE' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'ES' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'FR' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'GB' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'GBP', 'USD' ),
),
'IT' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'US' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'USD' ),
'discover' => array( 'USD' ),
),
'CA' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'CAD' ),
'jcb' => array( 'CAD' ),
),
)
);
},
'api.psd2-countries' => static function ( ContainerInterface $container ) : array {
return array(
'AT',
'BE',
'BG',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'GB',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'SK',
'SI',
'ES',
'SE',
);
},
);

View file

@ -0,0 +1,140 @@
<?php
/**
* The billing agreements endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
/**
* Class BillingAgreementsEndpoint
*/
class BillingAgreementsEndpoint {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* BillingAgreementsEndpoint constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Creates a billing agreement token.
*
* @param string $description The description.
* @param string $return_url The return URL.
* @param string $cancel_url The cancel URL.
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create_token( string $description, string $return_url, string $cancel_url ): stdClass {
$data = array(
'description' => $description,
'payer' => array(
'payment_method' => 'PAYPAL',
),
'plan' => array(
'type' => 'MERCHANT_INITIATED_BILLING',
'merchant_preferences' => array(
'return_url' => $return_url,
'cancel_url' => $cancel_url,
'skip_shipping_address' => true,
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing-agreements/agreement-tokens';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to create a billing agreement token.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json;
}
/**
* Checks if reference transactions are enabled in account.
*
* @throws RuntimeException If the request fails (no auth, no connection, etc.).
*/
public function reference_transaction_enabled(): bool {
try {
$this->is_request_logging_enabled = false;
try {
$this->create_token(
'Checking if reference transactions are enabled',
'https://example.com/return',
'https://example.com/cancel'
);
} finally {
$this->is_request_logging_enabled = true;
}
return true;
} catch ( PayPalApiException $exception ) {
return false;
}
}
}

View file

@ -14,6 +14,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class IdentityToken
@ -44,36 +46,51 @@ class IdentityToken {
private $logger;
/**
* The prefix.
* The settings
*
* @var string
* @var Settings
*/
private $prefix;
private $settings;
/**
* The customer repository.
*
* @var CustomerRepository
*/
protected $customer_repository;
/**
* IdentityToken constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
* @param string $prefix The prefix.
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
* @param Settings $settings The settings.
* @param CustomerRepository $customer_repository The customer repository.
*/
public function __construct( string $host, Bearer $bearer, LoggerInterface $logger, string $prefix ) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
$this->prefix = $prefix;
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger,
Settings $settings,
CustomerRepository $customer_repository
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
$this->settings = $settings;
$this->customer_repository = $customer_repository;
}
/**
* Generates a token for a specific customer.
* Generates a token for a specific user.
*
* @param int $customer_id The id of the customer.
* @param int $user_id The id of the user.
*
* @return Token
* @throws RuntimeException If the request fails.
*/
public function generate_for_customer( int $customer_id ): Token {
public function generate_for_user( int $user_id ): Token {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/identity/generate-token';
@ -84,8 +101,17 @@ class IdentityToken {
'Content-Type' => 'application/json',
),
);
if ( $customer_id && defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION ) {
$args['body'] = wp_json_encode( array( 'customer_id' => $this->prefix . $customer_id ) );
if (
( $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) )
&& defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION
) {
$customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) );
$args['body'] = wp_json_encode(
array(
'customer_id' => $customer_id,
)
);
}
$response = $this->request( $url, $args );

View file

@ -198,14 +198,20 @@ class OrderEndpoint {
return $is_purchase_unit;
}
);
$shipping_preferences = $contains_physical_goods
? $shipping_address_is_fixed ?
ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS
: ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE
: ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING;
if ( $this->has_items_without_shipping( $items ) ) {
$shipping_preferences = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING;
$shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING;
if ( $contains_physical_goods ) {
if ( $shipping_address_is_fixed ) {
// Checkout + no address given? Probably something weird happened, like no form validation?
// Also note that $items currently always seems to be an array with one item.
if ( $this->has_items_without_shipping( $items ) ) {
$shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING;
} else {
$shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS;
}
} else {
$shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE;
}
}
$bearer = $this->bearer->bearer();
@ -218,9 +224,9 @@ class OrderEndpoint {
$items
),
'application_context' => $this->application_context_repository
->current_context( $shipping_preferences )->to_array(),
->current_context( $shipping_preference )->to_array(),
);
if ( $payer ) {
if ( $payer && ! empty( $payer->email_address() ) && ! empty( $payer->name() ) ) {
$data['payer'] = $payer->to_array();
}
if ( $payment_token ) {
@ -229,6 +235,11 @@ class OrderEndpoint {
if ( $payment_method ) {
$data['payment_method'] = $payment_method->to_array();
}
/**
* The filter can be used to modify the order creation request body data.
*/
$data = apply_filters( 'ppcp_create_order_request_body_data', $data );
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array(
'method' => 'POST',
@ -591,7 +602,7 @@ class OrderEndpoint {
/**
* Checks if there is at least one item without shipping.
*
* @param array $items The items.
* @param PurchaseUnit[] $items The items.
* @return bool Whether items contains shipping or not.
*/
private function has_items_without_shipping( array $items ): bool {

View file

@ -12,7 +12,6 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use Psr\Log\LoggerInterface;
/**
@ -36,13 +35,6 @@ class PartnerReferrals {
*/
private $bearer;
/**
* The PartnerReferralsData.
*
* @var PartnerReferralsData
*/
private $data;
/**
* The logger.
*
@ -53,32 +45,29 @@ class PartnerReferrals {
/**
* PartnerReferrals constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param PartnerReferralsData $data The partner referrals data.
* @param LoggerInterface $logger The logger.
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
PartnerReferralsData $data,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->data = $data;
$this->logger = $logger;
}
/**
* Fetch the signup link.
*
* @param array $data The partner referrals data.
* @return string
* @throws RuntimeException If the request fails.
*/
public function signup_link(): string {
$data = $this->data->data();
public function signup_link( array $data ): string {
$bearer = $this->bearer->bearer();
$args = array(
'method' => 'POST',

View file

@ -11,10 +11,14 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentTokenActionLinks;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
/**
* Class PaymentTokenEndpoint
@ -44,41 +48,62 @@ class PaymentTokenEndpoint {
*/
private $factory;
/**
* The PaymentTokenActionLinks factory.
*
* @var PaymentTokenActionLinksFactory
*/
private $payment_token_action_links_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* The prefix.
* The customer repository.
*
* @var string
* @var CustomerRepository
*/
private $prefix;
protected $customer_repository;
/**
* The request id repository.
*
* @var PayPalRequestIdRepository
*/
private $request_id_repository;
/**
* PaymentTokenEndpoint constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param PaymentTokenFactory $factory The payment token factory.
* @param LoggerInterface $logger The logger.
* @param string $prefix The prefix.
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param PaymentTokenFactory $factory The payment token factory.
* @param PaymentTokenActionLinksFactory $payment_token_action_links_factory The PaymentTokenActionLinks factory.
* @param LoggerInterface $logger The logger.
* @param CustomerRepository $customer_repository The customer repository.
* @param PayPalRequestIdRepository $request_id_repository The request id repository.
*/
public function __construct(
string $host,
Bearer $bearer,
PaymentTokenFactory $factory,
PaymentTokenActionLinksFactory $payment_token_action_links_factory,
LoggerInterface $logger,
string $prefix
CustomerRepository $customer_repository,
PayPalRequestIdRepository $request_id_repository
) {
$this->host = $host;
$this->bearer = $bearer;
$this->factory = $factory;
$this->logger = $logger;
$this->prefix = $prefix;
$this->host = $host;
$this->bearer = $bearer;
$this->factory = $factory;
$this->payment_token_action_links_factory = $payment_token_action_links_factory;
$this->logger = $logger;
$this->customer_repository = $customer_repository;
$this->request_id_repository = $request_id_repository;
}
/**
@ -91,10 +116,8 @@ class PaymentTokenEndpoint {
*/
public function for_user( int $id ): array {
$bearer = $this->bearer->bearer();
$customer_id = $this->prefix . $id;
$url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens/?customer_id=' . $customer_id;
$args = array(
$url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens/?customer_id=' . $this->customer_repository->customer_id_for_user( $id );
$args = array(
'method' => 'GET',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
@ -183,4 +206,120 @@ class PaymentTokenEndpoint {
return wp_remote_retrieve_response_code( $response ) === 204;
}
/**
* Starts the process of PayPal account vaulting (without payment), returns the links for further actions.
*
* @param int $user_id The WP user id.
* @param string $return_url The URL to which the customer is redirected after finishing the approval.
* @param string $cancel_url The URL to which the customer is redirected if cancelled the operation.
*
* @return PaymentTokenActionLinks
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function start_paypal_token_creation(
int $user_id,
string $return_url,
string $cancel_url
): PaymentTokenActionLinks {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens';
$customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) );
$data = array(
'customer_id' => $customer_id,
'source' => array(
'paypal' => array(
'usage_type' => 'MERCHANT',
),
),
'application_context' => array(
'return_url' => $return_url,
'cancel_url' => $cancel_url,
// TODO: can use vault_on_approval to avoid /confirm-payment-token, but currently it's not working.
),
);
$request_id = uniqid( 'ppcp-vault', true );
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Request-Id' => $request_id,
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Failed to create payment token.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
$status = $json->status;
if ( 'CUSTOMER_ACTION_REQUIRED' !== $status ) {
throw new RuntimeException( 'Unexpected payment token creation status. ' . $status );
}
$links = $this->payment_token_action_links_factory->from_paypal_response( $json );
$this->request_id_repository->set( "ppcp-vault-{$user_id}", $request_id );
return $links;
}
/**
* Finishes the process of PayPal account vaulting.
*
* @param string $approval_token The id of the approval token approved by the customer.
* @param int $user_id The WP user id.
*
* @return string
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create_from_approval_token( string $approval_token, int $user_id ): string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/vault/approval-tokens/' . $approval_token . '/confirm-payment-token';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Request-Id' => $this->request_id_repository->get( "ppcp-vault-{$user_id}" ),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Failed to create payment token from approval token.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json->id;
}
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -146,22 +147,32 @@ class PaymentsEndpoint {
/**
* Capture an authorization by a given ID.
*
* @param string $authorization_id The id.
* @param string $authorization_id The id.
* @param Money|null $amount The amount to capture. If not specified, the whole authorized amount is captured.
*
* @return Capture
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function capture( string $authorization_id ): Capture {
public function capture( string $authorization_id, ?Money $amount = null ): Capture {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization_id . '/capture';
$args = array(
$data = array(
'final_capture' => true,
);
if ( $amount ) {
$data['amount'] = $amount->to_array();
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $data, JSON_FORCE_OBJECT ),
);
$response = $this->request( $url, $args );

View file

@ -16,6 +16,13 @@ use WP_Error;
*/
trait RequestTrait {
/**
* Whether to log the detailed request/response info.
*
* @var bool
*/
protected $is_request_logging_enabled = true;
/**
* Performs a request
*
@ -39,7 +46,9 @@ trait RequestTrait {
}
$response = wp_remote_get( $url, $args );
$this->logger->debug( $this->request_response_string( $url, $args, $response ) );
if ( $this->is_request_logging_enabled ) {
$this->logger->debug( $this->request_response_string( $url, $args, $response ) );
}
return $response;
}

View file

@ -143,13 +143,15 @@ class Address {
* @return array
*/
public function to_array(): array {
return array(
'country_code' => $this->country_code(),
'address_line_1' => $this->address_line_1(),
'address_line_2' => $this->address_line_2(),
'admin_area_1' => $this->admin_area_1(),
'admin_area_2' => $this->admin_area_2(),
'postal_code' => $this->postal_code(),
return array_filter(
array(
'country_code' => $this->country_code(),
'address_line_1' => $this->address_line_1(),
'address_line_2' => $this->address_line_2(),
'admin_area_1' => $this->admin_area_1(),
'admin_area_2' => $this->admin_area_2(),
'postal_code' => $this->postal_code(),
)
);
}
}

View file

@ -51,6 +51,13 @@ class Capture {
*/
private $seller_protection;
/**
* The detailed breakdown of the capture activity (fees, ...).
*
* @var SellerReceivableBreakdown|null
*/
private $seller_receivable_breakdown;
/**
* The invoice id.
*
@ -68,13 +75,14 @@ class Capture {
/**
* Capture constructor.
*
* @param string $id The ID.
* @param CaptureStatus $status The status.
* @param Amount $amount The amount.
* @param bool $final_capture The final capture.
* @param string $seller_protection The seller protection.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
* @param string $id The ID.
* @param CaptureStatus $status The status.
* @param Amount $amount The amount.
* @param bool $final_capture The final capture.
* @param string $seller_protection The seller protection.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
* @param SellerReceivableBreakdown|null $seller_receivable_breakdown The detailed breakdown of the capture activity (fees, ...).
*/
public function __construct(
string $id,
@ -83,16 +91,18 @@ class Capture {
bool $final_capture,
string $seller_protection,
string $invoice_id,
string $custom_id
string $custom_id,
?SellerReceivableBreakdown $seller_receivable_breakdown
) {
$this->id = $id;
$this->status = $status;
$this->amount = $amount;
$this->final_capture = $final_capture;
$this->seller_protection = $seller_protection;
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
$this->id = $id;
$this->status = $status;
$this->amount = $amount;
$this->final_capture = $final_capture;
$this->seller_protection = $seller_protection;
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
$this->seller_receivable_breakdown = $seller_receivable_breakdown;
}
/**
@ -158,6 +168,15 @@ class Capture {
return $this->custom_id;
}
/**
* Returns the detailed breakdown of the capture activity (fees, ...).
*
* @return SellerReceivableBreakdown|null
*/
public function seller_receivable_breakdown() : ?SellerReceivableBreakdown {
return $this->seller_receivable_breakdown;
}
/**
* Returns the entity as array.
*
@ -177,6 +196,9 @@ class Capture {
if ( $details ) {
$data['status_details'] = array( 'reason' => $details->reason() );
}
if ( $this->seller_receivable_breakdown ) {
$data['seller_receivable_breakdown'] = $this->seller_receivable_breakdown->to_array();
}
return $data;
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* The exchange rate object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class ExchangeRate.
*/
class ExchangeRate {
/**
* The source currency from which to convert an amount.
*
* @var string
*/
private $source_currency;
/**
* The target currency to which to convert an amount.
*
* @var string
*/
private $target_currency;
/**
* The target currency amount. Equivalent to one unit of the source currency.
*
* @var string
*/
private $value;
/**
* ExchangeRate constructor.
*
* @param string $source_currency The source currency from which to convert an amount.
* @param string $target_currency The target currency to which to convert an amount.
* @param string $value The target currency amount. Equivalent to one unit of the source currency.
*/
public function __construct( string $source_currency, string $target_currency, string $value ) {
$this->source_currency = $source_currency;
$this->target_currency = $target_currency;
$this->value = $value;
}
/**
* The source currency from which to convert an amount.
*
* @return string
*/
public function source_currency(): string {
return $this->source_currency;
}
/**
* The target currency to which to convert an amount.
*
* @return string
*/
public function target_currency(): string {
return $this->target_currency;
}
/**
* The target currency amount. Equivalent to one unit of the source currency.
*
* @return string
*/
public function value(): string {
return $this->value;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'source_currency' => $this->source_currency,
'target_currency' => $this->target_currency,
'value' => $this->value,
);
}
}

View file

@ -105,6 +105,9 @@ class PaymentToken {
* @return array
*/
public static function get_valid_types() {
/**
* Returns a list of valid payment token types.
*/
return apply_filters(
'woocommerce_paypal_payments_valid_payment_token_types',
array(

View file

@ -0,0 +1,76 @@
<?php
/**
* The links from CUSTOMER_ACTION_REQUIRED v2/vault/payment-tokens response.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentTokenActionLinks
*/
class PaymentTokenActionLinks {
/**
* The URL for customer PayPal hosted contingency flow.
*
* @var string
*/
private $approve_link;
/**
* The URL for a POST request to save an approved approval token and vault the underlying instrument.
*
* @var string
*/
private $confirm_link;
/**
* The URL for a GET request to get the state of the approval token.
*
* @var string
*/
private $status_link;
/**
* PaymentTokenActionLinks constructor.
*
* @param string $approve_link The URL for customer PayPal hosted contingency flow.
* @param string $confirm_link The URL for a POST request to save an approved approval token and vault the underlying instrument.
* @param string $status_link The URL for a GET request to get the state of the approval token.
*/
public function __construct( string $approve_link, string $confirm_link, string $status_link ) {
$this->approve_link = $approve_link;
$this->confirm_link = $confirm_link;
$this->status_link = $status_link;
}
/**
* Returns the URL for customer PayPal hosted contingency flow.
*
* @return string
*/
public function approve_link(): string {
return $this->approve_link;
}
/**
* Returns the URL for a POST request to save an approved approval token and vault the underlying instrument.
*
* @return string
*/
public function confirm_link(): string {
return $this->confirm_link;
}
/**
* Returns the URL for a GET request to get the state of the approval token.
*
* @return string
*/
public function status_link(): string {
return $this->status_link;
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* The platform fee object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PlatformFee.
*/
class PlatformFee {
/**
* The fee.
*
* @var Money
*/
private $amount;
/**
* The recipient of the fee.
*
* @var Payee|null
*/
private $payee;
/**
* PlatformFee constructor.
*
* @param Money $amount The fee.
* @param Payee|null $payee The recipient of the fee.
*/
public function __construct( Money $amount, ?Payee $payee ) {
$this->amount = $amount;
$this->payee = $payee;
}
/**
* The fee.
*
* @return Money
*/
public function amount(): Money {
return $this->amount;
}
/**
* The recipient of the fee.
*
* @return Payee|null
*/
public function payee(): ?Payee {
return $this->payee;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
$data = array(
'amount' => $this->amount->to_array(),
);
if ( $this->payee ) {
$data['payee'] = $this->payee->to_array();
}
return $data;
}
}

View file

@ -157,6 +157,15 @@ class PurchaseUnit {
return $this->amount;
}
/**
* Sets the amount.
*
* @param Amount $amount The value to set.
*/
public function set_amount( Amount $amount ): void {
$this->amount = $amount;
}
/**
* Returns the shipping.
*

View file

@ -0,0 +1,211 @@
<?php
/**
* The info about fees and amount that will be received by the seller.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class SellerReceivableBreakdown
*/
class SellerReceivableBreakdown {
/**
* The amount for this captured payment in the currency of the transaction.
*
* @var Money
*/
private $gross_amount;
/**
* The applicable fee for this captured payment in the currency of the transaction.
*
* @var Money|null
*/
private $paypal_fee;
/**
* The applicable fee for this captured payment in the receivable currency.
*
* Present only in cases the fee is charged in the receivable currency.
*
* @var Money|null
*/
private $paypal_fee_in_receivable_currency;
/**
* The net amount that the payee receives for this captured payment in their PayPal account.
*
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
*
* @var Money|null
*/
private $net_amount;
/**
* The net amount that is credited to the payee's PayPal account.
*
* Present only when the currency of the captured payment is different from the currency
* of the PayPal account where the payee wants to credit the funds. Computed as net_amount times exchange_rate.
*
* @var Money|null
*/
private $receivable_amount;
/**
* The exchange rate that determines the amount that is credited to the payee's PayPal account.
*
* Present when the currency of the captured payment is different from the currency of the PayPal account where the payee wants to credit the funds.
*
* @var ExchangeRate|null
*/
private $exchange_rate;
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*
* @var PlatformFee[]
*/
private $platform_fees;
/**
* SellerReceivableBreakdown constructor.
*
* @param Money $gross_amount The amount for this captured payment in the currency of the transaction.
* @param Money|null $paypal_fee The applicable fee for this captured payment in the currency of the transaction.
* @param Money|null $paypal_fee_in_receivable_currency The applicable fee for this captured payment in the receivable currency.
* @param Money|null $net_amount The net amount that the payee receives for this captured payment in their PayPal account.
* @param Money|null $receivable_amount The net amount that is credited to the payee's PayPal account.
* @param ExchangeRate|null $exchange_rate The exchange rate that determines the amount that is credited to the payee's PayPal account.
* @param PlatformFee[] $platform_fees An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*/
public function __construct(
Money $gross_amount,
?Money $paypal_fee,
?Money $paypal_fee_in_receivable_currency,
?Money $net_amount,
?Money $receivable_amount,
?ExchangeRate $exchange_rate,
array $platform_fees
) {
$this->gross_amount = $gross_amount;
$this->paypal_fee = $paypal_fee;
$this->paypal_fee_in_receivable_currency = $paypal_fee_in_receivable_currency;
$this->net_amount = $net_amount;
$this->receivable_amount = $receivable_amount;
$this->exchange_rate = $exchange_rate;
$this->platform_fees = $platform_fees;
}
/**
* The amount for this captured payment in the currency of the transaction.
*
* @return Money
*/
public function gross_amount(): ?Money {
return $this->gross_amount;
}
/**
* The applicable fee for this captured payment in the currency of the transaction.
*
* @return Money|null
*/
public function paypal_fee(): ?Money {
return $this->paypal_fee;
}
/**
* The applicable fee for this captured payment in the receivable currency.
*
* Present only in cases the fee is charged in the receivable currency.
*
* @return Money|null
*/
public function paypal_fee_in_receivable_currency(): ?Money {
return $this->paypal_fee_in_receivable_currency;
}
/**
* The net amount that the payee receives for this captured payment in their PayPal account.
*
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
*
* @return Money|null
*/
public function net_amount(): ?Money {
return $this->net_amount;
}
/**
* The net amount that is credited to the payee's PayPal account.
*
* Present only when the currency of the captured payment is different from the currency
* of the PayPal account where the payee wants to credit the funds. Computed as net_amount times exchange_rate.
*
* @return Money|null
*/
public function receivable_amount(): ?Money {
return $this->receivable_amount;
}
/**
* The exchange rate that determines the amount that is credited to the payee's PayPal account.
*
* Present when the currency of the captured payment is different from the currency of the PayPal account where the payee wants to credit the funds.
*
* @return ExchangeRate|null
*/
public function exchange_rate(): ?ExchangeRate {
return $this->exchange_rate;
}
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*
* @return PlatformFee[]
*/
public function platform_fees(): array {
return $this->platform_fees;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
$data = array(
'gross_amount' => $this->gross_amount->to_array(),
);
if ( $this->paypal_fee ) {
$data['paypal_fee'] = $this->paypal_fee->to_array();
}
if ( $this->paypal_fee_in_receivable_currency ) {
$data['paypal_fee_in_receivable_currency'] = $this->paypal_fee_in_receivable_currency->to_array();
}
if ( $this->net_amount ) {
$data['net_amount'] = $this->net_amount->to_array();
}
if ( $this->receivable_amount ) {
$data['receivable_amount'] = $this->receivable_amount->to_array();
}
if ( $this->exchange_rate ) {
$data['exchange_rate'] = $this->exchange_rate->to_array();
}
if ( $this->platform_fees ) {
$data['platform_fees'] = array_map(
function ( PlatformFee $fee ) {
return $fee->to_array();
},
$this->platform_fees
);
}
return $data;
}
}

View file

@ -110,4 +110,13 @@ class PayPalApiException extends RuntimeException {
}
return false;
}
/**
* Returns response issues.
*
* @return array
*/
public function issues(): array {
return $this->response->issues ?? array();
}
}

View file

@ -14,12 +14,15 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AmountBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
/**
* Class AmountFactory
*/
class AmountFactory {
use FreeTrialHandlerTrait;
/**
* The item factory.
@ -28,13 +31,31 @@ class AmountFactory {
*/
private $item_factory;
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* AmountFactory constructor.
*
* @param ItemFactory $item_factory The Item factory.
* @param ItemFactory $item_factory The Item factory.
* @param MoneyFactory $money_factory The Money factory.
* @param string $currency 3-letter currency code of the shop.
*/
public function __construct( ItemFactory $item_factory ) {
$this->item_factory = $item_factory;
public function __construct( ItemFactory $item_factory, MoneyFactory $money_factory, string $currency ) {
$this->item_factory = $item_factory;
$this->money_factory = $money_factory;
$this->currency = $currency;
}
/**
@ -45,8 +66,7 @@ class AmountFactory {
* @return Amount
*/
public function from_wc_cart( \WC_Cart $cart ): Amount {
$currency = get_woocommerce_currency();
$total = new Money( (float) $cart->get_total( 'numeric' ), $currency );
$total = new Money( (float) $cart->get_total( 'numeric' ), $this->currency );
$total_fees_amount = 0;
$fees = WC()->session->get( 'ppcp_fees' );
@ -57,22 +77,22 @@ class AmountFactory {
}
$item_total = $cart->get_cart_contents_total() + $cart->get_discount_total() + $total_fees_amount;
$item_total = new Money( (float) $item_total, $currency );
$item_total = new Money( (float) $item_total, $this->currency );
$shipping = new Money(
(float) $cart->get_shipping_total() + $cart->get_shipping_tax(),
$currency
$this->currency
);
$taxes = new Money(
(float) $cart->get_cart_contents_tax() + (float) $cart->get_discount_tax(),
$currency
$cart->get_subtotal_tax(),
$this->currency
);
$discount = null;
if ( $cart->get_discount_total() ) {
$discount = new Money(
(float) $cart->get_discount_total() + $cart->get_discount_tax(),
$currency
$this->currency
);
}
@ -100,9 +120,15 @@ class AmountFactory {
* @return Amount
*/
public function from_wc_order( \WC_Order $order ): Amount {
$currency = $order->get_currency();
$items = $this->item_factory->from_wc_order( $order );
$total = new Money( (float) $order->get_total(), $currency );
$currency = $order->get_currency();
$items = $this->item_factory->from_wc_order( $order );
$total_value = (float) $order->get_total();
if ( CreditCardGateway::ID === $order->get_payment_method() && $this->is_free_trial_order( $order ) ) {
$total_value = 1.0;
}
$total = new Money( $total_value, $currency );
$item_total = new Money(
(float) array_reduce(
$items,
@ -161,16 +187,7 @@ class AmountFactory {
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( \stdClass $data ): Amount {
if ( ! isset( $data->value ) || ! is_numeric( $data->value ) ) {
throw new RuntimeException( __( 'No value given', 'woocommerce-paypal-payments' ) );
}
if ( ! isset( $data->currency_code ) ) {
throw new RuntimeException(
__( 'No currency given', 'woocommerce-paypal-payments' )
);
}
$money = new Money( (float) $data->value, $data->currency_code );
$money = $this->money_factory->from_paypal_response( $data );
$breakdown = ( isset( $data->breakdown ) ) ? $this->break_down( $data->breakdown ) : null;
return new Amount( $money, $breakdown );
}

View file

@ -25,14 +25,26 @@ class CaptureFactory {
*/
private $amount_factory;
/**
* The SellerReceivableBreakdown factory.
*
* @var SellerReceivableBreakdownFactory
*/
private $seller_receivable_breakdown_factory;
/**
* CaptureFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
* @param AmountFactory $amount_factory The amount factory.
* @param SellerReceivableBreakdownFactory $seller_receivable_breakdown_factory The SellerReceivableBreakdown factory.
*/
public function __construct( AmountFactory $amount_factory ) {
public function __construct(
AmountFactory $amount_factory,
SellerReceivableBreakdownFactory $seller_receivable_breakdown_factory
) {
$this->amount_factory = $amount_factory;
$this->amount_factory = $amount_factory;
$this->seller_receivable_breakdown_factory = $seller_receivable_breakdown_factory;
}
/**
@ -44,7 +56,10 @@ class CaptureFactory {
*/
public function from_paypal_response( \stdClass $data ) : Capture {
$reason = $data->status_details->reason ?? null;
$reason = $data->status_details->reason ?? null;
$seller_receivable_breakdown = isset( $data->seller_receivable_breakdown ) ?
$this->seller_receivable_breakdown_factory->from_paypal_response( $data->seller_receivable_breakdown )
: null;
return new Capture(
(string) $data->id,
@ -56,7 +71,8 @@ class CaptureFactory {
(bool) $data->final_capture,
(string) $data->seller_protection->status,
(string) $data->invoice_id,
(string) $data->custom_id
(string) $data->custom_id,
$seller_receivable_breakdown
);
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* The ExchangeRateFactory Factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ExchangeRate;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ExchangeRateFactory
*/
class ExchangeRateFactory {
/**
* Returns an ExchangeRate object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return ExchangeRate|null
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): ?ExchangeRate {
// Looks like all fields in this object are optional, according to the docs,
// and sometimes we get an empty object.
$source_currency = $data->source_currency ?? '';
$target_currency = $data->target_currency ?? '';
$value = $data->value ?? '';
if ( ! $source_currency && ! $target_currency && ! $value ) {
// Do not return empty object.
return null;
}
return new ExchangeRate( $source_currency, $target_currency, $value );
}
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -17,7 +18,21 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
* Class ItemFactory
*/
class ItemFactory {
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* ItemFactory constructor.
*
* @param string $currency 3-letter currency code of the shop.
*/
public function __construct( string $currency ) {
$this->currency = $currency;
}
/**
* Creates items based off a WooCommerce cart.
@ -27,9 +42,8 @@ class ItemFactory {
* @return Item[]
*/
public function from_wc_cart( \WC_Cart $cart ): array {
$currency = get_woocommerce_currency();
$items = array_map(
static function ( array $item ) use ( $currency ): Item {
$items = array_map(
function ( array $item ): Item {
$product = $item['data'];
/**
@ -43,12 +57,12 @@ class ItemFactory {
$price_without_tax = (float) wc_get_price_excluding_tax( $product );
$price_without_tax_rounded = round( $price_without_tax, 2 );
$tax = round( $price - $price_without_tax_rounded, 2 );
$tax = new Money( $tax, $currency );
$tax = new Money( $tax, $this->currency );
return new Item(
mb_substr( $product->get_name(), 0, 127 ),
new Money( $price_without_tax_rounded, $currency ),
new Money( $price_without_tax_rounded, $this->currency ),
$quantity,
mb_substr( wp_strip_all_tags( $product->get_description() ), 0, 127 ),
substr( wp_strip_all_tags( $product->get_description() ), 0, 127 ) ?: '',
$tax,
$product->get_sku(),
( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS
@ -61,13 +75,13 @@ class ItemFactory {
$fees_from_session = WC()->session->get( 'ppcp_fees' );
if ( $fees_from_session ) {
$fees = array_map(
static function ( \stdClass $fee ) use ( $currency ): Item {
function ( \stdClass $fee ): Item {
return new Item(
$fee->name,
new Money( (float) $fee->amount, $currency ),
new Money( (float) $fee->amount, $this->currency ),
1,
'',
new Money( (float) $fee->tax, $currency )
new Money( (float) $fee->tax, $this->currency )
);
},
$fees_from_session
@ -110,21 +124,20 @@ class ItemFactory {
* @return Item
*/
private function from_wc_order_line_item( \WC_Order_Item_Product $item, \WC_Order $order ): Item {
$currency = $order->get_currency();
$product = $item->get_product();
/**
* The WooCommerce product.
*
* @var \WC_Product $product
* @var WC_Product $product
*/
$quantity = (int) $item->get_quantity();
$product = $item->get_product();
$currency = $order->get_currency();
$quantity = (int) $item->get_quantity();
$price = (float) $order->get_item_subtotal( $item, true );
$price_without_tax = (float) $order->get_item_subtotal( $item, false );
$price_without_tax_rounded = round( $price_without_tax, 2 );
$tax = round( $price - $price_without_tax_rounded, 2 );
$tax = new Money( $tax, $currency );
return new Item(
mb_substr( $product->get_name(), 0, 127 ),
new Money( $price_without_tax_rounded, $currency ),

View file

@ -0,0 +1,39 @@
<?php
/**
* The Money factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class MoneyFactory
*/
class MoneyFactory {
/**
* Returns a Money object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return Money
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): Money {
if ( ! isset( $data->value ) || ! is_numeric( $data->value ) ) {
throw new RuntimeException( 'No money value given' );
}
if ( ! isset( $data->currency_code ) ) {
throw new RuntimeException( 'No currency given' );
}
return new Money( (float) $data->value, $data->currency_code );
}
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerName;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerTaxInfo;
@ -54,10 +55,12 @@ class PayerFactory {
$national_number = preg_replace( '/[^0-9]/', '', $national_number );
$national_number = substr( $national_number, 0, 14 );
$phone = new PhoneWithType(
'HOME',
new Phone( $national_number )
);
if ( $national_number ) {
$phone = new PhoneWithType(
'HOME',
new Phone( $national_number )
);
}
}
return new Payer(
new PayerName(
@ -90,10 +93,12 @@ class PayerFactory {
$national_number = preg_replace( '/[^0-9]/', '', $national_number );
$national_number = substr( $national_number, 0, 14 );
$phone = new PhoneWithType(
'HOME',
new Phone( $national_number )
);
if ( $national_number ) {
$phone = new PhoneWithType(
'HOME',
new Phone( $national_number )
);
}
}
return new Payer(
new PayerName(
@ -147,4 +152,57 @@ class PayerFactory {
$tax_info
);
}
/**
* Returns a Payer object based off the given checkout form fields.
*
* @param array $form_fields The checkout form fields.
* @return Payer
*/
public function from_checkout_form( array $form_fields ): Payer {
$first_name = $form_fields['billing_first_name'] ?? '';
$last_name = $form_fields['billing_last_name'] ?? '';
$billing_email = $form_fields['billing_email'] ?? '';
$billing_country = $form_fields['billing_country'] ?? '';
$billing_address_1 = $form_fields['billing_address_1'] ?? '';
$billing_address_2 = $form_fields['billing_address_2'] ?? '';
$admin_area_1 = $form_fields['billing_state'] ?? '';
$admin_area_2 = $form_fields['billing_city'] ?? '';
$billing_postcode = $form_fields['billing_postcode'] ?? '';
$phone = null;
if ( isset( $form_fields['billing_phone'] ) && '' !== $form_fields['billing_phone'] ) {
// make sure the phone number contains only numbers and is max 14. chars long.
$national_number = $form_fields['billing_phone'];
$national_number = preg_replace( '/[^0-9]/', '', $national_number );
if ( null !== $national_number ) {
$national_number = substr( $national_number, 0, 14 );
if ( $national_number ) {
$phone = new PhoneWithType(
'HOME',
new Phone( $national_number )
);
}
}
}
return new Payer(
new PayerName( $first_name, $last_name ),
$billing_email,
'',
new Address(
$billing_country,
$billing_address_1,
$billing_address_2,
$admin_area_1,
$admin_area_2,
$billing_postcode
),
null,
$phone
);
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* The factory for links from CUSTOMER_ACTION_REQUIRED v2/vault/payment-tokens response.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentTokenActionLinks;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class PaymentTokenActionLinksFactory
*/
class PaymentTokenActionLinksFactory {
/**
* Returns a PaymentTokenActionLinks object based off a PayPal response.
*
* @param stdClass $data The JSON object.
*
* @return PaymentTokenActionLinks
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): PaymentTokenActionLinks {
if ( ! isset( $data->links ) ) {
throw new RuntimeException( 'Links not found.' );
}
$links_map = array();
foreach ( $data->links as $link ) {
if ( ! isset( $link->rel ) || ! isset( $link->href ) ) {
throw new RuntimeException( 'Invalid link data.' );
}
$links_map[ $link->rel ] = $link->href;
}
if ( ! array_key_exists( 'approve', $links_map ) ) {
throw new RuntimeException( 'Payment token approve link not found.' );
}
return new PaymentTokenActionLinks(
$links_map['approve'],
$links_map['confirm'] ?? '',
$links_map['status'] ?? ''
);
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* The PlatformFee Factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PlatformFee;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class PayeeFactory
*/
class PlatformFeeFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* The Payee factory.
*
* @var PayeeFactory
*/
private $payee_factory;
/**
* PlatformFeeFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
* @param PayeeFactory $payee_factory The Payee factory.
*/
public function __construct( MoneyFactory $money_factory, PayeeFactory $payee_factory ) {
$this->money_factory = $money_factory;
$this->payee_factory = $payee_factory;
}
/**
* Returns a Payee object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return PlatformFee
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): PlatformFee {
if ( ! isset( $data->amount ) ) {
throw new RuntimeException( 'Platform fee amount not found' );
}
$amount = $this->money_factory->from_paypal_response( $data->amount );
$payee = ( isset( $data->payee ) ) ? $this->payee_factory->from_paypal_response( $data->payee ) : null;
return new PlatformFee( $amount, $payee );
}
}

View file

@ -112,18 +112,18 @@ class PurchaseUnitFactory {
if (
! $this->shipping_needed( ... array_values( $items ) ) ||
empty( $shipping->address()->country_code() ) ||
( $shipping->address()->country_code() && ! $shipping->address()->postal_code() )
( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) )
) {
$shipping = null;
}
$reference_id = 'default';
$description = '';
$payee = $this->payee_repository->payee();
$wc_order_id = $order->get_order_number();
$custom_id = $this->prefix . $wc_order_id;
$invoice_id = $this->prefix . $wc_order_id;
$custom_id = (string) $order->get_id();
$invoice_id = $this->prefix . $order->get_order_number();
$soft_descriptor = '';
$purchase_unit = new PurchaseUnit(
$purchase_unit = new PurchaseUnit(
$amount,
$items,
$shipping,
@ -134,6 +134,9 @@ class PurchaseUnitFactory {
$invoice_id,
$soft_descriptor
);
/**
* Returns PurchaseUnit for the WC order.
*/
return apply_filters(
'woocommerce_paypal_payments_purchase_unit_from_wc_order',
$purchase_unit,
@ -157,9 +160,8 @@ class PurchaseUnitFactory {
if ( $this->shipping_needed( ... array_values( $items ) ) && is_a( $customer, \WC_Customer::class ) ) {
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer );
if (
2 !== strlen( $shipping->address()->country_code() )
|| ( ! $shipping->address()->postal_code() )
|| $this->country_without_postal_code( $shipping->address()->country_code() )
2 !== strlen( $shipping->address()->country_code() ) ||
( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) )
) {
$shipping = null;
}
@ -275,9 +277,6 @@ class PurchaseUnitFactory {
*/
private function country_without_postal_code( string $country_code ): bool {
$countries = array( 'AE', 'AF', 'AG', 'AI', 'AL', 'AN', 'AO', 'AW', 'BB', 'BF', 'BH', 'BI', 'BJ', 'BM', 'BO', 'BS', 'BT', 'BW', 'BZ', 'CD', 'CF', 'CG', 'CI', 'CK', 'CL', 'CM', 'CO', 'CR', 'CV', 'DJ', 'DM', 'DO', 'EC', 'EG', 'ER', 'ET', 'FJ', 'FK', 'GA', 'GD', 'GH', 'GI', 'GM', 'GN', 'GQ', 'GT', 'GW', 'GY', 'HK', 'HN', 'HT', 'IE', 'IQ', 'IR', 'JM', 'JO', 'KE', 'KH', 'KI', 'KM', 'KN', 'KP', 'KW', 'KY', 'LA', 'LB', 'LC', 'LK', 'LR', 'LS', 'LY', 'ML', 'MM', 'MO', 'MR', 'MS', 'MT', 'MU', 'MW', 'MZ', 'NA', 'NE', 'NG', 'NI', 'NP', 'NR', 'NU', 'OM', 'PA', 'PE', 'PF', 'PY', 'QA', 'RW', 'SA', 'SB', 'SC', 'SD', 'SL', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SY', 'TC', 'TD', 'TG', 'TL', 'TO', 'TT', 'TV', 'TZ', 'UG', 'UY', 'VC', 'VE', 'VG', 'VN', 'VU', 'WS', 'XA', 'XB', 'XC', 'XE', 'XL', 'XM', 'XN', 'XS', 'YE', 'ZM', 'ZW' );
if ( in_array( $country_code, $countries, true ) ) {
return true;
}
return false;
return in_array( $country_code, $countries, true );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* The SellerReceivableBreakdown Factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PlatformFee;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerReceivableBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class SellerReceivableBreakdownFactory
*/
class SellerReceivableBreakdownFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* The ExchangeRate factory.
*
* @var ExchangeRateFactory
*/
private $exchange_rate_factory;
/**
* The PlatformFee factory.
*
* @var PlatformFeeFactory
*/
private $platform_fee_factory;
/**
* SellerReceivableBreakdownFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
* @param ExchangeRateFactory $exchange_rate_factory The ExchangeRate factory.
* @param PlatformFeeFactory $platform_fee_factory The PlatformFee factory.
*/
public function __construct(
MoneyFactory $money_factory,
ExchangeRateFactory $exchange_rate_factory,
PlatformFeeFactory $platform_fee_factory
) {
$this->money_factory = $money_factory;
$this->exchange_rate_factory = $exchange_rate_factory;
$this->platform_fee_factory = $platform_fee_factory;
}
/**
* Returns a SellerReceivableBreakdown object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return SellerReceivableBreakdown
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): SellerReceivableBreakdown {
if ( ! isset( $data->gross_amount ) ) {
throw new RuntimeException( 'Seller breakdown gross amount not found' );
}
$gross_amount = $this->money_factory->from_paypal_response( $data->gross_amount );
$paypal_fee = ( isset( $data->paypal_fee ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee ) : null;
$paypal_fee_in_receivable_currency = ( isset( $data->paypal_fee_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee_in_receivable_currency ) : null;
$net_amount = ( isset( $data->net_amount ) ) ? $this->money_factory->from_paypal_response( $data->net_amount ) : null;
$receivable_amount = ( isset( $data->receivable_amount ) ) ? $this->money_factory->from_paypal_response( $data->receivable_amount ) : null;
$exchange_rate = ( isset( $data->exchange_rate ) ) ? $this->exchange_rate_factory->from_paypal_response( $data->exchange_rate ) : null;
$platform_fees = ( isset( $data->platform_fees ) ) ? array_map(
function ( stdClass $fee_data ): PlatformFee {
return $this->platform_fee_factory->from_paypal_response( $fee_data );
},
$data->platform_fees
) : array();
return new SellerReceivableBreakdown(
$gross_amount,
$paypal_fee,
$paypal_fee_in_receivable_currency,
$net_amount,
$receivable_amount,
$exchange_rate,
$platform_fees
);
}
}

View file

@ -19,169 +19,50 @@ class DccApplies {
*
* @var array
*/
private $allowed_country_currency_matrix = array(
'AU' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'ES' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'FR' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'GB' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'IT' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'US' => array(
'AUD',
'CAD',
'EUR',
'GBP',
'JPY',
'USD',
),
'CA' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
);
private $allowed_country_currency_matrix;
/**
* Which countries support which credit cards. Empty credit card arrays mean no restriction on
* currency. Otherwise only the currencies in the array are supported.
* currency.
*
* @var array
*/
private $country_card_matrix = array(
'AU' => array(
'mastercard' => array(),
'visa' => array(),
),
'ES' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'FR' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'GB' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'GBP', 'USD' ),
),
'IT' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'US' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'USD' ),
'discover' => array( 'USD' ),
),
'CA' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'CAD' ),
'jcb' => array( 'CAD' ),
),
);
private $country_card_matrix;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* DccApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
* @param array $country_card_matrix Which countries support which credit cards. Empty credit card arrays mean no restriction on
* currency.
* @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 $country_card_matrix,
string $currency,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->country_card_matrix = $country_card_matrix;
$this->currency = $currency;
$this->country = $country;
}
/**
* Returns whether DCC can be used in the current country and the current currency used.
@ -189,12 +70,10 @@ class DccApplies {
* @return bool
*/
public function for_country_currency(): bool {
$country = $this->country();
$currency = get_woocommerce_currency();
if ( ! in_array( $country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
$applies = in_array( $currency, $this->allowed_country_currency_matrix[ $country ], true );
$applies = in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
return $applies;
}
@ -204,13 +83,12 @@ class DccApplies {
* @return array
*/
public function valid_cards() : array {
$country = $this->country();
$cards = array();
if ( ! isset( $this->country_card_matrix[ $country ] ) ) {
$cards = array();
if ( ! isset( $this->country_card_matrix[ $this->country ] ) ) {
return $cards;
}
$supported_currencies = $this->country_card_matrix[ $country ];
$supported_currencies = $this->country_card_matrix[ $this->country ];
foreach ( $supported_currencies as $card => $currencies ) {
if ( $this->can_process_card( $card ) ) {
$cards[] = $card;
@ -233,11 +111,10 @@ class DccApplies {
* @return bool
*/
public function can_process_card( string $card ) : bool {
$country = $this->country();
if ( ! isset( $this->country_card_matrix[ $country ] ) ) {
if ( ! isset( $this->country_card_matrix[ $this->country ] ) ) {
return false;
}
if ( ! isset( $this->country_card_matrix[ $country ][ $card ] ) ) {
if ( ! isset( $this->country_card_matrix[ $this->country ][ $card ] ) ) {
return false;
}
@ -245,19 +122,7 @@ class DccApplies {
* If the supported currencies array is empty, there are no
* restrictions, which currencies are supported by a card.
*/
$supported_currencies = $this->country_card_matrix[ $country ][ $card ];
$currency = get_woocommerce_currency();
return empty( $supported_currencies ) || in_array( $currency, $supported_currencies, true );
}
/**
* Returns the country code of the shop.
*
* @return string
*/
private function country() : string {
$region = wc_get_base_location();
$country = $region['country'];
return $country;
$supported_currencies = $this->country_card_matrix[ $this->country ][ $card ];
return empty( $supported_currencies ) || in_array( $this->currency, $supported_currencies, true );
}
}

View file

@ -50,7 +50,7 @@ class ApplicationContextRepository {
$landingpage = $this->settings->has( 'landing_page' ) ?
$this->settings->get( 'landing_page' ) : ApplicationContext::LANDING_PAGE_NO_PREFERENCE;
$context = new ApplicationContext(
(string) home_url( \WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ),
network_home_url( \WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ),
(string) wc_get_checkout_url(),
(string) $brand_name,
$locale,
@ -74,7 +74,10 @@ class ApplicationContextRepository {
$parts = explode( '-', $locale );
if ( count( $parts ) === 3 ) {
return substr( $locale, 0, strrpos( $locale, '-' ) );
$ret = substr( $locale, 0, strrpos( $locale, '-' ) );
if ( false !== $ret ) {
return $ret;
}
}
return 'en';

View file

@ -0,0 +1,62 @@
<?php
/**
* The customer repository.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Repository
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
/**
* Class CustomerRepository
*/
class CustomerRepository {
const CLIENT_ID_MAX_LENGTH = 22;
/**
* The prefix.
*
* @var string
*/
protected $prefix;
/**
* CustomerRepository constructor.
*
* @param string $prefix The prefix.
*/
public function __construct( string $prefix ) {
$this->prefix = $prefix;
}
/**
* Returns the customer ID for the given user ID.
*
* @param int $user_id The user ID.
* @return string
*/
public function customer_id_for_user( int $user_id ): string {
if ( 0 === $user_id ) {
$guest_customer_id = WC()->session->get( 'ppcp_guest_customer_id' );
if ( is_string( $guest_customer_id ) && $guest_customer_id ) {
return $guest_customer_id;
}
$unique_id = substr( $this->prefix . strrev( uniqid() ), 0, self::CLIENT_ID_MAX_LENGTH );
assert( is_string( $unique_id ) );
WC()->session->set( 'ppcp_guest_customer_id', $unique_id );
return $unique_id;
}
$guest_customer_id = get_user_meta( $user_id, 'ppcp_guest_customer_id', true );
if ( $guest_customer_id ) {
return $guest_customer_id;
}
return $this->prefix . (string) $user_id;
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* PayPal order repository.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Repository
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class OrderRepository
*/
class OrderRepository {
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
protected $order_endpoint;
/**
* OrderRepository constructor.
*
* @param OrderEndpoint $order_endpoint The order endpoint.
*/
public function __construct( OrderEndpoint $order_endpoint ) {
$this->order_endpoint = $order_endpoint;
}
/**
* Gets a PayPal order for the given WooCommerce order.
*
* @param WC_Order $wc_order The WooCommerce order.
* @return Order The PayPal order.
* @throws RuntimeException When there is a problem getting the PayPal order.
*/
public function for_wc_order( WC_Order $wc_order ): Order {
$paypal_order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $paypal_order_id ) {
throw new RuntimeException( 'PayPal order ID not found in meta.' );
}
return $this->order_endpoint->order( $paypal_order_id );
}
}

View file

@ -15,14 +15,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
* Class PartnerReferralsData
*/
class PartnerReferralsData {
/**
* The merchant email.
*
* @var string
*/
private $merchant_email;
/**
* The DCC Applies Helper object.
*
@ -30,19 +22,39 @@ class PartnerReferralsData {
*/
private $dcc_applies;
/**
* The list of products ('PPCP', 'EXPRESS_CHECKOUT').
*
* @var string[]
*/
private $products;
/**
* PartnerReferralsData constructor.
*
* @param string $merchant_email The email of the merchant.
* @param DccApplies $dcc_applies The DCC Applies helper.
*/
public function __construct(
string $merchant_email,
DccApplies $dcc_applies
) {
$this->dcc_applies = $dcc_applies;
$this->products = array(
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
);
}
$this->merchant_email = $merchant_email;
$this->dcc_applies = $dcc_applies;
/**
* Returns a new copy of this object with the given value set.
*
* @param string[] $products The list of products ('PPCP', 'EXPRESS_CHECKOUT').
* @return static
*/
public function with_products( array $products ): self {
$obj = clone $this;
$obj->products = $products;
return $obj;
}
/**
@ -60,33 +72,26 @@ class PartnerReferralsData {
* @return array
*/
public function data(): array {
$data = $this->default_data();
return $data;
}
/**
* Returns the default data.
*
* @return array
*/
private function default_data(): array {
return array(
'partner_config_override' => array(
'partner_logo_url' => 'https://connect.woocommerce.com/images/woocommerce_logo.png',
/**
* Returns the URL which will be opened at the end of onboarding.
*/
'return_url' => apply_filters(
'woocommerce_paypal_payments_partner_config_override_return_url',
admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' )
),
/**
* Returns the description of the URL which will be opened at the end of onboarding.
*/
'return_url_description' => apply_filters(
'woocommerce_paypal_payments_partner_config_override_return_url_description',
__( 'Return to your shop.', 'woocommerce-paypal-payments' )
),
'show_add_credit_card' => true,
),
'products' => array(
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
),
'products' => $this->products,
'legal_consents' => array(
array(
'type' => 'SHARE_DATA_CONSENT',

View file

@ -26,8 +26,7 @@ class PayPalRequestIdRepository {
* @return string
*/
public function get_for_order_id( string $order_id ): string {
$all = $this->all();
return isset( $all[ $order_id ] ) ? (string) $all[ $order_id ]['id'] : '';
return $this->get( $order_id );
}
/**
@ -50,16 +49,39 @@ class PayPalRequestIdRepository {
* @return bool
*/
public function set_for_order( Order $order, string $request_id ): bool {
$all = $this->all();
$all[ $order->id() ] = array(
'id' => $request_id,
'expiration' => time() + 10 * DAY_IN_SECONDS,
);
$all = $this->cleanup( $all );
update_option( self::KEY, $all );
$this->set( $order->id(), $request_id );
return true;
}
/**
* Sets a request ID for the given key.
*
* @param string $key The key in the request ID storage.
* @param string $request_id The ID.
*/
public function set( string $key, string $request_id ): void {
$all = $this->all();
$day_in_seconds = 86400;
$all[ $key ] = array(
'id' => $request_id,
'expiration' => time() + 10 * $day_in_seconds,
);
$all = $this->cleanup( $all );
update_option( self::KEY, $all );
}
/**
* Returns a request ID.
*
* @param string $key The key in the request ID storage.
*
* @return string
*/
public function get( string $key ): string {
$all = $this->all();
return isset( $all[ $key ] ) ? (string) $all[ $key ]['id'] : '';
}
/**
* Return all IDs.
*

View file

@ -11,3 +11,7 @@
.ppcp-credit-card-gateway-form-field-disabled {
opacity: .5 !important;
}
.ppcp-dcc-order-button {
float: right;
}

View file

@ -9,12 +9,38 @@ import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer";
import dataClientIdAttributeHandler from "./modules/DataClientIdAttributeHandler";
import MessageRenderer from "./modules/Renderer/MessageRenderer";
import Spinner from "./modules/Helper/Spinner";
import {
getCurrentPaymentMethod,
ORDER_BUTTON_SELECTOR,
PaymentMethods
} from "./modules/Helper/CheckoutMethodState";
import {hide, setVisible} from "./modules/Helper/Hiding";
import {isChangePaymentPage} from "./modules/Helper/Subscriptions";
import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
const buttonsSpinner = new Spinner('.ppc-button-wrapper');
const cardsSpinner = new Spinner('#ppcp-hosted-fields');
const bootstrap = () => {
const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic);
const spinner = new Spinner();
const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler, spinner);
const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway);
const freeTrialHandler = new FreeTrialHandler(PayPalCommerceGateway, spinner, errorHandler);
const onSmartButtonClick = (data, actions) => {
window.ppcpFundingSource = data.fundingSource;
const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart;
if (isFreeTrial) {
freeTrialHandler.handle();
return actions.reject();
}
};
const onSmartButtonsInit = () => {
buttonsSpinner.unblock();
};
const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit);
const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages);
const context = PayPalCommerceGateway.context;
if (context === 'mini-cart' || context === 'product') {
@ -79,9 +105,69 @@ document.addEventListener(
console.error('PayPal button could not be configured.');
return;
}
const script = document.createElement('script');
if (
PayPalCommerceGateway.context !== 'checkout'
&& PayPalCommerceGateway.data_client_id.user === 0
&& PayPalCommerceGateway.data_client_id.has_subscriptions
) {
return;
}
// Sometimes PayPal script takes long time to load,
// so we additionally hide the standard order button here to avoid failed orders.
// Normally it is hidden later after the script load.
const hideOrderButtonIfPpcpGateway = () => {
// only in checkout and pay now page, otherwise it may break things (e.g. payment via product page),
// and also the loading spinner may look weird on other pages
if (
!['checkout', 'pay-now'].includes(PayPalCommerceGateway.context)
|| isChangePaymentPage()
|| (PayPalCommerceGateway.is_free_trial_cart && PayPalCommerceGateway.vaulted_paypal_email !== '')
) {
return;
}
const currentPaymentMethod = getCurrentPaymentMethod();
const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL;
const isCards = currentPaymentMethod === PaymentMethods.CARDS;
setVisible(ORDER_BUTTON_SELECTOR, !isPaypal && !isCards, true);
if (isPaypal) {
// stopped after the first rendering of the buttons, in onInit
buttonsSpinner.block();
} else {
buttonsSpinner.unblock();
}
if (isCards) {
cardsSpinner.block();
} else {
cardsSpinner.unblock();
}
}
jQuery(document).on('hosted_fields_loaded', () => {
cardsSpinner.unblock();
});
let bootstrapped = false;
hideOrderButtonIfPpcpGateway();
jQuery(document.body).on('updated_checkout payment_method_selected', () => {
if (bootstrapped) {
return;
}
hideOrderButtonIfPpcpGateway();
});
const script = document.createElement('script');
script.addEventListener('load', (event) => {
bootstrapped = true;
bootstrap();
});
script.setAttribute('src', PayPalCommerceGateway.button.url);

View file

@ -1,5 +1,6 @@
import onApprove from '../OnApproveHandler/onApproveForContinue.js';
import {payerData} from "../Helper/PayerData";
import {PaymentMethods} from "../Helper/CheckoutMethodState";
class CartActionHandler {
@ -18,6 +19,7 @@ class CartActionHandler {
body: JSON.stringify({
nonce: this.config.ajax.create_order.nonce,
purchase_units: [],
payment_method: PaymentMethods.PAYPAL,
bn_code:bnCode,
payer,
context:this.config.context

View file

@ -1,5 +1,6 @@
import onApprove from '../OnApproveHandler/onApproveForPayNow.js';
import {payerData} from "../Helper/PayerData";
import {getCurrentPaymentMethod} from "../Helper/CheckoutMethodState";
class CheckoutActionHandler {
@ -31,6 +32,7 @@ class CheckoutActionHandler {
bn_code:bnCode,
context:this.config.context,
order_id:this.config.order_id,
payment_method: getCurrentPaymentMethod(),
form:formValues,
createaccount: createaccount
})
@ -48,7 +50,12 @@ class CheckoutActionHandler {
.querySelector('ul')
);
} else {
errorHandler.message(data.data.message, true);
errorHandler.clear();
if (data.data.details.length > 0) {
errorHandler.message(data.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'), true);
} else {
errorHandler.message(data.data.message, true);
}
}
return;

View file

@ -0,0 +1,43 @@
import {PaymentMethods} from "../Helper/CheckoutMethodState";
import errorHandler from "../ErrorHandler";
class FreeTrialHandler {
constructor(
config,
spinner,
errorHandler
) {
this.config = config;
this.spinner = spinner;
this.errorHandler = errorHandler;
}
handle()
{
this.spinner.block();
fetch(this.config.ajax.vault_paypal.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce: this.config.ajax.vault_paypal.nonce,
return_url: location.href
}),
}).then(res => {
return res.json();
}).then(data => {
if (!data.success) {
this.spinner.unblock();
console.error(data);
this.errorHandler.message(data.data.message);
throw Error(data.data.message);
}
location.href = data.data.approve_link;
}).catch(error => {
this.spinner.unblock();
console.error(error);
this.errorHandler.genericError();
});
}
}
export default FreeTrialHandler;

View file

@ -2,6 +2,7 @@ import ButtonsToggleListener from '../Helper/ButtonsToggleListener';
import Product from '../Entity/Product';
import onApprove from '../OnApproveHandler/onApproveForContinue';
import {payerData} from "../Helper/PayerData";
import {PaymentMethods} from "../Helper/CheckoutMethodState";
class SingleProductActionHandler {
@ -84,6 +85,7 @@ class SingleProductActionHandler {
purchase_units,
payer,
bn_code:bnCode,
payment_method: PaymentMethods.PAYPAL,
context:this.config.context
})
}).then(function (res) {

View file

@ -1,5 +1,11 @@
import ErrorHandler from '../ErrorHandler';
import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler';
import { setVisible } from '../Helper/Hiding';
import {
getCurrentPaymentMethod,
isSavedCardSelected, ORDER_BUTTON_SELECTOR,
PaymentMethods
} from "../Helper/CheckoutMethodState";
class CheckoutBootstap {
constructor(gateway, renderer, messages, spinner) {
@ -7,31 +13,38 @@ class CheckoutBootstap {
this.renderer = renderer;
this.messages = messages;
this.spinner = spinner;
this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR;
this.buttonChangeObserver = new MutationObserver((el) => {
this.updateUi();
});
}
init() {
this.render();
// Unselect saved card.
// WC saves form values, so with our current UI it would be a bit weird
// if the user paid with saved, then after some time tries to pay again,
// but wants to enter a new card, and to do that they have to choose “Select payment” in the list.
jQuery('#saved-credit-card').val(jQuery('#saved-credit-card option:first').val());
jQuery(document.body).on('updated_checkout', () => {
this.render()
});
jQuery(document.body).
on('updated_checkout payment_method_selected', () => {
this.switchBetweenPayPalandOrderButton()
this.displayPlaceOrderButtonForSavedCreditCards()
})
jQuery(document.body).on('updated_checkout payment_method_selected', () => {
this.updateUi();
});
jQuery(document).on('hosted_fields_loaded', () => {
jQuery('#saved-credit-card').on('change', () => {
this.displayPlaceOrderButtonForSavedCreditCards()
this.updateUi();
})
});
this.switchBetweenPayPalandOrderButton()
this.displayPlaceOrderButtonForSavedCreditCards()
this.updateUi();
}
shouldRender() {
@ -60,55 +73,38 @@ class CheckoutBootstap {
this.gateway.hosted_fields.wrapper,
actionHandler.configuration(),
);
this.buttonChangeObserver.observe(
document.querySelector(this.standardOrderButtonSelector),
{attributes: true}
);
}
switchBetweenPayPalandOrderButton() {
jQuery('#saved-credit-card').val(jQuery('#saved-credit-card option:first').val());
updateUi() {
const currentPaymentMethod = getCurrentPaymentMethod();
const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL;
const isCard = currentPaymentMethod === PaymentMethods.CARDS;
const isSavedCard = isCard && isSavedCardSelected();
const isNotOurGateway = !isPaypal && !isCard;
const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart;
const hasVaultedPaypal = PayPalCommerceGateway.vaulted_paypal_email !== '';
const currentPaymentMethod = jQuery(
'input[name="payment_method"]:checked').val();
setVisible(this.standardOrderButtonSelector, (isPaypal && isFreeTrial && hasVaultedPaypal) || isNotOurGateway || isSavedCard, true);
setVisible('.ppcp-vaulted-paypal-details', isPaypal);
setVisible(this.gateway.button.wrapper, isPaypal && !(isFreeTrial && hasVaultedPaypal));
setVisible(this.gateway.messages.wrapper, isPaypal && !isFreeTrial);
setVisible(this.gateway.hosted_fields.wrapper, isCard && !isSavedCard);
if (currentPaymentMethod !== 'ppcp-gateway' && currentPaymentMethod !== 'ppcp-credit-card-gateway') {
this.renderer.hideButtons(this.gateway.button.wrapper);
this.renderer.hideButtons(this.gateway.messages.wrapper);
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
jQuery('#place_order').show();
if (isPaypal && !isFreeTrial) {
this.messages.render();
}
else {
jQuery('#place_order').hide();
if (currentPaymentMethod === 'ppcp-gateway') {
this.renderer.showButtons(this.gateway.button.wrapper);
this.renderer.showButtons(this.gateway.messages.wrapper);
this.messages.render()
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper)
if (isCard) {
if (isSavedCard) {
this.disableCreditCardFields();
} else {
this.enableCreditCardFields();
}
if (currentPaymentMethod === 'ppcp-credit-card-gateway') {
this.renderer.hideButtons(this.gateway.button.wrapper)
this.renderer.hideButtons(this.gateway.messages.wrapper)
this.renderer.showButtons(this.gateway.hosted_fields.wrapper)
}
}
}
displayPlaceOrderButtonForSavedCreditCards() {
const currentPaymentMethod = jQuery(
'input[name="payment_method"]:checked').val();
if (currentPaymentMethod !== 'ppcp-credit-card-gateway') {
return;
}
if (jQuery('#saved-credit-card').length && jQuery('#saved-credit-card').val() !== '') {
this.renderer.hideButtons(this.gateway.button.wrapper)
this.renderer.hideButtons(this.gateway.messages.wrapper)
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper)
jQuery('#place_order').show()
this.disableCreditCardFields()
} else {
jQuery('#place_order').hide()
this.renderer.hideButtons(this.gateway.button.wrapper)
this.renderer.hideButtons(this.gateway.messages.wrapper)
this.renderer.showButtons(this.gateway.hosted_fields.wrapper)
this.enableCreditCardFields()
}
}

View file

@ -22,9 +22,8 @@ class MiniCartBootstap {
}
shouldRender() {
return document.querySelector(this.gateway.button.mini_cart_wrapper) !==
null || document.querySelector(this.gateway.hosted_fields.mini_cart_wrapper) !==
null;
return document.querySelector(this.gateway.button.mini_cart_wrapper) !== null
|| document.querySelector(this.gateway.hosted_fields.mini_cart_wrapper) !== null;
}
render() {
@ -40,4 +39,4 @@ class MiniCartBootstap {
}
}
export default MiniCartBootstap;
export default MiniCartBootstap;

View file

@ -1,86 +1,17 @@
import ErrorHandler from '../ErrorHandler';
import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler';
import CheckoutBootstap from './CheckoutBootstap'
import {isChangePaymentPage} from "../Helper/Subscriptions";
class PayNowBootstrap {
class PayNowBootstrap extends CheckoutBootstap {
constructor(gateway, renderer, messages, spinner) {
this.gateway = gateway;
this.renderer = renderer;
this.messages = messages;
this.spinner = spinner;
super(gateway, renderer, messages, spinner)
}
init() {
this.render();
jQuery(document.body).on('updated_checkout', () => {
this.render();
});
jQuery(document.body).
on('updated_checkout payment_method_selected', () => {
this.switchBetweenPayPalandOrderButton();
});
this.switchBetweenPayPalandOrderButton();
}
shouldRender() {
if (document.querySelector(this.gateway.button.cancel_wrapper)) {
return false;
}
return document.querySelector(this.gateway.button.wrapper) !== null || document.querySelector(this.gateway.hosted_fields.wrapper) !== null;
}
render() {
if (!this.shouldRender()) {
return;
}
if (document.querySelector(this.gateway.hosted_fields.wrapper + '>div')) {
document.querySelector(this.gateway.hosted_fields.wrapper + '>div').setAttribute('style', '');
}
const actionHandler = new CheckoutActionHandler(
PayPalCommerceGateway,
new ErrorHandler(this.gateway.labels.error.generic),
this.spinner
);
this.renderer.render(
this.gateway.button.wrapper,
this.gateway.hosted_fields.wrapper,
actionHandler.configuration(),
);
}
switchBetweenPayPalandOrderButton() {
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has('change_payment_method')) {
updateUi() {
if (isChangePaymentPage()) {
return
}
const currentPaymentMethod = jQuery(
'input[name="payment_method"]:checked').val();
if (currentPaymentMethod !== 'ppcp-gateway' && currentPaymentMethod !== 'ppcp-credit-card-gateway') {
this.renderer.hideButtons(this.gateway.button.wrapper);
this.renderer.hideButtons(this.gateway.messages.wrapper);
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
jQuery('#place_order').show();
}
else {
jQuery('#place_order').hide();
if (currentPaymentMethod === 'ppcp-gateway') {
this.renderer.showButtons(this.gateway.button.wrapper);
this.renderer.showButtons(this.gateway.messages.wrapper);
this.messages.render();
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
}
if (currentPaymentMethod === 'ppcp-credit-card-gateway') {
this.renderer.hideButtons(this.gateway.button.wrapper);
this.renderer.hideButtons(this.gateway.messages.wrapper);
this.renderer.showButtons(this.gateway.hosted_fields.wrapper);
}
}
super.updateUi();
}
}

View file

@ -9,21 +9,51 @@ class SingleProductBootstap {
this.messages = messages;
}
init() {
handleChange() {
if (!this.shouldRender()) {
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
this.renderer.hideButtons(this.gateway.button.wrapper);
return;
}
this.render();
}
shouldRender() {
if (document.querySelector('form.cart') === null) {
return false;
init() {
document.querySelector('form.cart').addEventListener('change', this.handleChange.bind(this))
if (!this.shouldRender()) {
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
return;
}
return true;
this.render();
}
shouldRender() {
return document.querySelector('form.cart') !== null && !this.priceAmountIsZero();
}
priceAmountIsZero() {
let priceText = "0";
if (document.querySelector('form.cart ins .woocommerce-Price-amount')) {
priceText = document.querySelector('form.cart ins .woocommerce-Price-amount').innerText;
}
else if (document.querySelector('form.cart .woocommerce-Price-amount')) {
priceText = document.querySelector('form.cart .woocommerce-Price-amount').innerText;
}
else if (document.querySelector('.product .woocommerce-Price-amount')) {
priceText = document.querySelector('.product .woocommerce-Price-amount').innerText;
}
const amount = parseFloat(priceText.replace(/([^\d,\.\s]*)/g, ''));
return amount === 0;
}
render() {
@ -62,4 +92,4 @@ class SingleProductBootstap {
}
}
export default SingleProductBootstap;
export default SingleProductBootstap;

View file

@ -73,11 +73,11 @@ class ErrorHandler {
clear()
{
if (! this.wrapper.classList.contains('woocommerce-error')) {
if (this.messagesList === null) {
return;
}
this.wrapper.classList.remove('woocommerce-error');
this.wrapper.innerText = '';
this.messagesList.innerHTML = '';
}
}

View file

@ -0,0 +1,20 @@
export const PaymentMethods = {
PAYPAL: 'ppcp-gateway',
CARDS: 'ppcp-credit-card-gateway',
};
export const ORDER_BUTTON_SELECTOR = '#place_order';
export const getCurrentPaymentMethod = () => {
const el = document.querySelector('input[name="payment_method"]:checked');
if (!el) {
return null;
}
return el.value;
};
export const isSavedCardSelected = () => {
const savedCardList = document.querySelector('#saved-credit-card');
return savedCardList && savedCardList.value !== '';
};

View file

@ -0,0 +1,44 @@
const getElement = (selectorOrElement) => {
if (typeof selectorOrElement === 'string') {
return document.querySelector(selectorOrElement);
}
return selectorOrElement;
}
export const isVisible = (element) => {
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
}
export const setVisible = (selectorOrElement, show, important = false) => {
const element = getElement(selectorOrElement);
if (!element) {
return;
}
const currentValue = element.style.getPropertyValue('display');
if (!show) {
if (currentValue === 'none') {
return;
}
element.style.setProperty('display', 'none', important ? 'important' : '');
} else {
if (currentValue === 'none') {
element.style.removeProperty('display');
}
// still not visible (if something else added display: none in CSS)
if (!isVisible(element)) {
element.style.setProperty('display', 'block');
}
}
};
export const hide = (selectorOrElement, important = false) => {
setVisible(selectorOrElement, false, important);
};
export const show = (selectorOrElement) => {
setVisible(selectorOrElement, true);
};

View file

@ -1,7 +1,7 @@
class Spinner {
constructor() {
this.target = 'form.woocommerce-checkout';
constructor(target = 'form.woocommerce-checkout') {
this.target = target;
}
setTarget(target) {

View file

@ -0,0 +1,4 @@
export const isChangePaymentPage = () => {
const urlParams = new URLSearchParams(window.location.search)
return urlParams.has('change_payment_method');
}

View file

@ -4,7 +4,8 @@ const onApprove = (context, errorHandler) => {
method: 'POST',
body: JSON.stringify({
nonce: context.config.ajax.approve_order.nonce,
order_id:data.orderID
order_id:data.orderID,
funding_source: window.ppcpFundingSource,
})
}).then((res)=>{
return res.json();
@ -13,7 +14,7 @@ const onApprove = (context, errorHandler) => {
errorHandler.genericError();
return actions.restart().catch(err => {
errorHandler.genericError();
});;
});
}
location.href = context.config.redirect;
});

View file

@ -1,11 +1,14 @@
const onApprove = (context, errorHandler, spinner) => {
return (data, actions) => {
spinner.block();
errorHandler.clear();
return fetch(context.config.ajax.approve_order.endpoint, {
method: 'POST',
body: JSON.stringify({
nonce: context.config.ajax.approve_order.nonce,
order_id:data.orderID
order_id:data.orderID,
funding_source: window.ppcpFundingSource,
})
}).then((res)=>{
return res.json();

View file

@ -1,4 +1,5 @@
import dccInputFactory from "../Helper/DccInputFactory";
import {show} from "../Helper/Hiding";
class CreditCardRenderer {
@ -9,7 +10,6 @@ class CreditCardRenderer {
this.cardValid = false;
this.formValid = false;
this.currentHostedFieldsInstance = null;
this.formSubmissionSubscribed = false;
}
render(wrapper, contextConfig) {
@ -33,6 +33,8 @@ class CreditCardRenderer {
return;
}
const buttonSelector = wrapper + ' button';
if (this.currentHostedFieldsInstance) {
this.currentHostedFieldsInstance.teardown()
.catch(err => console.error(`Hosted fields teardown error: ${err}`));
@ -122,15 +124,18 @@ class CreditCardRenderer {
});
if (!this.formSubmissionSubscribed) {
document.querySelector(wrapper + ' button').addEventListener(
show(buttonSelector);
if (document.querySelector(wrapper).getAttribute('data-ppcp-subscribed') !== true) {
document.querySelector(buttonSelector).addEventListener(
'click',
event => {
event.preventDefault();
this._submit(contextConfig);
}
);
this.formSubmissionSubscribed = true;
document.querySelector(wrapper).setAttribute('data-ppcp-subscribed', true);
}
});
@ -143,7 +148,7 @@ class CreditCardRenderer {
}
disableFields() {
if( this.currentHostedFieldsInstance) {
if (this.currentHostedFieldsInstance) {
this.currentHostedFieldsInstance.setAttribute({
field: 'number',
attribute: 'disabled'
@ -160,7 +165,7 @@ class CreditCardRenderer {
}
enableFields() {
if( this.currentHostedFieldsInstance) {
if (this.currentHostedFieldsInstance) {
this.currentHostedFieldsInstance.removeAttribute({
field: 'number',
attribute: 'disabled'
@ -181,7 +186,7 @@ class CreditCardRenderer {
this.errorHandler.clear();
if (this.formValid && this.cardValid) {
const save_card = this.defaultConfig.save_card ? true : false;
const save_card = this.defaultConfig.can_save_vault_token ? true : false;
let vault = document.getElementById('ppcp-credit-card-vault') ?
document.getElementById('ppcp-credit-card-vault').checked : save_card;
if (this.defaultConfig.enforce_vault) {
@ -194,13 +199,34 @@ class CreditCardRenderer {
if (contingency !== 'NO_3D_SECURE') {
hostedFieldsData.contingencies = [contingency];
}
if (this.defaultConfig.payer) {
hostedFieldsData.cardholderName = this.defaultConfig.payer.name.given_name + ' ' + this.defaultConfig.payer.name.surname;
}
if (!hostedFieldsData.cardholderName) {
const firstName = document.getElementById('billing_first_name') ? document.getElementById('billing_first_name').value : '';
const lastName = document.getElementById('billing_last_name') ? document.getElementById('billing_last_name').value : '';
if (!firstName || !lastName) {
this.spinner.unblock();
this.errorHandler.message(this.defaultConfig.hosted_fields.labels.cardholder_name_required);
return;
}
hostedFieldsData.cardholderName = firstName + ' ' + lastName;
}
this.currentHostedFieldsInstance.submit(hostedFieldsData).then((payload) => {
payload.orderID = payload.orderId;
this.spinner.unblock();
return contextConfig.onApprove(payload);
}).catch(err => {
console.error(err);
this.spinner.unblock();
this.errorHandler.clear();
if (err.details) {
this.errorHandler.message(err.details.map(d => `${d.issue} ${d.description}`).join('<br/>'), true);
}
});
} else {
this.spinner.unblock();

View file

@ -1,7 +1,9 @@
class Renderer {
constructor(creditCardRenderer, defaultConfig) {
constructor(creditCardRenderer, defaultConfig, onSmartButtonClick, onSmartButtonsInit) {
this.defaultConfig = defaultConfig;
this.creditCardRenderer = creditCardRenderer;
this.onSmartButtonClick = onSmartButtonClick;
this.onSmartButtonsInit = onSmartButtonsInit;
}
render(wrapper, hostedFieldsWrapper, contextConfig) {
@ -19,6 +21,8 @@ class Renderer {
paypal.Buttons({
style,
...contextConfig,
onClick: this.onSmartButtonClick,
onInit: this.onSmartButtonsInit,
}).render(wrapper);
}

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
@ -52,7 +53,7 @@ return array(
*
* @var State $state
*/
if ( $state->current_state() <= State::STATE_PROGRESSIVE ) {
if ( $state->current_state() !== State::STATE_ONBOARDED ) {
return new DisabledSmartButton();
}
$settings = $container->get( 'wcgateway.settings' );
@ -70,8 +71,10 @@ return array(
$environment = $container->get( 'onboarding.environment' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
$settings_status = $container->get( 'wcgateway.settings.status' );
$currency = $container->get( 'api.shop.currency' );
return new SmartButton(
$container->get( 'button.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'session.handler' ),
$settings,
$payer_factory,
@ -82,13 +85,16 @@ return array(
$messages_apply,
$environment,
$payment_token_repository,
$settings_status
$settings_status,
$currency,
$container->get( 'wcgateway.all-funding-sources' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-button/',
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'button.request-data' => static function ( ContainerInterface $container ): RequestData {
@ -166,12 +172,21 @@ return array(
$logger
);
},
'button.endpoint.vault-paypal' => static function( ContainerInterface $container ) : StartPayPalVaultingEndpoint {
return new StartPayPalVaultingEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-token' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger );
},
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply();
return new MessagesApply(
$container->get( 'api.shop.country' )
);
},
'button.is-logged-in' => static function ( ContainerInterface $container ): bool {

View file

@ -9,6 +9,9 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Assets;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
@ -16,9 +19,11 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -30,6 +35,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class SmartButton implements SmartButtonInterface {
use FreeTrialHandlerTrait;
/**
* The Settings status helper.
*
@ -44,6 +51,13 @@ class SmartButton implements SmartButtonInterface {
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* The Session Handler.
*
@ -114,10 +128,39 @@ class SmartButton implements SmartButtonInterface {
*/
private $payment_token_repository;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* All existing funding sources.
*
* @var array
*/
private $all_funding_sources;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Cached payment tokens.
*
* @var PaymentToken[]|null
*/
private $payment_tokens = null;
/**
* SmartButton constructor.
*
* @param string $module_url The URL to the module.
* @param string $version The assets version.
* @param SessionHandler $session_handler The Session Handler.
* @param Settings $settings The Settings.
* @param PayerFactory $payer_factory The Payer factory.
@ -129,9 +172,13 @@ class SmartButton implements SmartButtonInterface {
* @param Environment $environment The environment object.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param SettingsStatus $settings_status The Settings status helper.
* @param string $currency 3-letter currency code of the shop.
* @param array $all_funding_sources All existing funding sources.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $module_url,
string $version,
SessionHandler $session_handler,
Settings $settings,
PayerFactory $payer_factory,
@ -142,10 +189,14 @@ class SmartButton implements SmartButtonInterface {
MessagesApply $messages_apply,
Environment $environment,
PaymentTokenRepository $payment_token_repository,
SettingsStatus $settings_status
SettingsStatus $settings_status,
string $currency,
array $all_funding_sources,
LoggerInterface $logger
) {
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->payer_factory = $payer_factory;
@ -157,6 +208,9 @@ class SmartButton implements SmartButtonInterface {
$this->environment = $environment;
$this->payment_token_repository = $payment_token_repository;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->all_funding_sources = $all_funding_sources;
$this->logger = $logger;
}
/**
@ -199,14 +253,19 @@ class SmartButton implements SmartButtonInterface {
11
);
$subscription_helper = $this->subscription_helper;
add_filter(
'woocommerce_credit_card_form_fields',
function ( $default_fields, $id ) {
function ( array $default_fields, $id ) use ( $subscription_helper ) : array {
if ( is_user_logged_in() && $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) && CreditCardGateway::ID === $id ) {
$default_fields['card-vault'] = sprintf(
'<p class="form-row form-row-wide"><label for="vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>',
'<p class="form-row form-row-wide"><label for="ppcp-credit-card-vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>',
esc_html__( 'Save your Credit Card', 'woocommerce-paypal-payments' )
);
if ( $subscription_helper->cart_contains_subscription() || $subscription_helper->order_pay_contains_subscription() ) {
$default_fields['card-vault'] = '';
}
$tokens = $this->payment_token_repository->all_for_user_id( get_current_user_id() );
if ( $tokens && $this->payment_token_repository->tokens_contains_card( $tokens ) ) {
@ -237,6 +296,38 @@ class SmartButton implements SmartButtonInterface {
2
);
}
if ( $this->is_free_trial_cart() ) {
add_action(
'woocommerce_review_order_after_submit',
function () {
$vaulted_email = $this->get_vaulted_paypal_email();
if ( ! $vaulted_email ) {
return;
}
?>
<div class="ppcp-vaulted-paypal-details">
<?php
echo wp_kses_post(
sprintf(
// translators: %1$s - email, %2$s, %3$s - HTML tags for a link.
esc_html__(
'Using %2$s%1$s%3$s PayPal.',
'woocommerce-paypal-payments'
),
$vaulted_email,
'<b>',
'</b>'
)
);
?>
</div>
<?php
}
);
}
return true;
}
@ -311,27 +402,14 @@ class SmartButton implements SmartButtonInterface {
*/
private function render_button_wrapper_registrar(): bool {
$not_enabled_on_cart = $this->settings->has( 'button_cart_enabled' ) &&
! $this->settings->get( 'button_cart_enabled' );
if (
is_cart()
&& ! $not_enabled_on_cart
) {
add_action(
$this->proceed_to_checkout_button_renderer_hook(),
array(
$this,
'button_renderer',
),
20
);
}
$not_enabled_on_product_page = $this->settings->has( 'button_single_product_enabled' ) &&
! $this->settings->get( 'button_single_product_enabled' );
if (
( is_product() || wc_post_content_has_shortcode( 'product_page' ) )
&& ! $not_enabled_on_product_page
// TODO: it seems like there is no easy way to properly handle vaulted PayPal free trial,
// so disable the buttons for now everywhere except checkout for free trial.
&& ! $this->is_free_trial_product()
) {
add_action(
$this->single_product_renderer_hook(),
@ -343,14 +421,21 @@ class SmartButton implements SmartButtonInterface {
);
}
add_action( $this->pay_order_renderer_hook(), array( $this, 'button_renderer' ), 10 );
$not_enabled_on_minicart = $this->settings->has( 'button_mini_cart_enabled' ) &&
! $this->settings->get( 'button_mini_cart_enabled' );
if (
! $not_enabled_on_minicart
! $not_enabled_on_minicart
&& ! $this->is_free_trial_cart()
) {
add_action(
$this->mini_cart_button_renderer_hook(),
static function () {
function () {
if ( $this->is_cart_price_total_zero() || $this->is_free_trial_cart() ) {
return;
}
echo '<p
id="ppc-button-minicart"
class="woocommerce-mini-cart__buttons buttons"
@ -360,8 +445,28 @@ class SmartButton implements SmartButtonInterface {
);
}
if ( $this->is_cart_price_total_zero() && ! $this->is_free_trial_cart() ) {
return false;
}
$not_enabled_on_cart = $this->settings->has( 'button_cart_enabled' ) &&
! $this->settings->get( 'button_cart_enabled' );
if (
is_cart()
&& ! $not_enabled_on_cart
&& ! $this->is_free_trial_cart()
) {
add_action(
$this->proceed_to_checkout_button_renderer_hook(),
array(
$this,
'button_renderer',
),
20
);
}
add_action( $this->checkout_button_renderer_hook(), array( $this, 'button_renderer' ), 10 );
add_action( $this->pay_order_renderer_hook(), array( $this, 'button_renderer' ), 10 );
return true;
}
@ -377,9 +482,6 @@ class SmartButton implements SmartButtonInterface {
if ( ! is_checkout() && ! $buttons_enabled ) {
return false;
}
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
$load_script = false;
if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) {
@ -392,17 +494,17 @@ class SmartButton implements SmartButtonInterface {
if ( in_array( $this->context(), array( 'pay-now', 'checkout' ), true ) && $this->can_render_dcc() ) {
wp_enqueue_style(
'ppcp-hosted-fields',
$this->module_url . '/assets/css/hosted-fields.css',
untrailingslashit( $this->module_url ) . '/assets/css/hosted-fields.css',
array(),
1
$this->version
);
}
if ( $load_script ) {
wp_enqueue_script(
'ppcp-smart-button',
$this->module_url . '/assets/js/button.js',
untrailingslashit( $this->module_url ) . '/assets/js/button.js',
array( 'jquery' ),
'1.3.2',
$this->version,
true
);
@ -427,10 +529,19 @@ class SmartButton implements SmartButtonInterface {
|| ! $product->is_in_stock()
)
) {
return;
}
echo '<div id="ppc-button"></div>';
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if ( ! isset( $available_gateways['ppcp-gateway'] ) ) {
return;
}
// The wrapper is needed for the loading spinner,
// otherwise jQuery block() prevents buttons rendering.
echo '<div class="ppc-button-wrapper"><div id="ppc-button"></div></div>';
}
/**
@ -556,12 +667,15 @@ class SmartButton implements SmartButtonInterface {
return;
}
$label = 'checkout' === $this->context() ? __( 'Place order', 'woocommerce-paypal-payments' ) : __( 'Pay for order', 'woocommerce-paypal-payments' );
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
$label = 'checkout' === $this->context() ? apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) ) : __( 'Pay for order', 'woocommerce' );
printf(
'<div id="%1$s" style="display:none;">
<button class="button alt">%2$s</button>
</div><div id="payments-sdk__contingency-lightbox"></div><style id="ppcp-hide-dcc">.payment_method_ppcp-credit-card-gateway {display:none;}</style>',
<button type="submit" class="button alt ppcp-dcc-order-button" style="display: none;">%2$s</button>
</div>
<div id="payments-sdk__contingency-lightbox"></div>
<style id="ppcp-hide-dcc">.payment_method_ppcp-credit-card-gateway {display:none;}</style>',
esc_attr( $id ),
esc_html( $label )
);
@ -583,7 +697,7 @@ class SmartButton implements SmartButtonInterface {
return false;
}
return is_user_logged_in();
return true;
}
/**
@ -598,6 +712,10 @@ class SmartButton implements SmartButtonInterface {
if ( is_product() ) {
return $this->subscription_helper->current_product_is_subscription();
}
if ( is_wc_endpoint_url( 'order-pay' ) ) {
return $this->subscription_helper->order_pay_contains_subscription();
}
return $this->subscription_helper->cart_contains_subscription();
}
@ -626,33 +744,42 @@ class SmartButton implements SmartButtonInterface {
private function localize_script(): array {
global $wp;
$is_free_trial_cart = $this->is_free_trial_cart();
$this->request_data->enqueue_nonce_fix();
$localize = array(
'script_attributes' => $this->attributes(),
'data_client_id' => array(
'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(),
'endpoint' => home_url( \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ) ),
'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ),
'user' => get_current_user_id(),
'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(),
'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ),
'user' => get_current_user_id(),
'has_subscriptions' => $this->has_subscriptions(),
),
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'change_cart' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ) ),
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
),
'create_order' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ) ),
'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ),
),
'approve_order' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ) ),
'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ),
),
'vault_paypal' => array(
'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ),
),
),
'enforce_vault' => $this->has_subscriptions(),
'save_card' => $this->can_save_vault_token(),
'can_save_vault_token' => $this->can_save_vault_token(),
'is_free_trial_cart' => $is_free_trial_cart,
'vaulted_paypal_email' => ( is_checkout() && $is_free_trial_cart ) ? $this->get_vaulted_paypal_email() : '',
'bn_codes' => $this->bn_codes(),
'payer' => $this->payerData(),
'button' => array(
@ -680,17 +807,18 @@ class SmartButton implements SmartButtonInterface {
'wrapper' => '#ppcp-hosted-fields',
'mini_cart_wrapper' => '#ppcp-hosted-fields-mini-cart',
'labels' => array(
'credit_card_number' => '',
'cvv' => '',
'mm_yy' => __( 'MM/YY', 'woocommerce-paypal-payments' ),
'fields_not_valid' => __(
'credit_card_number' => '',
'cvv' => '',
'mm_yy' => __( 'MM/YY', 'woocommerce-paypal-payments' ),
'fields_not_valid' => __(
'Unfortunately, your credit card details are not valid.',
'woocommerce-paypal-payments'
),
'card_not_supported' => __(
'card_not_supported' => __(
'Unfortunately, we do not support your credit card.',
'woocommerce-paypal-payments'
),
'cardholder_name_required' => __( 'Cardholder\'s first and last name are required, please fill the checkout form required fields.', 'woocommerce-paypal-payments' ),
),
'valid_cards' => $this->dcc_applies->valid_cards(),
'contingency' => $this->get_3ds_contingency(),
@ -745,7 +873,7 @@ class SmartButton implements SmartButtonInterface {
$params = array(
'client-id' => $this->client_id,
'currency' => get_woocommerce_currency(),
'currency' => $this->currency,
'integration-date' => PAYPAL_INTEGRATION_DATE,
'components' => implode( ',', $this->components() ),
'vault' => $this->can_save_vault_token() ? 'true' : 'false',
@ -777,6 +905,10 @@ class SmartButton implements SmartButtonInterface {
}
}
if ( $this->is_free_trial_cart() ) {
$disable_funding = array_keys( $this->all_funding_sources );
}
if ( count( $disable_funding ) > 0 ) {
$params['disable-funding'] = implode( ',', array_unique( $disable_funding ) );
}
@ -785,6 +917,11 @@ class SmartButton implements SmartButtonInterface {
if ( $this->settings_status->pay_later_messaging_is_enabled() || ! in_array( 'credit', $disable_funding, true ) ) {
$enable_funding[] = 'paylater';
}
if ( $this->is_free_trial_cart() ) {
$enable_funding = array();
}
if ( count( $enable_funding ) > 0 ) {
$params['enable-funding'] = implode( ',', array_unique( $enable_funding ) );
}
@ -843,7 +980,10 @@ class SmartButton implements SmartButtonInterface {
if ( $this->load_button_component() ) {
$components[] = 'buttons';
}
if ( $this->messages_apply->for_country() ) {
if (
$this->messages_apply->for_country()
&& ! $this->is_free_trial_cart()
) {
$components[] = 'messages';
}
if ( $this->dcc_is_enabled() ) {
@ -993,38 +1133,50 @@ class SmartButton implements SmartButtonInterface {
}
/**
* Return action name PayPal buttons will be rendered at on checkout page.
* Returns the action name that PayPal button will use for rendering on the checkout page.
*
* @return string Action name.
*/
private function checkout_button_renderer_hook(): string {
/**
* The filter returning the action name that PayPal button will use for rendering on the checkout page.
*/
return (string) apply_filters( 'woocommerce_paypal_payments_checkout_button_renderer_hook', 'woocommerce_review_order_after_payment' );
}
/**
* Return action name PayPal DCC button will be rendered at on checkout page.
* Returns the action name that PayPal DCC button will use for rendering on the checkout page.
*
* @return string
*/
private function checkout_dcc_button_renderer_hook(): string {
/**
* The filter returning the action name that PayPal DCC button will use for rendering on the checkout page.
*/
return (string) apply_filters( 'woocommerce_paypal_payments_checkout_dcc_renderer_hook', 'woocommerce_review_order_after_submit' );
}
/**
* Return action name PayPal button and Pay Later message will be rendered at on pay-order page.
* Returns the action name that PayPal button and Pay Later message will use for rendering on the pay-order page.
*
* @return string
*/
private function pay_order_renderer_hook(): string {
/**
* The filter returning the action name that PayPal button and Pay Later message will use for rendering on the pay-order page.
*/
return (string) apply_filters( 'woocommerce_paypal_payments_pay_order_dcc_renderer_hook', 'woocommerce_pay_order_after_submit' );
}
/**
* Return action name PayPal button will be rendered next to Proceed to checkout button (normally displayed in cart).
* Returns action name that PayPal button will use for rendering next to Proceed to checkout button (normally displayed in cart).
*
* @return string
*/
private function proceed_to_checkout_button_renderer_hook(): string {
/**
* The filter returning the action name that PayPal button will use for rendering next to Proceed to checkout button (normally displayed in cart).
*/
return (string) apply_filters(
'woocommerce_paypal_payments_proceed_to_checkout_button_renderer_hook',
'woocommerce_proceed_to_checkout'
@ -1032,11 +1184,14 @@ class SmartButton implements SmartButtonInterface {
}
/**
* Return action name PayPal button will be rendered in the WC mini cart.
* Returns the action name that PayPal button will use for rendering in the WC mini cart.
*
* @return string
*/
private function mini_cart_button_renderer_hook(): string {
/**
* The filter returning the action name that PayPal button will use for rendering in the WC mini cart.
*/
return (string) apply_filters(
'woocommerce_paypal_payments_mini_cart_button_renderer_hook',
'woocommerce_widget_shopping_cart_after_buttons'
@ -1044,11 +1199,57 @@ class SmartButton implements SmartButtonInterface {
}
/**
* Return action name PayPal button and Pay Later message will be rendered at on the single product page.
* Returns the action name that PayPal button and Pay Later message will use for rendering on the single product page.
*
* @return string
*/
private function single_product_renderer_hook(): string {
/**
* The filter returning the action name that PayPal button and Pay Later message will use for rendering on the single product page.
*/
return (string) apply_filters( 'woocommerce_paypal_payments_single_product_renderer_hook', 'woocommerce_single_product_summary' );
}
/**
* Check if cart product price total is 0.
*
* @return bool true if is 0, otherwise false.
*/
protected function is_cart_price_total_zero(): bool {
// phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
return WC()->cart->get_cart_contents_total() == 0;
}
/**
* Retrieves all payment tokens for the user, via API or cached if already queried.
*
* @return PaymentToken[]
*/
private function get_payment_tokens(): array {
if ( null === $this->payment_tokens ) {
$this->payment_tokens = $this->payment_token_repository->all_for_user_id( get_current_user_id() );
}
return $this->payment_tokens;
}
/**
* Returns the vaulted PayPal email or empty string.
*
* @return string
*/
private function get_vaulted_paypal_email(): string {
try {
$tokens = $this->get_payment_tokens();
foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) {
return $token->source()->paypal->payer->email_address;
}
}
} catch ( Exception $exception ) {
$this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() );
}
return '';
}
}

View file

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
@ -107,6 +108,15 @@ class ButtonModule implements ModuleInterface {
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . StartPayPalVaultingEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.vault-paypal' );
assert( $endpoint instanceof StartPayPalVaultingEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . ChangeCartEndpoint::ENDPOINT,

View file

@ -184,6 +184,9 @@ class ApproveOrderEndpoint implements EndpointInterface {
throw new RuntimeException( $message );
}
$funding_source = $data['funding_source'] ?? null;
$this->session_handler->replace_funding_source( $funding_source );
$this->session_handler->replace_order( $order );
wp_send_json_success( $order );
return true;

View file

@ -12,6 +12,8 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod;
@ -23,7 +25,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
@ -31,6 +35,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class CreateOrderEndpoint implements EndpointInterface {
use FreeTrialHandlerTrait;
const ENDPOINT = 'ppc-create-order';
/**
@ -175,6 +181,7 @@ class CreateOrderEndpoint implements EndpointInterface {
try {
$data = $this->request_data->read_request( $this->nonce() );
$this->parsed_request_data = $data;
$payment_method = $data['payment_method'] ?? '';
$wc_order = null;
if ( 'pay-now' === $data['context'] ) {
$wc_order = wc_get_order( (int) $data['order_id'] );
@ -191,6 +198,16 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->purchase_units = array( $this->purchase_unit_factory->from_wc_order( $wc_order ) );
} else {
$this->purchase_units = $this->cart_repository->all();
// The cart does not have any info about payment method, so we must handle free trial here.
if ( CreditCardGateway::ID === $payment_method && $this->is_free_trial_cart() ) {
$this->purchase_units[0]->set_amount(
new Amount(
new Money( 1.0, $this->purchase_units[0]->amount()->currency_code() ),
$this->purchase_units[0]->amount()->breakdown()
)
);
}
}
$this->set_bn_code( $data );
@ -337,6 +354,15 @@ class CreateOrderEndpoint implements EndpointInterface {
$payer = $this->payer_factory->from_paypal_response( json_decode( wp_json_encode( $data['payer'] ) ) );
}
if ( ! $payer && isset( $data['form'] ) ) {
parse_str( $data['form'], $form_fields );
if ( isset( $form_fields['billing_email'] ) && '' !== $form_fields['billing_email'] ) {
return $this->payer_factory->from_checkout_form( $form_fields );
}
}
return $payer;
}

View file

@ -80,7 +80,7 @@ class DataClientIdEndpoint implements EndpointInterface {
try {
$this->request_data->read_request( $this->nonce() );
$user_id = get_current_user_id();
$token = $this->identity_token->generate_for_customer( $user_id );
$token = $this->identity_token->generate_for_user( $user_id );
wp_send_json(
array(
'token' => $token->token(),

View file

@ -0,0 +1,111 @@
<?php
/**
* The endpoint for starting vaulting of PayPal account (for free trial).
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
/**
* Class StartPayPalVaultingEndpoint.
*/
class StartPayPalVaultingEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-vault-paypal';
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The PaymentTokenEndpoint.
*
* @var PaymentTokenEndpoint
*/
private $payment_token_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* StartPayPalVaultingEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param PaymentTokenEndpoint $payment_token_endpoint The PaymentTokenEndpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
PaymentTokenEndpoint $payment_token_endpoint,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->payment_token_endpoint = $payment_token_endpoint;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$user_id = get_current_user_id();
$return_url = $data['return_url'];
$cancel_url = add_query_arg( array( 'ppcp_vault' => 'cancel' ), $return_url );
$links = $this->payment_token_endpoint->start_paypal_token_creation(
$user_id,
$return_url,
$cancel_url
);
wp_send_json_success(
array(
'approve_link' => $links->approve_link(),
)
);
return true;
} catch ( Exception $error ) {
$this->logger->error( 'Failed to start PayPal vaulting: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
'message' => $error->getMessage(),
)
);
return false;
}
}
}

View file

@ -26,16 +26,32 @@ class MessagesApply {
'GB',
'FR',
'AU',
'IT',
'ES',
);
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* MessagesApply constructor.
*
* @param string $country 2-letter country code of the shop.
*/
public function __construct( string $country ) {
$this->country = $country;
}
/**
* Determines whether a credit messaging is enabled for the shops location country.
*
* @return bool
*/
public function for_country(): bool {
$region = wc_get_base_location();
$country = $region['country'];
return in_array( $country, $this->countries, true );
return in_array( $this->country, $this->countries, true );
}
}

View file

@ -23,31 +23,50 @@ class MessagesDisclaimers {
*/
private $disclaimers = array(
'US' => array(
'link' => 'https://developer.paypal.com/docs/commerce-platforms/admin-panel/woocommerce/us/',
'link' => 'https://developer.paypal.com/docs/checkout/pay-later/us/commerce-platforms/woocommerce/',
),
'GB' => array(
'link' => 'https://developer.paypal.com/docs/commerce-platforms/admin-panel/woocommerce/uk/',
'link' => 'https://developer.paypal.com/docs/checkout/pay-later/gb/commerce-platforms/woocommerce/',
),
'DE' => array(
'link' => 'https://developer.paypal.com/docs/commerce-platforms/admin-panel/woocommerce/de/',
'link' => 'https://developer.paypal.com/docs/checkout/pay-later/de/commerce-platforms/woocommerce/',
),
'AU' => array(
'link' => 'https://developer.paypal.com/docs/commerce-platforms/admin-panel/woocommerce/au/',
'link' => 'https://developer.paypal.com/docs/checkout/pay-later/au/commerce-platforms/woocommerce/',
),
'FR' => array(
'link' => 'https://developer.paypal.com/docs/commerce-platforms/admin-panel/woocommerce/fr/',
'link' => 'https://developer.paypal.com/docs/checkout/pay-later/fr/commerce-platforms/woocommerce/',
),
'IT' => array(
'link' => 'https://developer.paypal.com/docs/checkout/pay-later/it/commerce-platforms/woocommerce/',
),
'ES' => array(
'link' => 'https://developer.paypal.com/docs/checkout/pay-later/es/commerce-platforms/woocommerce/',
),
);
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* MessagesDisclaimers constructor.
*
* @param string $country 2-letter country code of the shop.
*/
public function __construct( string $country ) {
$this->country = $country;
}
/**
* Returns a disclaimer link based on country.
*
* @return string
*/
public function link_for_country(): string {
$region = wc_get_base_location();
$country = $region['country'];
return $this->disclaimers[ $country ]['link'] ?? '';
return $this->disclaimers[ $this->country ]['link'] ?? '';
}
}

View file

@ -1722,9 +1722,9 @@ mimic-fn@^2.1.0:
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
ms@2.1.2:
version "2.1.2"

View file

@ -29,6 +29,15 @@ class PPECHelper {
const PPEC_SETTINGS_OPTION_NAME = 'woocommerce_ppec_paypal_settings';
/**
* Checks if the PayPal Express Checkout plugin was configured previously.
*
* @return bool
*/
public static function is_plugin_configured() {
return is_array( get_option( self::PPEC_SETTINGS_OPTION_NAME ) );
}
/**
* Checks if the PayPal Express Checkout plugin is active.
*
@ -80,6 +89,9 @@ class PPECHelper {
* @return bool
*/
public static function use_ppec_compat_layer_for_subscriptions() {
/**
* The filter returning whether the compatibility layer for PPEC Subscriptions should be initialized.
*/
return ( ! self::is_gateway_available() ) && self::site_has_ppec_subscriptions() && apply_filters( 'woocommerce_paypal_payments_process_legacy_subscriptions', true );
}

View file

@ -142,7 +142,7 @@ class SettingsImporter {
$value = array_values(
array_intersect(
array_map( 'strtolower', is_array( $option_value ) ? $option_value : array() ),
array( 'card', 'credit', 'sepa', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort', 'venmo' )
array( 'card', 'credit', 'sepa', 'bancontact', 'blik', 'eps', 'giropay', 'ideal', 'mercadopago', 'mybank', 'p24', 'sofort', 'venmo' )
)
);

View file

@ -2,41 +2,28 @@
display: none;
}
#field-merchant_email_production,
#field-ppcp_disconnect_sandbox,
#field-ppcp_disconnect_production,
#field-merchant_id_production,
#field-client_id_production,
#field-client_secret_production,
#field-merchant_email_sandbox,
#field-merchant_id_sandbox,
#field-client_id_sandbox,
#field-client_secret_sandbox{
.ppcp-onboarded .ppcp-onboarding-element:not(.ppcp-always-shown-element) {
display: none;
}
#field-merchant_email_production.show,
#field-ppcp_disconnect_sandbox.show,
#field-ppcp_disconnect_production.show,
#field-merchant_id_production.show,
#field-client_id_production.show,
#field-client_secret_production.show,
#field-merchant_email_sandbox.show,
#field-merchant_id_sandbox.show,
#field-client_id_sandbox.show,
#field-client_secret_sandbox.show {
.ppcp-onboarding .ppcp-settings-field:not(.ppcp-onboarding-element):not(.ppcp-always-shown-element) {
display: none;
}
.ppcp-settings-field.hide {
display: none;
}
.ppcp-settings-field.show {
display: table-row;
}
#field-toggle_manual_input span.hide,
#field-toggle_manual_input.show span.show{
display: none;
}
#field-toggle_manual_input.show span.hide {
display: unset;
label.error {
color: red;
font-weight: bold;
}
#field-production_toggle_manual_input button,
#field-sandbox_toggle_manual_input button {
#field-toggle_manual_input button {
color: #0073aa;
transition-property: border, background, color;
transition-duration: .05s;
@ -49,42 +36,135 @@
padding: 0;
}
#field-sandbox_toggle_manual_input.onboarded,
#field-production_toggle_manual_input.onboarded {
display: none;
}
#field-ppcp_disconnect_sandbox.onboarded,
#field-ppcp_disconnect_production.onboarded,
#field-merchant_email_sandbox.onboarded,
#field-merchant_id_sandbox.onboarded,
#field-client_id_sandbox.onboarded,
#field-client_secret_sandbox.onboarded,
#field-merchant_email_production.onboarded,
#field-merchant_id_production.onboarded,
#field-client_id_production.onboarded,
#field-client_secret_production.onboarded {
display:table-row;
}
#field-ppcp_disconnect_sandbox.onboarded.hide,
#field-ppcp_disconnect_production.onboarded.hide,
#field-merchant_email_sandbox.onboarded.hide,
#field-merchant_id_sandbox.onboarded.hide,
#field-client_id_sandbox.onboarded.hide,
#field-client_secret_sandbox.onboarded.hide,
#field-merchant_email_production.onboarded.hide,
#field-merchant_id_production.onboarded.hide,
#field-client_id_production.onboarded.hide,
#field-client_secret_production.onboarded.hide {
display:none;
}
/* Probably not the best location for this but will do until there's a general purpose settings CSS file. */
.ppcp-settings-field-heading td, .ppcp-settings-field-heading th {
.ppcp-settings-field-heading td, .ppcp-settings-field-heading th, .ppcp-settings-no-title-col td {
padding-left: 0;
}
.woocommerce_page_wc-settings h3.ppcp-subheading {
font-size: 1.1em;
}
.input-text[pattern]:invalid {
border: red solid 2px;
}
ul.ppcp-onboarding-options, ul.ppcp-onboarding-options-sublist {
list-style: none;
}
ul.ppcp-onboarding-options-sublist {
margin-left: 15px;
}
.ppcp-muted-text {
opacity: 0.6;
font-size: 85%;
}
#field-ppcp_onboarading_header > td, #field-ppcp_onboarading_options > td {
padding: 0;
}
.ppcp-onboarding-header, .ppcp-onboarding-cards-options {
display: flex;
width: 1200px;
}
.ppcp-onboarding-header-left, .ppcp-onboarding-header-right {
flex: 50%;
}
.ppcp-onboarding-header-right {
text-align: right;
}
.ppcp-onboarding-header h2 {
margin-top: 0;
}
.ppcp-onboarding-header-left img {
height: 60px;
}
.ppcp-onboarding-header-cards img, .ppcp-onboarding-header-paypal-logos img {
margin: 5px;
}
.ppcp-onboarding-header-cards img {
height: 37px;
}
.ppcp-onboarding-header-paypal-logos img {
height: 32px;
}
.ppcp-onboarding-cards-options table {
margin-left: 35px;
margin-right: 15px;
}
.ppcp-onboarding-cards-options table th, .ppcp-onboarding-cards-options table td {
padding-top: 2px;
}
.ppcp-onboarding-cards-options table th {
width: 350px;
vertical-align: middle !important;
}
.ppcp-onboarding-cards-options table tr th, .ppcp-onboarding-cards-options table tr td {
padding-bottom: 0;
}
.ppcp-onboarding-cards-options table {
padding-bottom: 15px;
}
.ppcp-onboarding-cards-options table tr {
height: 40px;
}
.ppcp-onboarding-cards-screen {
flex: 1;
align-self: center;
}
.ppcp-onboarding-cards-screen img {
width: 100%;
}
.woocommerce-help-tip.ppcp-table-row-tooltip {
margin-left: -20px !important;
}
@media (max-width: 1200px) {
.ppcp-onboarding-header, .ppcp-onboarding-cards-options {
width: unset;
display: block;
}
.ppcp-onboarding-header-right {
text-align: left;
}
.ppcp-onboarding-cards-screen {
display: none;
}
.ppcp-onboarding-header-left img {
height: 30px;
}
.ppcp-onboarding-header-cards img, .ppcp-onboarding-header-paypal-logos img {
margin: 2px;
}
.ppcp-onboarding-header-cards img {
height: 27px;
}
.ppcp-onboarding-header-paypal-logos img {
height: 23px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

@ -1,9 +1,11 @@
// Onboarding.
const ppcp_onboarding = {
BUTTON_SELECTOR: '[data-paypal-onboard-button]',
PAYPAL_JS_ID: 'ppcp-onboarding-paypal-js',
_timeout: false,
STATE_START: 'start',
STATE_ONBOARDED: 'onboarded',
init: function() {
document.addEventListener('DOMContentLoaded', this.reload);
},
@ -15,7 +17,7 @@ const ppcp_onboarding = {
return;
}
// Add event listeners to buttons.
// Add event listeners to buttons preventing link clicking if PayPal init failed.
buttons.forEach(
(element) => {
if (element.hasAttribute('data-ppcp-button-initialized')) {
@ -83,14 +85,13 @@ const ppcp_onboarding = {
authCode: authCode,
sharedId: sharedId,
nonce: PayPalCommerceGatewayOnboarding.nonce,
env: env
env: env,
acceptCards: document.querySelector('#ppcp-onboarding-accept-cards').checked,
}
)
}
);
},
};
function ppcp_onboarding_sandboxCallback(...args) {
@ -101,150 +102,151 @@ function ppcp_onboarding_productionCallback(...args) {
return ppcp_onboarding.loginSeller('production', ...args);
}
/**
* Since the PayPal modal will redirect the user a dirty form
* provokes an alert if the user wants to leave the page. Since the user
* needs to toggle the sandbox switch, we disable this dirty state with the
* following workaround for checkboxes.
*
* @param event
*/
const checkBoxOnClick = (event) => {
const value = event.target.checked;
if (event.target.getAttribute('id') === 'ppcp-sandbox_on') {
toggleSandboxProduction(! value);
}
event.preventDefault();
event.stopPropagation();
setTimeout( () => {
event.target.checked = value;
},1
);
};
/**
* Toggles the credential input fields.
*
* @param forProduction
*/
const credentialToggle = (forProduction) => {
const sandboxClassSelectors = [
'#field-ppcp_disconnect_sandbox',
'#field-merchant_email_sandbox',
'#field-merchant_id_sandbox',
'#field-client_id_sandbox',
'#field-client_secret_sandbox',
];
const productionClassSelectors = [
'#field-ppcp_disconnect_production',
'#field-merchant_email_production',
'#field-merchant_id_production',
'#field-client_id_production',
'#field-client_secret_production',
];
const selectors = forProduction ? productionClassSelectors : sandboxClassSelectors;
document.querySelectorAll(selectors.join()).forEach(
(element) => {element.classList.toggle('show')}
)
};
/**
* Toggles the visibility of the sandbox/production input fields.
*
* @param showProduction
*/
const toggleSandboxProduction = (showProduction) => {
const productionDisplaySelectors = [
'#field-credentials_production_heading',
'#field-production_toggle_manual_input',
'#field-ppcp_onboarding_production',
];
const productionClassSelectors = [
'#field-ppcp_disconnect_production',
'#field-merchant_email_production',
'#field-merchant_id_production',
'#field-client_id_production',
'#field-client_secret_production',
];
const sandboxDisplaySelectors = [
'#field-credentials_sandbox_heading',
'#field-sandbox_toggle_manual_input',
'#field-ppcp_onboarding_sandbox',
];
const sandboxClassSelectors = [
'#field-ppcp_disconnect_sandbox',
'#field-merchant_email_sandbox',
'#field-merchant_id_sandbox',
'#field-client_id_sandbox',
'#field-client_secret_sandbox',
];
if (showProduction) {
document.querySelectorAll(productionDisplaySelectors.join()).forEach(
(element) => {element.style.display = ''}
);
document.querySelectorAll(sandboxDisplaySelectors.join()).forEach(
(element) => {element.style.display = 'none'}
);
document.querySelectorAll(productionClassSelectors.join()).forEach(
(element) => {element.classList.remove('hide')}
);
document.querySelectorAll(sandboxClassSelectors.join()).forEach(
(element) => {
element.classList.remove('show');
element.classList.add('hide');
}
);
return;
}
document.querySelectorAll(productionDisplaySelectors.join()).forEach(
(element) => {element.style.display = 'none'}
);
document.querySelectorAll(sandboxDisplaySelectors.join()).forEach(
(element) => {element.style.display = ''}
);
document.querySelectorAll(sandboxClassSelectors.join()).forEach(
(element) => {element.classList.remove('hide')}
);
document.querySelectorAll(productionClassSelectors.join()).forEach(
(element) => {
element.classList.remove('show');
element.classList.add('hide');
}
)
};
const disconnect = (event) => {
event.preventDefault();
const fields = event.target.classList.contains('production') ? [
'#field-merchant_email_production input',
'#field-merchant_id_production input',
'#field-client_id_production input',
'#field-client_secret_production input',
] : [
'#field-merchant_email_sandbox input',
'#field-merchant_id_sandbox input',
'#field-client_id_sandbox input',
'#field-client_secret_sandbox input',
];
document.querySelectorAll(fields.join()).forEach(
(element) => {
element.value = '';
}
);
document.querySelector('.woocommerce-save-button').click();
};
(() => {
const sandboxSwitchElement = document.querySelector('#ppcp-sandbox_on');
if (sandboxSwitchElement) {
toggleSandboxProduction(! sandboxSwitchElement.checked);
}
const productionCredentialElementsSelectors = [
'#field-merchant_email_production',
'#field-merchant_id_production',
'#field-client_id_production',
'#field-client_secret_production',
];
const sandboxCredentialElementsSelectors = [
'#field-merchant_email_sandbox',
'#field-merchant_id_sandbox',
'#field-client_id_sandbox',
'#field-client_secret_sandbox',
];
const updateOptionsState = () => {
const cardsChk = document.querySelector('#ppcp-onboarding-accept-cards');
if (!cardsChk) {
return;
}
document.querySelectorAll('#ppcp-onboarding-dcc-options input').forEach(input => {
input.disabled = !cardsChk.checked;
});
const basicRb = document.querySelector('#ppcp-onboarding-dcc-basic');
const isExpress = !cardsChk.checked || basicRb.checked;
const expressButtonSelectors = [
'#field-ppcp_onboarding_production_express',
'#field-ppcp_onboarding_sandbox_express',
];
const ppcpButtonSelectors = [
'#field-ppcp_onboarding_production_ppcp',
'#field-ppcp_onboarding_sandbox_ppcp',
];
document.querySelectorAll(expressButtonSelectors.join()).forEach(
element => element.style.display = isExpress ? '' : 'none'
);
document.querySelectorAll(ppcpButtonSelectors.join()).forEach(
element => element.style.display = !isExpress ? '' : 'none'
);
const screemImg = document.querySelector('#ppcp-onboarding-cards-screen-img');
if (screemImg) {
const currentRb = Array.from(document.querySelectorAll('#ppcp-onboarding-dcc-options input[type="radio"]'))
.filter(rb => rb.checked)[0] ?? null;
const imgUrl = currentRb.getAttribute('data-screen-url');
screemImg.src = imgUrl;
}
};
const updateManualInputControls = (shown, isSandbox, isAnyEnvOnboarded) => {
const productionElementsSelectors = productionCredentialElementsSelectors;
const sandboxElementsSelectors = sandboxCredentialElementsSelectors;
const otherElementsSelectors = [
'.woocommerce-save-button',
];
if (!isAnyEnvOnboarded) {
otherElementsSelectors.push('#field-sandbox_on');
}
document.querySelectorAll(productionElementsSelectors.join()).forEach(
element => {
element.classList.remove('hide', 'show');
element.classList.add((shown && !isSandbox) ? 'show' : 'hide');
}
);
document.querySelectorAll(sandboxElementsSelectors.join()).forEach(
element => {
element.classList.remove('hide', 'show');
element.classList.add((shown && isSandbox) ? 'show' : 'hide');
}
);
document.querySelectorAll(otherElementsSelectors.join()).forEach(
element => element.style.display = shown ? '' : 'none'
);
};
const updateEnvironmentControls = (isSandbox) => {
const productionElementsSelectors = [
'#field-ppcp_disconnect_production',
'#field-credentials_production_heading',
];
const sandboxElementsSelectors = [
'#field-ppcp_disconnect_sandbox',
'#field-credentials_sandbox_heading',
];
document.querySelectorAll(productionElementsSelectors.join()).forEach(
element => element.style.display = !isSandbox ? '' : 'none'
);
document.querySelectorAll(sandboxElementsSelectors.join()).forEach(
element => element.style.display = isSandbox ? '' : 'none'
);
};
let isDisconnecting = false;
const disconnect = (event) => {
event.preventDefault();
const fields = event.target.classList.contains('production') ? productionCredentialElementsSelectors : sandboxCredentialElementsSelectors;
document.querySelectorAll(fields.map(f => f + ' input').join()).forEach(
(element) => {
element.value = '';
}
);
isDisconnecting = true;
document.querySelector('.woocommerce-save-button').click();
};
// Prevent the message about unsaved checkbox/radiobutton when reloading the page.
// (WC listens for changes on all inputs and sets dirty flag until form submission)
const preventDirtyCheckboxPropagation = event => {
event.preventDefault();
event.stopPropagation();
const value = event.target.checked;
setTimeout( () => {
event.target.checked = value;
}, 1
);
};
const sandboxSwitchElement = document.querySelector('#ppcp-sandbox_on');
const validate = () => {
const selectors = sandboxSwitchElement.checked ? sandboxCredentialElementsSelectors : productionCredentialElementsSelectors;
const values = selectors.map(s => document.querySelector(s + ' input')).map(el => el.value);
const errors = [];
if (values.some(v => !v)) {
errors.push(PayPalCommerceGatewayOnboarding.error_messages.no_credentials);
}
return errors;
};
const isAnyEnvOnboarded = PayPalCommerceGatewayOnboarding.sandbox_state === ppcp_onboarding.STATE_ONBOARDED ||
PayPalCommerceGatewayOnboarding.production_state === ppcp_onboarding.STATE_ONBOARDED;
document.querySelectorAll('.ppcp-disconnect').forEach(
(button) => {
@ -255,43 +257,89 @@ const disconnect = (event) => {
}
);
// Prevent a possibly dirty form arising from this particular checkbox.
if (sandboxSwitchElement) {
sandboxSwitchElement.addEventListener(
'click',
(event) => {
const value = event.target.checked;
document.querySelectorAll('.ppcp-onboarding-options input').forEach(
(element) => {
element.addEventListener('click', event => {
updateOptionsState();
toggleSandboxProduction( ! value );
preventDirtyCheckboxPropagation(event);
});
}
);
event.preventDefault();
event.stopPropagation();
setTimeout( () => {
event.target.checked = value;
}, 1
);
}
);
}
const isSandboxInBackend = PayPalCommerceGatewayOnboarding.current_env === 'sandbox';
if (sandboxSwitchElement.checked !== isSandboxInBackend) {
sandboxSwitchElement.checked = isSandboxInBackend;
}
// document.querySelectorAll('#mainform input[type="checkbox"]').forEach(
// (checkbox) => {
// checkbox.addEventListener('click', checkBoxOnClick);
// }
// );
updateOptionsState();
document.querySelectorAll('#field-sandbox_toggle_manual_input button, #field-production_toggle_manual_input button').forEach(
(button) => {
button.addEventListener(
'click',
(event) => {
event.preventDefault();
const isProduction = event.target.classList.contains('production-toggle');
credentialToggle(isProduction);
}
)
}
);
const settingsContainer = document.querySelector('#mainform .form-table');
const markCurrentOnboardingState = (isOnboarded) => {
settingsContainer.classList.remove('ppcp-onboarded', 'ppcp-onboarding');
settingsContainer.classList.add(isOnboarded ? 'ppcp-onboarded' : 'ppcp-onboarding');
}
markCurrentOnboardingState(PayPalCommerceGatewayOnboarding.current_state === ppcp_onboarding.STATE_ONBOARDED);
const manualInputToggleButton = document.querySelector('#field-toggle_manual_input button');
let isManualInputShown = PayPalCommerceGatewayOnboarding.current_state === ppcp_onboarding.STATE_ONBOARDED;
manualInputToggleButton.addEventListener(
'click',
(event) => {
event.preventDefault();
isManualInputShown = !isManualInputShown;
updateManualInputControls(isManualInputShown, sandboxSwitchElement.checked, isAnyEnvOnboarded);
}
);
sandboxSwitchElement.addEventListener(
'click',
(event) => {
const isSandbox = sandboxSwitchElement.checked;
if (isAnyEnvOnboarded) {
const onboardingState = isSandbox ? PayPalCommerceGatewayOnboarding.sandbox_state : PayPalCommerceGatewayOnboarding.production_state;
const isOnboarded = onboardingState === ppcp_onboarding.STATE_ONBOARDED;
markCurrentOnboardingState(isOnboarded);
isManualInputShown = isOnboarded;
}
updateManualInputControls(isManualInputShown, isSandbox, isAnyEnvOnboarded);
updateEnvironmentControls(isSandbox);
preventDirtyCheckboxPropagation(event);
}
);
updateManualInputControls(isManualInputShown, sandboxSwitchElement.checked, isAnyEnvOnboarded);
updateEnvironmentControls(sandboxSwitchElement.checked);
document.querySelector('#mainform').addEventListener('submit', e => {
if (isDisconnecting) {
return;
}
const errors = validate();
if (errors.length) {
e.preventDefault();
const errorLabel = document.querySelector('#ppcp-form-errors-label');
errorLabel.parentElement.parentElement.classList.remove('hide');
errorLabel.innerHTML = errors.join('<br/>');
errorLabel.scrollIntoView();
window.scrollBy(0, -120); // WP + WC floating header
}
});
// Onboarding buttons.
ppcp_onboarding.init();

View file

@ -23,7 +23,7 @@ document.addEventListener(
}
group.forEach( (elementToShow) => {
document.querySelector(elementToShow).style.display = 'table-row';
document.querySelector(elementToShow).style.display = '';
})
if('ppcp-message_enabled' === event.target.getAttribute('id')){
@ -56,7 +56,7 @@ document.addEventListener(
return;
}
if (value === elementToToggle.value && domElement.style.display !== 'none') {
domElement.style.display = 'table-row';
domElement.style.display = '';
return;
}
domElement.style.display = 'none';
@ -69,7 +69,7 @@ document.addEventListener(
const value = event.target.value;
group.forEach( (elementToToggle) => {
if (value === elementToToggle.value) {
document.querySelector(elementToToggle.selector).style.display = 'table-row';
document.querySelector(elementToToggle.selector).style.display = '';
return;
}
document.querySelector(elementToToggle.selector).style.display = 'none';
@ -171,6 +171,16 @@ document.addEventListener(
return;
}
const allOptions = Array.from(document.querySelectorAll('select[name="ppcp[disable_cards][]"] option'));
const iconVersions = {
'visa': {
'light': {'label': 'Visa (light)'},
'dark' : {'label': 'Visa (dark)', 'value': 'visa-dark'}
},
'mastercard': {
'light': {'label': 'Mastercard (light)'},
'dark' : {'label': 'Mastercard (dark)', 'value': 'mastercard-dark'}
}
}
const replace = () => {
const validOptions = allOptions.filter(
(option) => {
@ -181,13 +191,35 @@ document.addEventListener(
const selectedValidOptions = validOptions.map(
(option) => {
option = option.cloneNode(true);
option.selected = target.querySelector('option[value="' + option.value + '"]') && target.querySelector('option[value="' + option.value + '"]').selected;
let value = option.value;
option.selected = target.querySelector('option[value="' + value + '"]') && target.querySelector('option[value="' + value + '"]').selected;
if(value === 'visa' || value === 'mastercard') {
let darkOption = option.cloneNode(true);
let currentVersion = iconVersions[value];
let darkValue = iconVersions[value].dark.value;
option.text = currentVersion.light.label;
darkOption.text = currentVersion.dark.label;
darkOption.value = darkValue;
darkOption.selected = target.querySelector('option[value="' + darkValue + '"]') && target.querySelector('option[value="' + darkValue + '"]').selected;
return [option, darkOption];
}
return option;
}
);
target.innerHTML = '';
selectedValidOptions.forEach(
(option) => {
if(Array.isArray(option)){
option.forEach(
(option) => {
target.append(option);
}
)
}
target.append(option);
}
);

View file

@ -18,6 +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\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController;
@ -117,9 +118,8 @@ return array(
);
},
'onboarding.state' => function( ContainerInterface $container ) : State {
$environment = $container->get( 'onboarding.environment' );
$settings = $container->get( 'wcgateway.settings' );
return new State( $environment, $settings );
return new State( $settings );
},
'onboarding.environment' => function( ContainerInterface $container ) : Environment {
$settings = $container->get( 'wcgateway.settings' );
@ -131,15 +131,18 @@ return array(
$login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' );
return new OnboardingAssets(
$container->get( 'onboarding.url' ),
$container->get( 'ppcp.asset-version' ),
$state,
$login_seller_endpoint
$container->get( 'onboarding.environment' ),
$login_seller_endpoint,
$container->get( 'wcgateway.current-ppcp-settings-page-id' )
);
},
'onboarding.url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-onboarding/',
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
@ -188,7 +191,6 @@ return array(
return new PartnerReferrals(
CONNECT_WOO_SANDBOX_URL,
new ConnectBearer(),
$container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
@ -197,7 +199,6 @@ return array(
return new PartnerReferrals(
CONNECT_WOO_URL,
new ConnectBearer(),
$container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
@ -205,11 +206,19 @@ return array(
$partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' );
$partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' );
$partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' );
$settings = $container->get( 'wcgateway.settings' );
return new OnboardingRenderer(
$settings,
$partner_referrals,
$partner_referrals_sandbox
$partner_referrals_sandbox,
$partner_referrals_data
);
},
'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer {
return new OnboardingOptionsRenderer(
$container->get( 'onboarding.url' ),
$container->get( 'api.shop.country' )
);
},
'onboarding.rest' => static function( $container ) : OnboardingRESTController {

View file

@ -10,7 +10,9 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding\Assets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class OnboardingAssets
@ -24,6 +26,13 @@ class OnboardingAssets {
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* The State.
*
@ -31,6 +40,13 @@ class OnboardingAssets {
*/
private $state;
/**
* The Environment.
*
* @var Environment
*/
private $environment;
/**
* The LoginSeller Endpoint.
*
@ -38,22 +54,38 @@ class OnboardingAssets {
*/
private $login_seller_endpoint;
/**
* ID of the current PPCP gateway settings page, or empty if it is not such page.
*
* @var string
*/
protected $page_id;
/**
* OnboardingAssets constructor.
*
* @param string $module_url The URL to the module.
* @param string $version The assets version.
* @param State $state The State object.
* @param Environment $environment The Environment.
* @param LoginSellerEndpoint $login_seller_endpoint The LoginSeller endpoint.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
*/
public function __construct(
string $module_url,
string $version,
State $state,
LoginSellerEndpoint $login_seller_endpoint
Environment $environment,
LoginSellerEndpoint $login_seller_endpoint,
string $page_id
) {
$this->module_url = untrailingslashit( $module_url );
$this->version = $version;
$this->state = $state;
$this->environment = $environment;
$this->login_seller_endpoint = $login_seller_endpoint;
$this->page_id = $page_id;
}
/**
@ -63,28 +95,28 @@ class OnboardingAssets {
*/
public function register(): bool {
$url = $this->module_url . '/assets/css/onboarding.css';
$url = untrailingslashit( $this->module_url ) . '/assets/css/onboarding.css';
wp_register_style(
'ppcp-onboarding',
$url,
array(),
1
$this->version
);
$url = $this->module_url . '/assets/js/settings.js';
$url = untrailingslashit( $this->module_url ) . '/assets/js/settings.js';
wp_register_script(
'ppcp-settings',
$url,
array(),
1,
$this->version,
true
);
$url = $this->module_url . '/assets/js/onboarding.js';
$url = untrailingslashit( $this->module_url ) . '/assets/js/onboarding.js';
wp_register_script(
'ppcp-onboarding',
$url,
array( 'jquery' ),
1,
$this->version,
true
);
wp_localize_script(
@ -103,9 +135,16 @@ class OnboardingAssets {
*/
public function get_script_data() {
return array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( LoginSellerEndpoint::ENDPOINT ) ),
'nonce' => wp_create_nonce( $this->login_seller_endpoint::nonce() ),
'paypal_js_url' => 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js',
'endpoint' => \WC_AJAX::get_endpoint( LoginSellerEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( $this->login_seller_endpoint::nonce() ),
'paypal_js_url' => 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js',
'sandbox_state' => State::get_state_name( $this->state->sandbox_state() ),
'production_state' => State::get_state_name( $this->state->production_state() ),
'current_state' => State::get_state_name( $this->state->current_state() ),
'current_env' => $this->environment->current_environment(),
'error_messages' => array(
'no_credentials' => __( 'API credentials must be entered to save the settings.', 'woocommerce-paypal-payments' ),
),
);
}
@ -131,7 +170,6 @@ class OnboardingAssets {
* @return bool
*/
private function should_render_onboarding_script(): bool {
global $current_section;
return 'ppcp-gateway' === $current_section;
return PayPalGateway::ID === $this->page_id;
}
}

View file

@ -128,6 +128,7 @@ class LoginSellerEndpoint implements EndpointInterface {
$this->settings->set( 'sandbox_on', $is_sandbox );
$this->settings->set( 'products_dcc_enabled', null );
$this->settings->persist();
$endpoint = $is_sandbox ? $this->login_seller_sandbox : $this->login_seller_production;
$credentials = $endpoint->credentials_for(
$data['sharedId'],
@ -143,10 +144,30 @@ class LoginSellerEndpoint implements EndpointInterface {
}
$this->settings->set( 'client_secret', $credentials->client_secret );
$this->settings->set( 'client_id', $credentials->client_id );
$accept_cards = (bool) ( $data['acceptCards'] ?? true );
$funding_sources = array();
if ( $this->settings->has( 'disable_funding' ) ) {
$funding_sources = $this->settings->get( 'disable_funding' );
if ( ! is_array( $funding_sources ) ) {
$funding_sources = array();
}
}
if ( $accept_cards ) {
$funding_sources = array_diff( $funding_sources, array( 'card' ) );
} else {
if ( ! in_array( 'card', $funding_sources, true ) ) {
$funding_sources[] = 'card';
}
}
$this->settings->set( 'disable_funding', $funding_sources );
$this->settings->persist();
if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) {
$this->cache->delete( PayPalBearer::CACHE_KEY );
}
wp_schedule_single_event(
time() + 5,
WebhookRegistrar::EVENT_HOOK

View file

@ -65,16 +65,15 @@ class OnboardingModule implements ModuleInterface {
if ( 'ppcp_onboarding' !== $config['type'] ) {
return $field;
}
$renderer = $c->get( 'onboarding.render' );
$is_production = 'production' === $config['env'];
/**
* The OnboardingRenderer.
*
* @var OnboardingRenderer $renderer
*/
$renderer = $c->get( 'onboarding.render' );
assert( $renderer instanceof OnboardingRenderer );
$is_production = 'production' === $config['env'];
$products = $config['products'];
ob_start();
$renderer->render( $is_production );
$renderer->render( $is_production, $products );
$content = ob_get_contents();
ob_end_clean();
return $content;

View file

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
@ -138,13 +138,13 @@ class OnboardingRESTController {
return array(
'environment' => $environment->current_environment(),
'onboarded' => ( $state->current_state() >= State::STATE_ONBOARDED ),
'state' => $this->get_onboarding_state_name( $state->current_state() ),
'state' => State::get_state_name( $state->current_state() ),
'sandbox' => array(
'state' => $this->get_onboarding_state_name( $state->sandbox_state() ),
'state' => State::get_state_name( $state->sandbox_state() ),
'onboarded' => ( $state->sandbox_state() >= State::STATE_ONBOARDED ),
),
'production' => array(
'state' => $this->get_onboarding_state_name( $state->production_state() ),
'state' => State::get_state_name( $state->production_state() ),
'onboarded' => ( $state->production_state() >= State::STATE_ONBOARDED ),
),
);
@ -207,7 +207,7 @@ class OnboardingRESTController {
}
foreach ( WC()->payment_gateways->payment_gateways() as $gateway ) {
if ( PayPalGateway::ID === $gateway->id ) {
if ( PayPalGateway::ID === $gateway->id || CreditCardGateway::ID === $gateway->id ) {
$gateway->update_option( 'enabled', 'yes' );
break;
}
@ -265,34 +265,6 @@ class OnboardingRESTController {
return add_query_arg( $this->return_url_args, $url );
}
/**
* Translates an onboarding state to a string.
*
* @param int $state An onboarding state to translate as returned by {@link State} methods.
* @return string A string representing the state: "start", "progressive" or "onboarded".
* @see State::current_state(), State::sandbox_state(), State::production_state().
*/
public function get_onboarding_state_name( $state ) {
$name = 'unknown';
switch ( absint( $state ) ) {
case State::STATE_START:
$name = 'start';
break;
case State::STATE_PROGRESSIVE:
$name = 'progressive';
break;
case State::STATE_ONBOARDED:
$name = 'onboarded';
break;
default:
break;
}
return $name;
}
/**
* Generates a signup link for onboarding for a given environment and optionally adding certain URL arguments
* to the URL users are redirected after completing the onboarding flow.

View file

@ -0,0 +1,224 @@
<?php
/**
* Renders the onboarding options.
*
* @package WooCommerce\PayPalCommerce\Onboarding\Render
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding\Render;
/**
* Class OnboardingRenderer
*/
class OnboardingOptionsRenderer {
/**
* The module url.
*
* @var string
*/
private $module_url;
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* OnboardingOptionsRenderer constructor.
*
* @param string $module_url The module url (for assets).
* @param string $country 2-letter country code of the shop.
*/
public function __construct( string $module_url, string $country ) {
$this->module_url = $module_url;
$this->country = $country;
}
/**
* Renders the onboarding options.
*
* @param bool $is_shop_supports_dcc Whether the shop can use DCC (country, currency).
*/
public function render( bool $is_shop_supports_dcc ): string {
return '
<ul class="ppcp-onboarding-options">
<li>
<label><input type="checkbox" disabled checked> ' .
__( 'Enable PayPal Payments — includes PayPal, Venmo, Pay Later — with fraud protection', 'woocommerce-paypal-payments' ) . '
</label>
</li>
<li>
<label><input type="checkbox" id="ppcp-onboarding-accept-cards" checked> ' .
__( 'Securely accept all major credit & debit cards on the strength of the PayPal network', 'woocommerce-paypal-payments' ) . '
</label>
</li>
<li>' . $this->render_dcc( $is_shop_supports_dcc ) . '</li>
</ul>';
}
/**
* Renders the onboarding DCC options.
*
* @param bool $is_shop_supports_dcc Whether the shop can use DCC (country, currency).
*/
private function render_dcc( bool $is_shop_supports_dcc ): string {
$items = array();
$is_us_shop = 'US' === $this->country;
if ( $is_shop_supports_dcc ) {
$dcc_table_rows = array(
$this->render_table_row(
__( 'Credit & Debit Card form fields', 'woocommerce-paypal-payments' ),
__( 'Customizable user experience', 'woocommerce-paypal-payments' )
),
! $is_us_shop ? '' : $this->render_table_row(
__( 'Credit & Debit Card pricing', 'woocommerce-paypal-payments' ),
__( '2.59% + $0.49', 'woocommerce-paypal-payments' ),
'',
__( 'for US domestic transactions', 'woocommerce-paypal-payments' )
),
$this->render_table_row(
__( 'Seller Protection', 'woocommerce-paypal-payments' ),
__( 'Yes', 'woocommerce-paypal-payments' ),
__( 'No matter what you sell, Seller Protection can help you avoid chargebacks, reversals, and fees on eligible PayPal payment transactions — even when a customer has filed a dispute.', 'woocommerce-paypal-payments' ),
__( 'for eligible PayPal transactions', 'woocommerce-paypal-payments' )
),
$this->render_table_row(
__( 'Fraud Protection', 'woocommerce-paypal-payments' ),
__( 'Yes', 'woocommerce-paypal-payments' ),
__( 'Included with Advanced Checkout at no extra cost, Fraud Protection gives you the insight and control you need to better balance chargebacks and declines.', 'woocommerce-paypal-payments' )
),
! $is_us_shop ? '' : $this->render_table_row(
__( 'Chargeback Protection', 'woocommerce-paypal-payments' ),
__( 'Optional', 'woocommerce-paypal-payments' ),
__( 'If you choose this optional, fee-based alternative to Fraud Protection, PayPal will manage chargebacks for eligible credit and debit card transactions — so you wont have to worry about unexpected costs.', 'woocommerce-paypal-payments' ),
__( 'extra 0.4% per transaction', 'woocommerce-paypal-payments' )
),
$this->render_table_row(
__( 'Additional Vetting and Underwriting Required', 'woocommerce-paypal-payments' ),
__( 'Yes', 'woocommerce-paypal-payments' ),
__( 'Business Ownership and other business information will be required during the application for Advanced Card Processing.', 'woocommerce-paypal-payments' )
),
$this->render_table_row(
__( 'Seller Account Type', 'woocommerce-paypal-payments' ),
__( 'Business', 'woocommerce-paypal-payments' ),
__( 'For Standard payments, Casual sellers may connect their Personal PayPal account in eligible countries to sell on WooCommerce. For Advanced payments, a Business PayPal account is required.', 'woocommerce-paypal-payments' )
),
);
$items[] = '
<li>
<label>
<input type="radio" id="ppcp-onboarding-dcc-acdc" name="ppcp_onboarding_dcc" value="acdc" checked ' .
'data-screen-url="' . $this->get_screen_url( 'acdc' ) . '"> ' .
__( 'Advanced Card Processing', 'woocommerce-paypal-payments' ) . '
</label>
' . $this->render_tooltip( __( 'PayPal acts as the payment processor for card transactions. You can add optional features like Chargeback Protection for more security.', 'woocommerce-paypal-payments' ) ) . '
<table>
' . implode( '', $dcc_table_rows ) . '
</table>
</li>';
}
$basic_table_rows = array(
$this->render_table_row(
__( 'Credit & Debit Card form fields', 'woocommerce-paypal-payments' ),
__( 'Prebuilt user experience', 'woocommerce-paypal-payments' )
),
! $is_us_shop ? '' : $this->render_table_row(
__( 'Credit & Debit Card pricing', 'woocommerce-paypal-payments' ),
__( '3.49% + $0.49', 'woocommerce-paypal-payments' ),
'',
__( 'for US domestic transactions', 'woocommerce-paypal-payments' )
),
$this->render_table_row(
__( 'Seller Protection', 'woocommerce-paypal-payments' ),
__( 'Yes', 'woocommerce-paypal-payments' ),
__( 'No matter what you sell, Seller Protection can help you avoid chargebacks, reversals, and fees on eligible PayPal payment transactions — even when a customer has filed a dispute.', 'woocommerce-paypal-payments' ),
__( 'for eligible PayPal transactions', 'woocommerce-paypal-payments' )
),
$this->render_table_row(
__( 'Seller Account Type', 'woocommerce-paypal-payments' ),
__( 'Business or Casual', 'woocommerce-paypal-payments' ),
__( 'For Standard payments, Casual sellers may connect their Personal PayPal account in eligible countries to sell on WooCommerce. For Advanced payments, a Business PayPal account is required.', 'woocommerce-paypal-payments' )
),
);
$items[] = '
<li ' . ( ! $is_shop_supports_dcc ? 'style="display: none;"' : '' ) . '>
<label>
<input type="radio" id="ppcp-onboarding-dcc-basic" name="ppcp_onboarding_dcc" value="basic" ' .
( ! $is_shop_supports_dcc ? 'checked' : '' ) .
' data-screen-url="' . $this->get_screen_url( 'basic' ) . '"' .
'> ' .
__( 'Standard Card Processing', 'woocommerce-paypal-payments' ) . '
</label>
' . $this->render_tooltip( __( 'Card transactions are managed by PayPal, which simplifies compliance requirements for you.', 'woocommerce-paypal-payments' ) ) . '
<table>
' . implode( $basic_table_rows ) . '
</table>
</li>';
return '
<div class="ppcp-onboarding-cards-options">
<ul id="ppcp-onboarding-dcc-options" class="ppcp-onboarding-options-sublist">' .
implode( '', $items ) .
'
</ul>
<div class="ppcp-onboarding-cards-screen">' .
( $is_shop_supports_dcc ? '<img id="ppcp-onboarding-cards-screen-img" />' : '' ) . '
</div>
</div>';
}
/**
* Returns HTML of a row for the cards options tables.
*
* @param string $header The text in the first cell.
* @param string $value The text in the second cell.
* @param string $tooltip The text shown on hover.
* @param string $note The additional description text, such as about conditions.
* @return string
*/
private function render_table_row( string $header, string $value, string $tooltip = '', string $note = '' ): string {
$value_html = $value;
if ( $note ) {
$value_html .= '<br/><span class="ppcp-muted-text">' . $note . '</span>';
}
$tooltip_html = '';
if ( $tooltip ) {
$tooltip_html = $this->render_tooltip( $tooltip, array( 'ppcp-table-row-tooltip' ) );
}
return "
<tr>
<th>$tooltip_html $header</th>
<td>$value_html</td>
</tr>";
}
/**
* Returns HTML of a tooltip (question mark icon).
*
* @param string $tooltip The text shown on hover.
* @param string[] $classes Additional CSS classes.
* @return string
*/
private function render_tooltip( string $tooltip, array $classes = array() ): string {
return '<span class="woocommerce-help-tip ' . implode( ' ', $classes ) . '" data-tip="' . esc_attr( $tooltip ) . '"></span> ';
}
/**
* Returns the screen image URL.
*
* @param string $key The image suffix, 'acdc' or 'basic'.
* @return string
*/
private function get_screen_url( string $key ): string {
return untrailingslashit( $this->module_url ) . "/assets/images/cards-screen-$key.png";
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Onboarding\Render;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
@ -39,31 +40,50 @@ class OnboardingRenderer {
*/
private $sandbox_partner_referrals;
/**
* The default partner referrals data.
*
* @var PartnerReferralsData
*/
private $partner_referrals_data;
/**
* OnboardingRenderer constructor.
*
* @param Settings $settings The settings.
* @param PartnerReferrals $production_partner_referrals The PartnerReferrals for production.
* @param PartnerReferrals $sandbox_partner_referrals The PartnerReferrals for sandbox.
* @param Settings $settings The settings.
* @param PartnerReferrals $production_partner_referrals The PartnerReferrals for production.
* @param PartnerReferrals $sandbox_partner_referrals The PartnerReferrals for sandbox.
* @param PartnerReferralsData $partner_referrals_data The default partner referrals data.
*/
public function __construct( Settings $settings, PartnerReferrals $production_partner_referrals, PartnerReferrals $sandbox_partner_referrals ) {
public function __construct(
Settings $settings,
PartnerReferrals $production_partner_referrals,
PartnerReferrals $sandbox_partner_referrals,
PartnerReferralsData $partner_referrals_data
) {
$this->settings = $settings;
$this->production_partner_referrals = $production_partner_referrals;
$this->sandbox_partner_referrals = $sandbox_partner_referrals;
$this->partner_referrals_data = $partner_referrals_data;
}
/**
* Returns the action URL for the onboarding button/link.
*
* @param boolean $is_production Whether the production or sandbox button should be rendered.
* @param boolean $is_production Whether the production or sandbox button should be rendered.
* @param string[] $products The list of products ('PPCP', 'EXPRESS_CHECKOUT').
* @return string URL.
*/
public function get_signup_link( bool $is_production ) {
public function get_signup_link( bool $is_production, array $products ) {
$args = array(
'displayMode' => 'minibrowser',
);
$url = $is_production ? $this->production_partner_referrals->signup_link() : $this->sandbox_partner_referrals->signup_link();
$data = $this->partner_referrals_data
->with_products( $products )
->data();
$url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data );
$url = add_query_arg( $args, $url );
return $url;
@ -72,20 +92,26 @@ class OnboardingRenderer {
/**
* Renders the "Connect to PayPal" button.
*
* @param bool $is_production Whether the production or sandbox button should be rendered.
* @param bool $is_production Whether the production or sandbox button should be rendered.
* @param string[] $products The list of products ('PPCP', 'EXPRESS_CHECKOUT').
*/
public function render( bool $is_production ) {
public function render( bool $is_production, array $products ) {
try {
$id = 'connect-to' . ( $is_production ? 'production' : 'sandbox' ) . strtolower( implode( '-', $products ) );
$this->render_button(
$this->get_signup_link( $is_production ),
$is_production ? 'connect-to-production' : 'connect-to-sandbox',
$is_production ? __( 'Connect to PayPal', 'woocommerce-paypal-payments' ) : __( 'Connect to PayPal Sandbox', 'woocommerce-paypal-payments' ),
$this->get_signup_link( $is_production, $products ),
$id,
$is_production ? __( 'Activate PayPal', 'woocommerce-paypal-payments' ) : __( 'Test payments with PayPal sandbox', 'woocommerce-paypal-payments' ),
$is_production ? 'primary' : 'secondary',
$is_production ? 'production' : 'sandbox'
);
} catch ( RuntimeException $exception ) {
esc_html_e(
'We could not properly connect to PayPal. Please reload the page to continue',
'woocommerce-paypal-payments'
echo esc_html(
__(
'We could not properly connect to PayPal. Try reloading the page.',
'woocommerce-paypal-payments'
) . " {$exception->getMessage()} {$exception->getFile()}:{$exception->getLine()}"
);
}
}
@ -96,13 +122,14 @@ class OnboardingRenderer {
* @param string $url The url of the button.
* @param string $id The ID of the button.
* @param string $label The button text.
* @param string $class The CSS class for button ('primary', 'secondary').
* @param string $env The environment ('production' or 'sandbox').
*/
private function render_button( string $url, string $id, string $label, string $env ) {
private function render_button( string $url, string $id, string $label, string $class, string $env ) {
?>
<a
target="_blank"
class="button-primary"
class="button-<?php echo esc_attr( $class ); ?>"
id="<?php echo esc_attr( $id ); ?>"
data-paypal-onboard-complete="ppcp_onboarding_<?php echo esc_attr( $env ); ?>Callback"
data-paypal-onboard-button="true"

View file

@ -16,16 +16,8 @@ use Psr\Container\ContainerInterface;
*/
class State {
const STATE_START = 0;
const STATE_PROGRESSIVE = 4;
const STATE_ONBOARDED = 8;
/**
* The Environment.
*
* @var Environment
*/
private $environment;
const STATE_START = 0;
const STATE_ONBOARDED = 8;
/**
* The Settings.
@ -37,16 +29,29 @@ class State {
/**
* State constructor.
*
* @param Environment $environment The Environment.
* @param ContainerInterface $settings The Settings.
*/
public function __construct(
Environment $environment,
ContainerInterface $settings
) {
$this->environment = $environment;
$this->settings = $settings;
$this->settings = $settings;
}
/**
* Returns the state of the specified environment (or the active environment if null).
*
* @param string|null $environment 'sandbox', 'production'.
* @return int
*/
public function environment_state( ?string $environment = null ): int {
switch ( $environment ) {
case Environment::PRODUCTION:
return $this->production_state();
case Environment::SANDBOX:
return $this->sandbox_state();
}
return $this->current_state();
}
/**
@ -57,9 +62,6 @@ class State {
public function current_state(): int {
return $this->state_by_keys(
array(
'merchant_email',
),
array(
'merchant_email',
'merchant_id',
@ -77,9 +79,6 @@ class State {
public function sandbox_state() : int {
return $this->state_by_keys(
array(
'merchant_email_sandbox',
),
array(
'merchant_email_sandbox',
'merchant_id_sandbox',
@ -97,9 +96,6 @@ class State {
public function production_state() : int {
return $this->state_by_keys(
array(
'merchant_email_production',
),
array(
'merchant_email_production',
'merchant_id_production',
@ -110,36 +106,36 @@ class State {
}
/**
* Returns the state based on progressive and onboarded values being looked up in the settings.
* Translates an onboarding state to a string.
*
* @param int $state An onboarding state to translate.
* @return string A string representing the state: "start" or "onboarded".
*/
public static function get_state_name( int $state ) : string {
switch ( $state ) {
case self::STATE_START:
return 'start';
case self::STATE_ONBOARDED:
return 'onboarded';
default:
return 'unknown';
}
}
/**
* Returns the state based on onboarding settings values.
*
* @param array $progressive_keys The keys which need to be present to be at least in progressive state.
* @param array $onboarded_keys The keys which need to be present to be in onboarded state.
*
* @return int
*/
private function state_by_keys( array $progressive_keys, array $onboarded_keys ) : int {
$state = self::STATE_START;
$is_progressive = true;
foreach ( $progressive_keys as $key ) {
if ( ! $this->settings->has( $key ) || ! $this->settings->get( $key ) ) {
$is_progressive = false;
}
}
if ( $is_progressive ) {
$state = self::STATE_PROGRESSIVE;
}
$is_onboarded = true;
private function state_by_keys( array $onboarded_keys ) : int {
foreach ( $onboarded_keys as $key ) {
if ( ! $this->settings->has( $key ) || ! $this->settings->get( $key ) ) {
$is_onboarded = false;
return self::STATE_START;
}
}
if ( $is_onboarded ) {
$state = self::STATE_ONBOARDED;
}
return $state;
return self::STATE_ONBOARDED;
}
}

View file

@ -28,7 +28,10 @@ return array(
return $session_handler;
},
'session.cancellation.view' => function ( ContainerInterface $container ) : CancelView {
return new CancelView();
return new CancelView(
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.funding-source.renderer' )
);
},
'session.cancellation.controller' => function ( ContainerInterface $container ) : CancelController {
return new CancelController(

View file

@ -67,7 +67,7 @@ class CancelController {
add_action(
'woocommerce_review_order_after_submit',
function () use ( $url ) {
$this->view->render_session_cancellation( $url );
$this->view->render_session_cancellation( $url, $this->session_handler->funding_source() );
}
);
}

View file

@ -9,31 +9,66 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Session\Cancellation;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
/**
* Class CancelView
*/
class CancelView {
/**
* The settings.
*
* @var ContainerInterface
*/
protected $settings;
/**
* The funding source renderer.
*
* @var FundingSourceRenderer
*/
protected $funding_source_renderer;
/**
* CancelView constructor.
*
* @param ContainerInterface $settings The settings.
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
*/
public function __construct(
ContainerInterface $settings,
FundingSourceRenderer $funding_source_renderer
) {
$this->settings = $settings;
$this->funding_source_renderer = $funding_source_renderer;
}
/**
* Renders the cancel link.
*
* @param string $url The URL.
* @param string $url The URL.
* @param string|null $funding_source The ID of the funding source, such as 'venmo'.
*/
public function render_session_cancellation( string $url ) {
public function render_session_cancellation( string $url, ?string $funding_source ) {
?>
<p id="ppcp-cancel"
class="has-text-align-center ppcp-cancel"
>
<?php
$name = $funding_source ?
$this->funding_source_renderer->render_name( $funding_source )
: ( $this->settings->has( 'title' ) ? $this->settings->get( 'title' ) : __( 'PayPal', 'woocommerce-paypal-payments' ) );
printf(
// translators: the placeholders are html tags for a link.
// translators: %3$ is funding source like "PayPal" or "Venmo", other placeholders are html tags for a link.
esc_html__(
'You are currently paying with PayPal. If you want to cancel
'You are currently paying with %3$s. If you want to cancel
this process, please click %1$shere%2$s.',
'woocommerce-paypal-payments'
),
'<a href="' . esc_url( $url ) . '">',
'</a>'
'</a>',
esc_html( $name )
);
?>
</p>

View file

@ -40,6 +40,13 @@ class SessionHandler {
*/
private $insufficient_funding_tries = 0;
/**
* The funding source of the current checkout (venmo, ...) or null.
*
* @var string|null
*/
private $funding_source = null;
/**
* Returns the order.
*
@ -84,6 +91,28 @@ class SessionHandler {
return $this;
}
/**
* Returns the funding source of the current checkout (venmo, ...) or null.
*
* @return string|null
*/
public function funding_source(): ?string {
return $this->funding_source;
}
/**
* Replaces the funding source of the current checkout.
*
* @param string|null $funding_source The funding source.
*
* @return SessionHandler
*/
public function replace_funding_source( ?string $funding_source ): SessionHandler {
$this->funding_source = $funding_source;
$this->store_session();
return $this;
}
/**
* Returns how many times the customer tried to use the PayPal Gateway in this session.
*
@ -113,6 +142,7 @@ class SessionHandler {
$this->order = null;
$this->bn_code = '';
$this->insufficient_funding_tries = 0;
$this->funding_source = null;
$this->store_session();
return $this;
}

View file

@ -37,9 +37,11 @@ class Renderer {
foreach ( $items as $item ) {
?>
<tr>
<td data-export-label="<?php echo esc_attr( $item['label'] ); ?>"><?php echo esc_attr( $item['label'] ); ?></td>
<td data-export-label="<?php echo esc_attr( $item['exported_label'] ?? $item['label'] ); ?>">
<?php echo esc_attr( $item['label'] ); ?>
</td>
<td class="help"><?php echo wc_help_tip( $item['description'] ); ?></td>
<td><?php echo esc_attr( $item['value'] ); ?></td>
<td><?php echo wp_kses_post( $item['value'] ); ?></td>
</tr>
<?php
}

View file

@ -14,11 +14,13 @@ use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\WebhookInfoStorage;
/**
* Class StatusReportModule
@ -44,6 +46,8 @@ class StatusReportModule implements ModuleInterface {
add_action(
'woocommerce_system_status_report',
function () use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
/* @var State $state The state. */
$state = $c->get( 'onboarding.state' );
@ -57,38 +61,93 @@ class StatusReportModule implements ModuleInterface {
/* @var MessagesApply $messages_apply The messages apply. */
$messages_apply = $c->get( 'button.helper.messages-apply' );
$last_webhook_storage = $c->get( 'webhook.last-webhook-storage' );
assert( $last_webhook_storage instanceof WebhookInfoStorage );
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );
/* @var Renderer $renderer The renderer. */
$renderer = $c->get( 'status-report.renderer' );
$had_ppec_plugin = PPECHelper::is_plugin_configured();
$items = array(
array(
'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ),
'description' => esc_html__( 'Whether PayPal account is correctly configured or not.', 'woocommerce-paypal-payments' ),
'value' => $this->onboarded( $bearer, $state ),
'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ),
'exported_label' => 'Onboarded',
'description' => esc_html__( 'Whether PayPal account is correctly configured or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$this->onboarded( $bearer, $state )
),
),
array(
'label' => esc_html__( 'Shop country code', 'woocommerce-paypal-payments' ),
'description' => esc_html__( 'Country / State value on Settings / General / Store Address.', 'woocommerce-paypal-payments' ),
'value' => wc_get_base_location()['country'],
'label' => esc_html__( 'Shop country code', 'woocommerce-paypal-payments' ),
'exported_label' => 'Shop country code',
'description' => esc_html__( 'Country / State value on Settings / General / Store Address.', 'woocommerce-paypal-payments' ),
'value' => $c->get( 'api.shop.country' ),
),
array(
'label' => esc_html__( 'PayPal card processing available in country', 'woocommerce-paypal-payments' ),
'description' => esc_html__( 'Whether PayPal card processing is available in country or not.', 'woocommerce-paypal-payments' ),
'value' => $dcc_applies->for_country_currency()
? esc_html__( 'Yes', 'woocommerce-paypal-payments' )
: esc_html__( 'No', 'woocommerce-paypal-payments' ),
'label' => esc_html__( 'WooCommerce currency supported', 'woocommerce-paypal-payments' ),
'exported_label' => 'WooCommerce currency supported',
'description' => esc_html__( 'Whether PayPal supports the default store currency or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$c->get( 'api.shop.is-currency-supported' )
),
),
array(
'label' => esc_html__( 'Pay Later messaging available in country', 'woocommerce-paypal-payments' ),
'description' => esc_html__( 'Whether Pay Later is available in country or not.', 'woocommerce-paypal-payments' ),
'value' => $messages_apply->for_country()
? esc_html__( 'Yes', 'woocommerce-paypal-payments' )
: esc_html__( 'No', 'woocommerce-paypal-payments' ),
'label' => esc_html__( 'PayPal card processing available in country', 'woocommerce-paypal-payments' ),
'exported_label' => 'PayPal card processing available in country',
'description' => esc_html__( 'Whether PayPal card processing is available in country or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$dcc_applies->for_country_currency()
),
),
array(
'label' => esc_html__( 'Vault enabled', 'woocommerce-paypal-payments' ),
'description' => esc_html__( 'Whether vaulting is enabled on PayPal account or not.', 'woocommerce-paypal-payments' ),
'value' => $this->vault_enabled( $bearer ),
'label' => esc_html__( 'Pay Later messaging available in country', 'woocommerce-paypal-payments' ),
'exported_label' => 'Pay Later messaging available in country',
'description' => esc_html__( 'Whether Pay Later is available in country or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$messages_apply->for_country()
),
),
array(
'label' => esc_html__( 'Webhook status', 'woocommerce-paypal-payments' ),
'exported_label' => 'Webhook status',
'description' => esc_html__( 'Whether we received webhooks successfully.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html( ! $last_webhook_storage->is_empty() ),
),
array(
'label' => esc_html__( 'Vault enabled', 'woocommerce-paypal-payments' ),
'exported_label' => 'Vault enabled',
'description' => esc_html__( 'Whether vaulting is enabled on PayPal account or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$this->vault_enabled( $bearer )
),
),
array(
'label' => esc_html__( 'Logging enabled', 'woocommerce-paypal-payments' ),
'exported_label' => 'Logging enabled',
'description' => esc_html__( 'Whether logging of plugin events and errors is enabled.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$settings->has( 'logging_enabled' ) && $settings->get( 'logging_enabled' )
),
),
array(
'label' => esc_html__( 'Reference Transactions', 'woocommerce-paypal-payments' ),
'exported_label' => 'Reference Transactions',
'description' => esc_html__( 'Whether Reference Transactions are enabled for the connected account', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$this->reference_transaction_enabled( $billing_agreements_endpoint )
),
),
array(
'label' => esc_html__( 'Used PayPal Checkout plugin', 'woocommerce-paypal-payments' ),
'exported_label' => 'Used PayPal Checkout plugin',
'description' => esc_html__( 'Whether the PayPal Checkout Gateway plugin was configured previously or not', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$had_ppec_plugin
),
),
);
@ -112,37 +171,56 @@ class StatusReportModule implements ModuleInterface {
*
* @param Bearer $bearer The bearer.
* @param State $state The state.
* @return string
* @return bool
*/
private function onboarded( $bearer, $state ): string {
private function onboarded( Bearer $bearer, State $state ): bool {
try {
$token = $bearer->bearer();
} catch ( RuntimeException $exception ) {
return esc_html__( 'No', 'woocommerce-paypal-payments' );
return false;
}
$current_state = $state->current_state();
if ( $token->is_valid() && $current_state === $state::STATE_ONBOARDED ) {
return esc_html__( 'Yes', 'woocommerce-paypal-payments' );
}
return esc_html__( 'No', 'woocommerce-paypal-payments' );
return $token->is_valid() && $current_state === $state::STATE_ONBOARDED;
}
/**
* It returns whether vaulting is enabled or not.
*
* @param Bearer $bearer The bearer.
* @return string
* @return bool
*/
private function vault_enabled( $bearer ) {
private function vault_enabled( Bearer $bearer ): bool {
try {
$token = $bearer->bearer();
return $token->vaulting_available()
? esc_html__( 'Yes', 'woocommerce-paypal-payments' )
: esc_html__( 'No', 'woocommerce-paypal-payments' );
return $token->vaulting_available();
} catch ( RuntimeException $exception ) {
return esc_html__( 'No', 'woocommerce-paypal-payments' );
return false;
}
}
/**
* Checks if reference transactions are enabled in account.
*
* @param BillingAgreementsEndpoint $billing_agreements_endpoint The endpoint.
*/
private function reference_transaction_enabled( BillingAgreementsEndpoint $billing_agreements_endpoint ): bool {
try {
return $billing_agreements_endpoint->reference_transaction_enabled();
} catch ( RuntimeException $exception ) {
return false;
}
}
/**
* Converts the bool value to "yes" icon or dash.
*
* @param bool $value The value.
* @return string
*/
private function bool_to_html( bool $value ): string {
return $value
? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'
: '<mark class="no">&ndash;</mark>';
}
}

View file

@ -23,12 +23,14 @@ return array(
$endpoint = $container->get( 'api.endpoint.order' );
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' );
$environment = $container->get( 'onboarding.environment' );
return new RenewalHandler(
$logger,
$repository,
$endpoint,
$purchase_unit_factory,
$payer_factory
$payer_factory,
$environment
);
},
'subscription.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository {

View file

@ -0,0 +1,94 @@
<?php
/**
* Helper trait for the subscriptions handling.
*
* @package WooCommerce\PayPalCommerce\Subscription
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WC_Order;
use WC_Product;
use WC_Subscription;
use WC_Subscriptions_Product;
/**
* Class FreeTrialHandlerTrait
*/
trait FreeTrialHandlerTrait {
use SubscriptionsHandlerTrait;
/**
* Checks if the cart contains only free trial.
*
* @return bool
*/
protected function is_free_trial_cart(): bool {
if ( ! $this->is_wcs_plugin_active() ) {
return false;
}
$cart = WC()->cart;
if ( ! $cart || $cart->is_empty() || (float) $cart->get_total( 'numeric' ) > 0 ) {
return false;
}
foreach ( $cart->get_cart() as $item ) {
$product = $item['data'] ?? null;
if ( ! $product instanceof WC_Product ) {
continue;
}
if ( WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) {
return true;
}
}
return false;
}
/**
* Checks if the current product contains free trial.
*
* @return bool
*/
protected function is_free_trial_product(): bool {
if ( ! $this->is_wcs_plugin_active() ) {
return false;
}
$product = wc_get_product();
return $product
&& WC_Subscriptions_Product::is_subscription( $product )
&& WC_Subscriptions_Product::get_trial_length( $product ) > 0;
}
/**
* Checks if the given order contains only free trial.
*
* @param WC_Order $wc_order The WooCommerce order.
* @return bool
*/
protected function is_free_trial_order( WC_Order $wc_order ): bool {
if ( ! $this->is_wcs_plugin_active() ) {
return false;
}
if ( (float) $wc_order->get_total( 'numeric' ) > 0 ) {
return false;
}
$subs = wcs_get_subscriptions_for_order( $wc_order );
return ! empty(
array_filter(
$subs,
function ( WC_Subscription $sub ): bool {
return (float) $sub->get_total_initial_payment() <= 0;
}
)
);
}
}

View file

@ -11,6 +11,9 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription\Helper;
use WC_Product;
use WC_Subscriptions_Product;
/**
* Class SubscriptionHelper
*/
@ -26,7 +29,7 @@ class SubscriptionHelper {
return false;
}
$product = wc_get_product();
return is_a( $product, \WC_Product::class ) && $product->is_type( 'subscription' );
return $product && WC_Subscriptions_Product::is_subscription( $product );
}
/**
@ -44,10 +47,10 @@ class SubscriptionHelper {
}
foreach ( $cart->get_cart() as $item ) {
if ( ! isset( $item['data'] ) || ! is_a( $item['data'], \WC_Product::class ) ) {
if ( ! isset( $item['data'] ) || ! is_a( $item['data'], WC_Product::class ) ) {
continue;
}
if ( $item['data']->is_type( 'subscription' ) ) {
if ( $item['data']->is_type( 'subscription' ) || $item['data']->is_type( 'subscription_variation' ) ) {
return true;
}
}
@ -55,6 +58,25 @@ class SubscriptionHelper {
return false;
}
/**
* Whether pay for order contains subscriptions.
*
* @return bool
*/
public function order_pay_contains_subscription(): bool {
if ( ! $this->plugin_is_active() || ! is_wc_endpoint_url( 'order-pay' ) ) {
return false;
}
global $wp;
$order_id = (int) $wp->query_vars['order-pay'];
if ( 0 === $order_id ) {
return false;
}
return $this->has_subscription( $order_id );
}
/**
* Whether only automatic payment gateways are accepted.
*
@ -65,12 +87,10 @@ class SubscriptionHelper {
if ( ! $this->plugin_is_active() ) {
return false;
}
$accept_manual_renewals = ( 'no' !== get_option(
//phpcs:disable Inpsyde.CodeQuality.VariablesName.SnakeCaseVar
$accept_manual_renewals = 'no' !== get_option(
\WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals',
//phpcs:enable Inpsyde.CodeQuality.VariablesName.SnakeCaseVar
'no'
) ) ? true : false;
);
return ! $accept_manual_renewals;
}

View file

@ -10,21 +10,26 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
/**
* Class RenewalHandler
*/
class RenewalHandler {
use OrderMetaTrait;
use TransactionIdHandlingTrait;
use PaymentsStatusHandlingTrait;
/**
* The logger.
*
@ -60,6 +65,13 @@ class RenewalHandler {
*/
private $payer_factory;
/**
* The environment.
*
* @var Environment
*/
protected $environment;
/**
* RenewalHandler constructor.
*
@ -68,13 +80,15 @@ class RenewalHandler {
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param PayerFactory $payer_factory The payer factory.
* @param Environment $environment The environment.
*/
public function __construct(
LoggerInterface $logger,
PaymentTokenRepository $repository,
OrderEndpoint $order_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
PayerFactory $payer_factory
PayerFactory $payer_factory,
Environment $environment
) {
$this->logger = $logger;
@ -82,6 +96,7 @@ class RenewalHandler {
$this->order_endpoint = $order_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->payer_factory = $payer_factory;
$this->environment = $environment;
}
/**
@ -90,52 +105,23 @@ class RenewalHandler {
* @param \WC_Order $wc_order The WooCommerce order.
*/
public function renew( \WC_Order $wc_order ) {
$this->logger->log(
'info',
sprintf(
// translators: %d is the id of the order.
__( 'Start moneytransfer for order %d', 'woocommerce-paypal-payments' ),
(int) $wc_order->get_id()
),
array(
'order' => $wc_order,
)
);
try {
$this->process_order( $wc_order );
} catch ( \Exception $error ) {
$this->logger->log(
'error',
$this->logger->error(
sprintf(
// translators: %1$d is the order number, %2$s the error message.
__(
'An error occured while trying to renew the subscription for order %1$d: %2$s',
'woocommerce-paypal-payments'
),
(int) $wc_order->get_id(),
'An error occurred while trying to renew the subscription for order %1$d: %2$s',
$wc_order->get_id(),
$error->getMessage()
),
array(
'order' => $wc_order,
)
);
return;
}
$this->logger->log(
'info',
$this->logger->info(
sprintf(
// translators: %d is the order number.
__(
'Moneytransfer for order %d is completed.',
'woocommerce-paypal-payments'
),
(int) $wc_order->get_id()
),
array(
'order' => $wc_order,
'Renewal for order %d is completed.',
$wc_order->get_id()
)
);
}
@ -164,7 +150,19 @@ class RenewalHandler {
$token
);
$this->capture_order( $order, $wc_order );
$this->add_paypal_meta( $wc_order, $order, $this->environment );
if ( $order->intent() === 'AUTHORIZE' ) {
$order = $this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' );
}
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$this->update_transaction_id( $transaction_id, $wc_order );
}
$this->handle_new_order_status( $order, $wc_order );
}
/**
@ -176,6 +174,9 @@ class RenewalHandler {
* @return PaymentToken|null
*/
private function get_token_for_customer( \WC_Customer $customer, \WC_Order $wc_order ) {
/**
* Returns a payment token for a customer, or null.
*/
$token = apply_filters( 'woocommerce_paypal_payments_subscriptions_get_token_for_customer', null, $customer, $wc_order );
if ( null !== $token ) {
return $token;
@ -185,12 +186,8 @@ class RenewalHandler {
if ( ! $tokens ) {
$error_message = sprintf(
// translators: %d is the customer id.
__(
'Payment failed. No payment tokens found for customer %d.',
'woocommerce-paypal-payments'
),
(int) $customer->get_id()
'Payment failed. No payment tokens found for customer %d.',
$customer->get_id()
);
$wc_order->update_status(
@ -198,14 +195,7 @@ class RenewalHandler {
$error_message
);
$this->logger->log(
'error',
$error_message,
array(
'customer' => $customer,
'order' => $wc_order,
)
);
$this->logger->error( $error_message );
}
$subscription = function_exists( 'wcs_get_subscription' ) ? wcs_get_subscription( $wc_order->get_meta( '_subscription_renewal' ) ) : null;
@ -223,25 +213,4 @@ class RenewalHandler {
return current( $tokens );
}
/**
* If the PayPal order is captured/authorized the WooCommerce order gets updated accordingly.
*
* @param Order $order The PayPal order.
* @param \WC_Order $wc_order The related WooCommerce order.
*/
private function capture_order( Order $order, \WC_Order $wc_order ) {
if ( $order->intent() === 'CAPTURE' && $order->status()->is( OrderStatus::COMPLETED ) ) {
$wc_order->update_status(
'processing',
__( 'Payment received.', 'woocommerce-paypal-payments' )
);
}
if ( $order->intent() === 'AUTHORIZE' ) {
$this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' );
}
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* Helper trait for the free trial subscriptions handling.
*
* @package WooCommerce\PayPalCommerce\Subscription
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WC_Subscriptions;
/**
* Class SubscriptionsHandlerTrait
*/
trait SubscriptionsHandlerTrait {
/**
* Whether the subscription plugin is active or not.
*
* @return bool
*/
protected function is_wcs_plugin_active(): bool {
return class_exists( WC_Subscriptions::class );
}
}

View file

@ -14,30 +14,47 @@ use WooCommerce\PayPalCommerce\Vaulting\Assets\MyAccountPaymentsAssets;
use WooCommerce\PayPalCommerce\Vaulting\Endpoint\DeletePaymentTokenEndpoint;
return array(
'vaulting.module-url' => static function ( ContainerInterface $container ): string {
'vaulting.module-url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-vaulting/',
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'vaulting.assets.myaccount-payments' => function( ContainerInterface $container ) : MyAccountPaymentsAssets {
'vaulting.assets.myaccount-payments' => function( ContainerInterface $container ) : MyAccountPaymentsAssets {
return new MyAccountPaymentsAssets(
$container->get( 'vaulting.module-url' )
$container->get( 'vaulting.module-url' ),
$container->get( 'ppcp.asset-version' )
);
},
'vaulting.payment-tokens-renderer' => static function (): PaymentTokensRenderer {
'vaulting.payment-tokens-renderer' => static function (): PaymentTokensRenderer {
return new PaymentTokensRenderer();
},
'vaulting.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository {
'vaulting.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository {
$factory = $container->get( 'api.factory.payment-token' );
$endpoint = $container->get( 'api.endpoint.payment-token' );
return new PaymentTokenRepository( $factory, $endpoint );
},
'vaulting.endpoint.delete' => function( ContainerInterface $container ) : DeletePaymentTokenEndpoint {
'vaulting.endpoint.delete' => function( ContainerInterface $container ) : DeletePaymentTokenEndpoint {
return new DeletePaymentTokenEndpoint(
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'button.request-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'vaulting.payment-token-checker' => function( ContainerInterface $container ) : PaymentTokenChecker {
return new PaymentTokenChecker(
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'api.repository.order' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.processor.authorized-payments' ),
$container->get( 'api.endpoint.payments' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'vaulting.customer-approval-listener' => function( ContainerInterface $container ) : CustomerApprovalListener {
return new CustomerApprovalListener(
$container->get( 'api.endpoint.payment-token' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -23,15 +23,25 @@ class MyAccountPaymentsAssets {
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* MyAccountPaymentsAssets constructor.
*
* @param string $module_url The URL to the module.
* @param string $version The assets version.
*/
public function __construct(
string $module_url
string $module_url,
string $version
) {
$this->module_url = untrailingslashit( $module_url );
$this->version = $version;
}
/**
@ -42,9 +52,9 @@ class MyAccountPaymentsAssets {
public function enqueue(): void {
wp_enqueue_script(
'ppcp-vaulting-myaccount-payments',
$this->module_url . '/assets/js/myaccount-payments.js',
untrailingslashit( $this->module_url ) . '/assets/js/myaccount-payments.js',
array( 'jquery' ),
'1',
$this->version,
true
);
}
@ -58,7 +68,7 @@ class MyAccountPaymentsAssets {
'PayPalCommerceGatewayVaulting',
array(
'delete' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( DeletePaymentTokenEndpoint::ENDPOINT ) ),
'endpoint' => \WC_AJAX::get_endpoint( DeletePaymentTokenEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeletePaymentTokenEndpoint::nonce() ),
),
)

View file

@ -0,0 +1,78 @@
<?php
/**
* Confirm approval token after the PayPal vaulting approval by customer (v2/vault/payment-tokens with CUSTOMER_ACTION_REQUIRED response).
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
/**
* Class CustomerApprovalListener
*/
class CustomerApprovalListener {
use FreeTrialHandlerTrait;
/**
* The PaymentTokenEndpoint.
*
* @var PaymentTokenEndpoint
*/
private $payment_token_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CustomerApprovalListener constructor.
*
* @param PaymentTokenEndpoint $payment_token_endpoint The PaymentTokenEndpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct( PaymentTokenEndpoint $payment_token_endpoint, LoggerInterface $logger ) {
$this->payment_token_endpoint = $payment_token_endpoint;
$this->logger = $logger;
}
/**
* Listens for redirects after the PayPal vaulting approval by customer.
*
* @return void
*/
public function listen(): void {
$token = filter_input( INPUT_GET, 'approval_token_id', FILTER_SANITIZE_STRING );
if ( ! is_string( $token ) ) {
return;
}
$url = (string) filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL );
try {
$query = wp_parse_url( $url, PHP_URL_QUERY );
if ( $query && str_contains( $query, 'ppcp_vault=cancel' ) ) {
return;
}
try {
$this->payment_token_endpoint->create_from_approval_token( $token, get_current_user_id() );
} catch ( Exception $exception ) {
$this->logger->error( 'Failed to create payment token. ' . $exception->getMessage() );
}
} finally {
wp_safe_redirect( remove_query_arg( array( 'ppcp_vault', 'approval_token_id', 'approval_session_id' ), $url ) );
exit();
}
}
}

View file

@ -0,0 +1,189 @@
<?php
/**
* Check if payment token is saved and updates order accordingly.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class PaymentTokenChecker
*/
class PaymentTokenChecker {
use FreeTrialHandlerTrait;
/**
* The payment token repository.
*
* @var PaymentTokenRepository
*/
protected $payment_token_repository;
/**
* The order repository.
*
* @var OrderRepository
*/
protected $order_repository;
/**
* The settings.
*
* @var Settings
*/
protected $settings;
/**
* The authorized payments processor.
*
* @var AuthorizedPaymentsProcessor
*/
protected $authorized_payments_processor;
/**
* The payments endpoint.
*
* @var PaymentsEndpoint
*/
protected $payments_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* PaymentTokenChecker constructor.
*
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param OrderRepository $order_repository The order repository.
* @param Settings $settings The settings.
* @param AuthorizedPaymentsProcessor $authorized_payments_processor The authorized payments processor.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
PaymentTokenRepository $payment_token_repository,
OrderRepository $order_repository,
Settings $settings,
AuthorizedPaymentsProcessor $authorized_payments_processor,
PaymentsEndpoint $payments_endpoint,
LoggerInterface $logger
) {
$this->payment_token_repository = $payment_token_repository;
$this->order_repository = $order_repository;
$this->settings = $settings;
$this->authorized_payments_processor = $authorized_payments_processor;
$this->payments_endpoint = $payments_endpoint;
$this->logger = $logger;
}
/**
* Check if payment token exist and updates order accordingly.
*
* @param int $order_id The order ID.
* @param int $customer_id The customer ID.
* @param string $intent The intent from settings when order was created.
* @return void
*/
public function check_and_update( int $order_id, int $customer_id, string $intent ):void {
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return;
}
if ( $wc_order->get_status() === 'processing' || 'capture' !== $intent ) {
return;
}
$tokens = $this->payment_token_repository->all_for_user_id( $customer_id );
if ( $tokens ) {
try {
if ( $this->is_free_trial_order( $wc_order ) ) {
if ( CreditCardGateway::ID === $wc_order->get_payment_method() ) {
$order = $this->order_repository->for_wc_order( $wc_order );
$this->authorized_payments_processor->void_authorizations( $order );
$wc_order->payment_complete();
}
return;
}
$this->capture_authorized_payment( $wc_order );
} catch ( Exception $exception ) {
$this->logger->error( $exception->getMessage() );
}
return;
}
$this->logger->error( "Payment for subscription parent order #{$order_id} was not saved on PayPal." );
try {
$order = $this->order_repository->for_wc_order( $wc_order );
$this->authorized_payments_processor->void_authorizations( $order );
} catch ( RuntimeException $exception ) {
$this->logger->warning( $exception->getMessage() );
}
$this->update_failed_status( $wc_order );
}
/**
* Captures authorized payments for the given WC order.
*
* @param WC_Order $wc_order The WC order.
* @throws Exception When there is a problem capturing the payment.
*/
private function capture_authorized_payment( WC_Order $wc_order ): void {
if ( $this->settings->has( 'intent' ) && strtoupper( (string) $this->settings->get( 'intent' ) ) === 'CAPTURE' ) {
if ( ! $this->authorized_payments_processor->capture_authorized_payment( $wc_order ) ) {
throw new Exception( "Could not capture payment for order: #{$wc_order->get_id()}" );
}
}
}
/**
* Updates WC order and subscription status to failed and canceled respectively.
*
* @param WC_Order $wc_order The WC order.
*/
private function update_failed_status( WC_Order $wc_order ): void {
$error_message = __( 'Could not process order because it was not possible to save the payment on PayPal.', 'woocommerce-paypal-payments' );
$wc_order->update_status( 'failed', $error_message );
/**
* Function already exist in Subscription plugin
*
* @psalm-suppress UndefinedFunction
*/
$subscriptions = wcs_get_subscriptions_for_order( $wc_order->get_id() );
foreach ( $subscriptions as $key => $subscription ) {
if ( $subscription->get_parent_id() === $wc_order->get_id() ) {
try {
$subscription->update_status( 'cancelled' );
break;
} catch ( Exception $exception ) {
$this->logger->error( "Could not update cancelled status on subscription #{$subscription->get_id()} " . $exception->getMessage() );
}
}
}
}
}

View file

@ -38,6 +38,16 @@ class VaultingModule implements ModuleInterface {
*/
public function run( ContainerInterface $container ): void {
$settings = $container->get( 'wcgateway.settings' );
if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) {
return;
}
$listener = $container->get( 'vaulting.customer-approval-listener' );
assert( $listener instanceof CustomerApprovalListener );
$listener->listen();
add_filter(
'woocommerce_account_menu_items',
function( $menu_links ) {
@ -93,6 +103,17 @@ class VaultingModule implements ModuleInterface {
}
);
$subscription_helper = $container->get( 'subscription.helper' );
add_action(
'woocommerce_created_customer',
function( int $customer_id ) use ( $subscription_helper ) {
$guest_customer_id = WC()->session->get( 'ppcp_guest_customer_id' );
if ( $guest_customer_id && $subscription_helper->cart_contains_subscription() ) {
update_user_meta( $customer_id, 'ppcp_guest_customer_id', $guest_customer_id );
}
}
);
$asset_loader = $container->get( 'vaulting.assets.myaccount-payments' );
add_action(
'wp_enqueue_scripts',
@ -113,6 +134,16 @@ class VaultingModule implements ModuleInterface {
$endpoint->handle_request();
}
);
add_action(
'woocommerce_paypal_payments_check_saved_payment',
function ( int $order_id, int $customer_id, string $intent ) use ( $container ) {
$payment_token_checker = $container->get( 'vaulting.payment-token-checker' );
$payment_token_checker->check_and_update( $order_id, $customer_id, $intent );
},
10,
3
);
}
/**

View file

@ -1722,9 +1722,9 @@ mimic-fn@^2.1.0:
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
ms@2.1.2:
version "2.1.2"

View file

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Amex_acceptancemark_50x50</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Amex_acceptancemark_50x50">
<polygon id="Shape" fill="#FFFFFF" fill-rule="nonzero" points="50 0 0 0 0 50 50 50"></polygon>
<path d="M25.0930414,18.1130368 L21.2948396,18.1130368 L23.1939405,13.5354022 L25.0930414,18.1130368 Z M27.5916246,24.0501324 L31.8892892,24.0501324 L25.7128236,10.0571358 L20.7951652,10.0571358 L14.6182767,24.0501324 L18.8161334,24.0501324 L19.9755555,21.2514908 L26.412114,21.2514908 L27.5916246,24.0501324 Z M46.2218867,24.0501324 L50,24.0501324 L50,10.0571358 L44.1229584,10.0571358 L40.9845044,18.7725731 L37.8661389,10.0571358 L31.8892892,10.0571358 L31.8892892,24.0501324 L35.6671911,24.0501324 L35.6671911,14.255204 L39.2655654,24.0501324 L42.6237239,24.0501324 L46.2218867,14.234904 L46.2218867,24.0501324 Z M23.7946916,36.8350715 L23.7946916,34.6562122 L31.6905191,34.6562122 L31.6905191,31.4978812 L23.7946916,31.4978812 L23.7946916,29.3190219 L31.8903465,29.3190219 L31.8903465,26.0807599 L19.9766128,26.0807599 L19.9766128,40.0733335 L31.8903465,40.0733335 L31.8903465,36.8350715 L23.7946916,36.8350715 Z M46.1576036,33.0427906 L50,37.1306893 L50,28.989148 L46.1576036,33.0427906 Z M44.9840138,40.0733335 L50,40.0733335 L43.3646714,33.0370812 L50,26.0807599 L45.0637333,26.0807599 L40.9661076,30.558375 L36.9078129,26.0807599 L31.8907695,26.0807599 L38.4871899,33.0770467 L31.8907695,40.0733335 L36.7682509,40.0733335 L40.8861766,35.5557529 L44.9840138,40.0733335 Z M50,50 L50,42.0773174 L43.9679599,42.0773174 L40.8622819,38.6432456 L37.7411674,42.0773174 L17.8527325,42.0773174 L17.8527325,26.072513 L11.4335135,26.072513 L19.3957387,8.05336344 L27.0746107,8.05336344 L29.8157358,14.2264457 L29.8157358,8.05336344 L39.3205444,8.05336344 L40.9709711,12.7052196 L42.6319707,8.05336344 L50,8.05336344 L50,0 L0,0 L0,50 L50,50 L50,50 Z" id="Fill-18" fill="#216EA9"></path>
<svg width="134px" height="85px" viewBox="0 0 134 85" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AmEx Card Icon</title>
<g id="RD-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="LP---venmo---social---primary" transform="translate(-824.000000, -1587.000000)">
<g id="AmEx-Card-Icon" transform="translate(824.988647, 1587.578995)">
<rect id="Rectangle" fill="#216EA9" x="0" y="0" width="132.39159" height="83.7921454" rx="4.581738"></rect>
<g id="Amex" transform="translate(11.042625, 26.813487)" fill-rule="nonzero">
<polygon id="path3082" fill="#FFFFFF" points="58.1781633 28.829246 58.1781633 0.111970975 88.752584 0.111970975 92.0328881 4.36473322 95.4216632 0.111970975 111.627357 0.111970975 100.111543 14.3998649 111.627357 28.829246 95.1623158 28.829246 91.8866983 24.4852763 88.560787 28.829246"></polygon>
<polygon id="path3080" fill="#FFFFFF" points="12.9456251 0.111970975 24.8692065 0.111970975 28.7820013 9.06638213 28.7820013 0.111970975 43.6041122 0.111970975 45.933401 6.58395398 48.1915676 0.111970975 83.0100213 0.111970975 83.0100213 28.829228 23.1027999 28.829228 21.2713726 24.3023659 16.3877429 24.3023659 14.5692593 28.829228 0.216820214 28.829228"></polygon>
<path d="M15.3859151,3.6521988 L6.08653439,25.1547742 L12.140911,25.1547742 L13.8567623,20.8489817 L23.8318149,20.8489817 L25.5387763,25.1547742 L31.7265095,25.1547742 L22.436018,3.6521988 L15.3859151,3.6521988 Z M18.8265072,8.6564979 L21.8670326,16.1806509 L15.7770926,16.1806509 L18.8265072,8.6564979 L18.8265072,8.6564979 Z" id="path3046" fill="#216EA9"></path>
<polygon id="path3048" fill="#216EA9" points="32.3666185 25.1511618 32.3666185 3.64856809 40.9702248 3.68034593 45.9743744 17.543851 50.8586955 3.64856809 59.3934994 3.64856809 59.3934994 25.1511618 53.9881232 25.1511618 53.9881232 9.30714548 48.2583106 25.1511618 43.5177841 25.1511618 37.7719947 9.30714548 37.7719947 25.1511618"></polygon>
<polygon id="path3050" fill="#216EA9" points="63.091914 25.1511618 63.091914 3.64856809 80.7305103 3.64856809 80.7305103 8.45836792 68.5541888 8.45836792 68.5541888 12.1364341 80.4460167 12.1364341 80.4460167 16.6632958 68.5541888 16.6632958 68.5541888 20.482831 80.7305103 20.482831 80.7305103 25.1511618"></polygon>
<polygon id="path3066" fill="#216EA9" points="80.1997365 25.1511618 88.7878839 14.5324773 79.9952578 3.64856809 86.80532 3.64856809 92.0417773 10.3769555 97.2960166 3.64856809 103.839367 3.64856809 95.1623158 14.3998648 103.766272 25.1511618 96.9572869 25.1511618 91.8728606 18.5288349 86.9120057 25.1511618"></polygon>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="88px" height="55px" viewBox="0 0 88 55" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>iDEAL_acceptancemark_80x50</title>
<g id="RD-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="LP---venmo---social---primary" transform="translate(-1290.000000, -1489.000000)">
<g id="iDEAL_acceptancemark_80x50" transform="translate(1290.674250, 1489.000000)">
<rect id="Rectangle" fill="#000000" x="0" y="0" width="86.55975" height="54.78165" rx="3.55725"></rect>
<g id="iDeal" transform="translate(19.722475, 8.569431)" fill-rule="nonzero">
<g id="Group-10">
<path d="M1.31483165,0.4147596 L1.31483165,38.542788 L23.6150559,38.542788 C38.3257411,38.542788 44.7042759,30.3306152 44.7042759,19.4407953 C44.7042759,8.59428425 38.3257411,0.4147596 23.6150559,0.4147596 L1.31483165,0.4147596 Z" id="Fill-6" fill="#FFFFFF"></path>
<path d="M24.4089797,0.0145181949 C43.6643857,0.0145181949 46.5394168,12.2271201 46.5394168,19.5055435 C46.5394168,32.133495 38.680567,39.0878016 24.4089797,39.0878016 L0.807144801,39.0878016 L0.807144801,0.0145181949 L24.4089797,0.0145181949 Z M2.66510307,1.8517804 L2.66510307,37.249739 L24.4089797,37.249739 C37.5392931,37.249739 44.6818631,31.2003587 44.6818631,19.5055435 C44.6818631,7.48721161 36.9395275,1.8517804 24.4089797,1.8517804 L2.66510307,1.8517804 Z" id="Fill-8" fill="#000000"></path>
</g>
<polygon id="Fill-11" fill="#000000" points="5.58237061 34.364139 12.5430893 34.364139 12.5430893 22.1081216 5.58237061 22.1081216"></polygon>
<g id="Group-16" transform="translate(4.652482, 4.673437)">
<path d="M8.73559986,11.2615791 C8.73559986,13.6248309 6.79935854,15.5409215 4.40980375,15.5409215 C2.02146263,15.5409215 0.0834007826,13.6248309 0.0834007826,11.2615791 C0.0834007826,8.89992785 2.02146263,6.98303694 4.40980375,6.98303694 C6.79935854,6.98303694 8.73559986,8.89992785 8.73559986,11.2615791" id="Fill-12" fill="#000000"></path>
<path d="M22.3930799,13.2641209 L22.3930799,15.2470356 L17.4327257,15.2470356 L17.4327257,7.28636627 L22.233075,7.28636627 L22.233075,9.26848068 L19.4381473,9.26848068 L19.4381473,10.1914133 L22.0813635,10.1914133 L22.0813635,12.1731276 L19.4381473,12.1731276 L19.4381473,13.2641209 L22.3930799,13.2641209 Z M23.3559412,15.2496365 L25.7841318,7.28296504 L28.6367095,7.28296504 L31.0640911,15.2496365 L28.9767452,15.2496365 L28.5218135,13.708481 L25.8990278,13.708481 L25.4426801,15.2496365 L23.3559412,15.2496365 Z M26.4850382,11.7269668 L27.9356009,11.7269668 L27.2723189,9.47555532 L27.1519613,9.47555532 L26.4850382,11.7269668 Z M32.0714544,7.2831651 L34.0764715,7.2831651 L34.0764715,13.2641209 L37.0477888,13.2641209 C36.2329978,2.4118093 27.6012291,0.0671642083 19.7581572,0.0671642083 L11.3960302,0.0671642083 L11.3960302,7.28736663 L12.6337925,7.28736663 C14.8904481,7.28736663 16.2924632,8.80091216 16.2924632,11.2355897 C16.2924632,13.7476951 14.9242292,15.2470356 12.6337925,15.2470356 L11.3960302,15.2470356 L11.3960302,29.6942429 L19.7581572,29.6942429 C32.5104059,29.6942429 36.9480638,23.8371318 37.1046299,15.2470356 L32.0714544,15.2470356 L32.0714544,7.2831651 Z M11.394412,9.26988119 L11.394412,13.2641209 L12.6337925,13.2641209 C13.492681,13.2641209 14.2858279,13.0186324 14.2858279,11.2355897 C14.2858279,9.49336174 13.4030701,9.26988119 12.6337925,9.26988119 L11.394412,9.26988119 Z" id="Fill-14" fill="#CC0066"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="134px" height="84px" viewBox="0 0 134 84" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Mastercard_acceptancemark_80x50</title>
<g id="RD-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="LP---venmo---social---primary" transform="translate(-967.000000, -1489.000000)">
<g id="Mastercard_acceptancemark_80x50" transform="translate(967.005261, 1489.000000)">
<rect id="Rectangle" fill="#000000" x="0" y="0" width="132.39159" height="83.7921454" rx="4.581738"></rect>
<g id="mc_symbol" transform="translate(16.758429, 11.730900)" fill-rule="nonzero">
<rect id="Rectangle" fill="#FF5F00" x="34.961091" y="6.91411461" width="29.2905829" height="48.0845243"></rect>
<path d="M37.978038,30.9606238 C37.9706835,21.5790039 42.2555184,12.7149877 49.5979316,6.92260862 C37.1243267,-2.93038543 19.2117299,-1.49521164 8.44863276,10.2195284 C-2.31446439,21.9342684 -2.31446439,39.9954731 8.44863276,51.7102132 C19.2117299,63.4249532 37.1243267,64.8601271 49.5979316,55.0071329 C42.2531818,49.2129145 37.9680851,40.3452244 37.978038,30.9606238 Z" id="Path" fill="#EB001B"></path>
<path d="M95.9169404,50.3974171 L95.9169404,49.0088616 L96.3394821,49.0088616 L96.3394821,48.7215742 L95.333833,48.7215742 L95.333833,49.0088616 L95.7310222,49.0088616 L95.7310222,50.3974171 L95.9169404,50.3974171 Z M97.8690825,50.3974171 L97.8690825,48.7215742 L97.5648527,48.7215742 L97.2099177,49.9186049 L96.8549827,48.7215742 L96.5507529,48.7215742 L96.5507529,50.3974171 L96.7704745,50.3974171 L96.7704745,49.1285648 L97.1000568,50.2178626 L97.3282294,50.2178626 L97.6578119,49.1285648 L97.6578119,50.3974171 L97.8690825,50.3974171 Z" id="Shape" fill="#F79E1B"></path>
<path d="M98.8240266,30.9606238 C98.8240266,42.6682247 92.1727326,53.3479581 81.695674,58.4641002 C71.2186153,63.5802422 58.7549062,62.2345869 49.5979316,54.9986391 C56.9371412,49.2013423 61.2222985,40.3385046 61.2222985,30.9563768 C61.2222985,21.5742491 56.9371412,12.7114113 49.5979316,6.91411461 C58.7549062,-0.321833201 71.2186153,-1.66748859 81.695674,3.44865344 C92.1727326,8.56479549 98.8240266,19.2445288 98.8240266,30.9521297 L98.8240266,30.9606238 Z" id="Path" fill="#F79E1B"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show more