Merge branch 'trunk' into modularity-full-migration

# Conflicts:
#	modules/ppcp-blocks/src/BlocksModule.php
This commit is contained in:
Pedro Silva 2023-12-19 10:28:47 +00:00
commit 5837d65b18
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
150 changed files with 8479 additions and 3005 deletions

View file

@ -2,6 +2,12 @@
if (!defined('PAYPAL_INTEGRATION_DATE')) {
define('PAYPAL_INTEGRATION_DATE', '2023-06-02');
}
if (!defined('PAYPAL_URL')) {
define( 'PAYPAL_URL', 'https://www.paypal.com' );
}
if (!defined('PAYPAL_SANDBOX_URL')) {
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
}
if (!defined('EP_PAGES')) {
define('EP_PAGES', 4096);
}

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\PPCP;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
@ -47,6 +48,19 @@ function ppcp_get_paypal_order( $paypal_id_or_wc_order ): Order {
return $order_endpoint->order( $paypal_id_or_wc_order );
}
/**
* Creates a PayPal order for the given WC order.
*
* @param WC_Order $wc_order The WC order.
* @throws Exception When the operation fails.
*/
function ppcp_create_paypal_order_for_wc_order( WC_Order $wc_order ): Order {
$order_processor = PPCP::container()->get( 'wcgateway.order-processor' );
assert( $order_processor instanceof OrderProcessor );
return $order_processor->create_order( $wc_order );
}
/**
* Captures the PayPal order.
*

View file

@ -1,5 +1,37 @@
*** Changelog ***
= 2.4.3 - xxxx-xx-xx =
* Fix - PayPal Subscription initiated without a WooCommerce order #1907
* Fix - Block Checkout reloads when submitting order with empty fields #1904
* Fix - "Send checkout billing and shipping data to Apple Pay" displayed when Apple Pay is disabled #1883
* Fix - "Order does not contain intent" error for ACDC renewals when triggering 3D Secure #1888
* Enhancement - Add button to reload feature eligibility status from Connection tab #1902
* Enhancement - Apple Pay validation message improvements #1901
* Enhancement - Improve support for Classic Cart & Classic Checkout blocks #1894
* Enhancement - Ensure uniform button appearance for PayPal, Google Pay, and Apple Pay buttons #1900
* Enhancement - remove string translations for package tracking carriers from repository #1885
* Enhancement - Incorrect margins when PayPal buttons are rendered as separate gateways. #1908
* Feature preview - Save payment methods (Vault v3) integration #1779
= 2.4.2 - 2023-12-04 =
* Fix - Action callback arguments count in ShipStation tracking integration #1841
* Fix - Google Pay scripts loading on unrelated admin pages #1834
* Fix - Do not ignore disabled APMs list in blocks #1865
* Fix - Display Package Tracking metabox below Order actions when HPOS is active #1850
* Fix - ApplePay use checkout form data to update shipping and billing #1832
* Fix - Fix Apple Pay CSS #1872
* Enhancement - Allow redirect to PayPal with "Place order" button if smart buttons failed to load #1840 #1870
* Enhancement - Extend list of supported countries for Package Tracking v2 integration #1848
* Enhancement - Improve Block Theme support for Pay Later messaging #1855
* Enhancement - Render block buttons separately and add block style settings #1858
* Enhancement - Enable Block Cart and Block Express Checkout button locations by default #1852
* Enhancement - Improve single product page button placement with Block themes #1847
* Enhancement - Remove the Home location from default enabled Pay Later messaging locations #1856
* Enhancement - Chrome browser detected as eligible for Apple Pay on settings page #1828
* Enhancement - Hide Apple Pay & Google Pay for subscription type products #1835
* Enhancement - Add Standard Card Button gateway styling settings & preview #1827
* Feature preview - Upgrade to new Hosted Card Fields for Advanced Card Processing #1843
= 2.4.1 - 2023-11-14 =
* Fix - Error "PayPal order ID not found in meta" prevents automations from triggering when buying subscription via third-party payment gateway #1822
* Fix - Card button subscription support declaration #1796

View file

@ -20,13 +20,14 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-onboarding/module.php" )(),
( require "$modules_dir/ppcp-session/module.php" )(),
( require "$modules_dir/ppcp-status-report/module.php" )(),
( require "$modules_dir/ppcp-subscription/module.php" )(),
( require "$modules_dir/ppcp-wc-subscriptions/module.php" )(),
( require "$modules_dir/ppcp-wc-gateway/module.php" )(),
( require "$modules_dir/ppcp-webhooks/module.php" )(),
( require "$modules_dir/ppcp-vaulting/module.php" )(),
( require "$modules_dir/ppcp-order-tracking/module.php" )(),
( require "$modules_dir/ppcp-uninstall/module.php" )(),
( require "$modules_dir/ppcp-blocks/module.php" )(),
( require "$modules_dir/ppcp-paypal-subscriptions/module.php" )(),
);
if ( apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
@ -60,5 +61,13 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-card-fields/module.php" )();
}
if ( apply_filters(
//phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.save_payment_methods_enabled',
getenv( 'PCP_SAVE_PAYMENT_METHODS' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-save-payment-methods/module.php" )();
}
return $modules;
};

View file

@ -9,12 +9,16 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
@ -50,7 +54,6 @@ 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;
@ -74,31 +77,40 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
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 {
// It seems this 'api.paypal-website-url' key is always overridden in ppcp-onboarding/services.php.
'api.paypal-website-url' => function( ContainerInterface $container ) : string {
return PAYPAL_URL;
},
'api.factory.paypal-checkout-url' => function( ContainerInterface $container ) : callable {
return function ( string $id ) use ( $container ): string {
return $container->get( 'api.paypal-website-url' ) . '/checkoutnow?token=' . $id;
};
},
'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' );
@ -114,7 +126,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' ),
@ -125,10 +137,10 @@ return array(
$container->get( 'api.helper.failure-registry' )
);
},
'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' ),
@ -138,7 +150,14 @@ return array(
$container->get( 'api.repository.customer' )
);
},
'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint {
'api.endpoint.payment-tokens' => static function( ContainerInterface $container ) : PaymentTokensEndpoint {
return new PaymentTokensEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint {
return new WebhookEndpoint(
$container->get( 'api.host' ),
@ -148,7 +167,7 @@ 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' ),
@ -156,7 +175,7 @@ return array(
$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' );
$settings = $container->get( 'wcgateway.settings' );
$customer_repository = $container->get( 'api.repository.customer' );
@ -168,7 +187,7 @@ return array(
$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' );
@ -181,7 +200,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(
@ -190,7 +209,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' );
@ -204,7 +223,7 @@ return array(
$intent = $settings->has( 'intent' ) && strtoupper( (string) $settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE';
$application_context_repository = $container->get( 'api.repository.application-context' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
return new OrderEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
@ -219,14 +238,14 @@ return array(
$bn_code
);
},
'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint {
'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.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts {
'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts {
return new CatalogProducts(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
@ -234,7 +253,7 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-plans' => static function( ContainerInterface $container ): BillingPlans {
'api.endpoint.billing-plans' => static function( ContainerInterface $container ): BillingPlans {
return new BillingPlans(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
@ -243,53 +262,60 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-subscriptions' => static function( ContainerInterface $container ): BillingSubscriptions {
'api.endpoint.billing-subscriptions' => static function( ContainerInterface $container ): BillingSubscriptions {
return new BillingSubscriptions(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository {
'api.endpoint.payment-method-tokens' => static function( ContainerInterface $container ): PaymentMethodTokensEndpoint {
return new PaymentMethodTokensEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'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 {
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
return new PartnerReferralsData( $dcc_applies );
},
'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.repository.customer' => static function( ContainerInterface $container ): CustomerRepository {
'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 {
'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 {
'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.payment-token-action-links' => static function ( ContainerInterface $container ) : PaymentTokenActionLinksFactory {
'api.factory.payment-token-action-links' => static function ( ContainerInterface $container ) : PaymentTokenActionLinksFactory {
return new PaymentTokenActionLinksFactory();
},
'api.factory.webhook' => static function ( ContainerInterface $container ): WebhookFactory {
'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(
@ -298,7 +324,7 @@ return array(
$container->get( 'api.factory.fraud-processor-response' )
);
},
'api.factory.refund' => static function ( ContainerInterface $container ): RefundFactory {
'api.factory.refund' => static function ( ContainerInterface $container ): RefundFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new RefundFactory(
$amount_factory,
@ -306,7 +332,7 @@ return array(
$container->get( 'api.factory.refund_payer' )
);
},
'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' );
$item_factory = $container->get( 'api.factory.item' );
@ -326,32 +352,32 @@ return array(
$sanitizer
);
},
'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 {
'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 {
return new ShippingFactory(
$container->get( 'api.factory.address' ),
$container->get( 'api.factory.shipping-option' )
);
},
'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory {
'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory {
return new ShippingPreferenceFactory();
},
'api.factory.shipping-option' => static function ( ContainerInterface $container ): ShippingOptionFactory {
'api.factory.shipping-option' => static function ( ContainerInterface $container ): ShippingOptionFactory {
return new ShippingOptionFactory(
$container->get( 'api.factory.money' )
);
},
'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,
@ -359,86 +385,84 @@ return array(
$container->get( 'api.shop.currency' )
);
},
'api.factory.money' => static function ( ContainerInterface $container ): MoneyFactory {
'api.factory.money' => static function ( ContainerInterface $container ): MoneyFactory {
return new MoneyFactory();
},
'api.factory.payer' => static function ( ContainerInterface $container ): PayerFactory {
'api.factory.payer' => static function ( ContainerInterface $container ): PayerFactory {
$address_factory = $container->get( 'api.factory.address' );
return new PayerFactory( $address_factory );
},
'api.factory.refund_payer' => static function ( ContainerInterface $container ): RefundPayerFactory {
'api.factory.refund_payer' => static function ( ContainerInterface $container ): RefundPayerFactory {
return new RefundPayerFactory();
},
'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 {
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' );
$application_context_factory = $container->get( 'api.factory.application-context' );
$payment_source_factory = $container->get( 'api.factory.payment-source' );
return new OrderFactory(
$purchase_unit_factory,
$payer_factory,
$application_context_repository,
$application_context_factory,
$payment_source_factory
$application_context_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' );
$refund_factory = $container->get( 'api.factory.refund' );
return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory );
},
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
return new AuthorizationFactory();
},
'api.factory.exchange-rate' => static function ( ContainerInterface $container ): ExchangeRateFactory {
'api.factory.exchange-rate' => static function ( ContainerInterface $container ): ExchangeRateFactory {
return new ExchangeRateFactory();
},
'api.factory.platform-fee' => static function ( ContainerInterface $container ): PlatformFeeFactory {
'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 {
'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.factory.seller-payable-breakdown' => static function ( ContainerInterface $container ): SellerPayableBreakdownFactory {
'api.factory.seller-payable-breakdown' => static function ( ContainerInterface $container ): SellerPayableBreakdownFactory {
return new SellerPayableBreakdownFactory(
$container->get( 'api.factory.money' ),
$container->get( 'api.factory.platform-fee' )
);
},
'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory {
'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory {
return new FraudProcessorResponseFactory();
},
'api.factory.product' => static function( ContainerInterface $container ): ProductFactory {
'api.factory.product' => static function( ContainerInterface $container ): ProductFactory {
return new ProductFactory();
},
'api.factory.billing-cycle' => static function( ContainerInterface $container ): BillingCycleFactory {
'api.factory.billing-cycle' => static function( ContainerInterface $container ): BillingCycleFactory {
return new BillingCycleFactory( $container->get( 'api.shop.currency' ) );
},
'api.factory.payment-preferences' => static function( ContainerInterface $container ):PaymentPreferencesFactory {
'api.factory.payment-preferences' => static function( ContainerInterface $container ):PaymentPreferencesFactory {
return new PaymentPreferencesFactory( $container->get( 'api.shop.currency' ) );
},
'api.factory.plan' => static function( ContainerInterface $container ): PlanFactory {
'api.factory.plan' => static function( ContainerInterface $container ): PlanFactory {
return new PlanFactory(
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' )
);
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
'api.factory.card-authentication-result-factory' => static function( ContainerInterface $container ): CardAuthenticationResultFactory {
return new CardAuthenticationResultFactory();
},
'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' ),
@ -447,7 +471,7 @@ return array(
);
},
'api.shop.currency' => static function ( ContainerInterface $container ) : string {
'api.shop.currency' => static function ( ContainerInterface $container ) : string {
$currency = get_woocommerce_currency();
if ( $currency ) {
return $currency;
@ -460,18 +484,18 @@ return array(
return $currency;
},
'api.shop.country' => static function ( ContainerInterface $container ) : string {
'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 {
'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 {
'api.shop.is-currency-supported' => static function ( ContainerInterface $container ) : bool {
return in_array(
$container->get( 'api.shop.currency' ),
$container->get( 'api.supported-currencies' ),
@ -480,7 +504,7 @@ return array(
},
'api.shop.is-latin-america' => static function ( ContainerInterface $container ): bool {
'api.shop.is-latin-america' => static function ( ContainerInterface $container ): bool {
return in_array(
$container->get( 'api.shop.country' ),
array(
@ -538,7 +562,7 @@ return array(
*
* From https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies/
*/
'api.supported-currencies' => static function ( ContainerInterface $container ) : array {
'api.supported-currencies' => static function ( ContainerInterface $container ) : array {
return array(
'AUD',
'BRL',
@ -571,7 +595,7 @@ return array(
/**
* The matrix which countries and currency combinations can be used for DCC.
*/
'api.dcc-supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
'api.dcc-supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for DCC.
*/
@ -893,7 +917,7 @@ return array(
/**
* 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 {
'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.
*/
@ -1062,7 +1086,7 @@ return array(
);
},
'api.psd2-countries' => static function ( ContainerInterface $container ) : array {
'api.psd2-countries' => static function ( ContainerInterface $container ) : array {
return array(
'AT',
'BE',
@ -1094,19 +1118,19 @@ return array(
'SE',
);
},
'api.order-helper' => static function( ContainerInterface $container ): OrderHelper {
'api.order-helper' => static function( ContainerInterface $container ): OrderHelper {
return new OrderHelper();
},
'api.helper.order-transient' => static function( ContainerInterface $container ): OrderTransient {
'api.helper.order-transient' => static function( ContainerInterface $container ): OrderTransient {
$cache = new Cache( 'ppcp-paypal-bearer' );
$purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
return new OrderTransient( $cache, $purchase_unit_sanitizer );
},
'api.helper.failure-registry' => static function( ContainerInterface $container ): FailureRegistry {
'api.helper.failure-registry' => static function( ContainerInterface $container ): FailureRegistry {
$cache = new Cache( 'ppcp-paypal-api-status-cache' );
return new FailureRegistry( $cache );
},
'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(
'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(
static function( ContainerInterface $container ): PurchaseUnitSanitizer {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
@ -1116,4 +1140,11 @@ return array(
return new PurchaseUnitSanitizer( $behavior, $line_name );
}
),
'api.user-id-token' => static function( ContainerInterface $container ): UserIdToken {
return new UserIdToken(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,105 @@
<?php
/**
* Generates user ID token for payer.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Authentication
*/
namespace WooCommerce\PayPalCommerce\ApiClient\Authentication;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WP_Error;
/**
* Class UserIdToken
*/
class UserIdToken {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* UserIdToken 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;
}
/**
* Returns `id_token` which uniquely identifies the payer.
*
* @param string $target_customer_id Vaulted customer id.
*
* @return string
*
* @throws PayPalApiException If the request fails.
* @throws RuntimeException If something unexpected happens.
*/
public function id_token( string $target_customer_id = '' ): string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=id_token';
if ( $target_customer_id ) {
$url = add_query_arg(
array(
'target_customer_id' => $target_customer_id,
),
$url
);
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/x-www-form-urlencoded',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException( $json, $status_code );
}
return $json->id_token;
}
}

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -27,7 +28,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ErrorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WP_Error;
@ -174,12 +175,15 @@ class OrderEndpoint {
/**
* Creates an order.
*
* @param PurchaseUnit[] $items The purchase unit items for the order.
* @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values.
* @param Payer|null $payer The payer off the order.
* @param PaymentToken|null $payment_token The payment token.
* @param string $paypal_request_id The paypal request id.
* @param string $user_action The user action.
* @param PurchaseUnit[] $items The purchase unit items for the order.
* @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values.
* @param Payer|null $payer The payer off the order.
* @param PaymentToken|null $payment_token The payment token.
* @param string $paypal_request_id The PayPal request id.
* @param string $user_action The user action.
* @param string $payment_method WC payment method.
* @param array $request_data Request data.
* @param PaymentSource|null $payment_source The payment source.
*
* @return Order
* @throws RuntimeException If the request fails.
@ -190,7 +194,10 @@ class OrderEndpoint {
Payer $payer = null,
PaymentToken $payment_token = null,
string $paypal_request_id = '',
string $user_action = ApplicationContext::USER_ACTION_CONTINUE
string $user_action = ApplicationContext::USER_ACTION_CONTINUE,
string $payment_method = '',
array $request_data = array(),
PaymentSource $payment_source = null
): Order {
$bearer = $this->bearer->bearer();
$data = array(
@ -217,11 +224,16 @@ class OrderEndpoint {
if ( $payment_token ) {
$data['payment_source']['token'] = $payment_token->to_array();
}
if ( $payment_source ) {
$data['payment_source'] = array(
$payment_source->name() => $payment_source->properties(),
);
}
/**
* The filter can be used to modify the order creation request body data.
*/
$data = apply_filters( 'ppcp_create_order_request_body_data', $data );
$data = apply_filters( 'ppcp_create_order_request_body_data', $data, $payment_method, $request_data );
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array(
'method' => 'POST',
@ -260,26 +272,25 @@ class OrderEndpoint {
);
throw $error;
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
$error = new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
$this->logger->warning(
sprintf(
'Failed to create order. PayPal API response: %1$s',
$error->getMessage()
),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$order = $this->order_factory->from_paypal_response( $json );
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );

View file

@ -0,0 +1,155 @@
<?php
/**
* The Payment Method Tokens endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class PaymentMethodTokensEndpoint
*/
class PaymentMethodTokensEndpoint {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* PaymentMethodTokensEndpoint 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 setup token.
*
* @param PaymentSource $payment_source The payment source.
*
* @return stdClass
*
* @throws RuntimeException When something when wrong with the request.
* @throws PayPalApiException When something when wrong setting up the token.
*/
public function setup_tokens( PaymentSource $payment_source ): stdClass {
$data = array(
'payment_source' => array(
$payment_source->name() => $payment_source->properties(),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v3/vault/setup-tokens';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'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 setup 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;
}
/**
* Creates a payment token for the given payment source.
*
* @param PaymentSource $payment_source The payment source.
*
* @return stdClass
*
* @throws RuntimeException When something when wrong with the request.
* @throws PayPalApiException When something when wrong setting up the token.
*/
public function payment_tokens( PaymentSource $payment_source ): stdClass {
$data = array(
'payment_source' => array(
$payment_source->name() => $payment_source->properties(),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'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 setup 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;
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* Payment tokens version 3 endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WP_Error;
/**
* Class PaymentTokensEndpoint
*/
class PaymentTokensEndpoint {
use RequestTrait;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* PaymentTokensEndpoint constructor.
*
* @param string $host The bearer.
* @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;
}
/**
* Deletes a payment token with the given id.
*
* @param string $id Payment token id.
*
* @return void
*
* @throws RuntimeException When something went wrong with the request.
* @throws PayPalApiException When something went wrong deleting the payment token.
*/
public function delete( string $id ): void {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens/' . $id;
$args = array(
'method' => 'DELETE',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException( $json, $status_code );
}
}
}

View file

@ -14,7 +14,6 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
*/
class CardAuthenticationResult {
const LIABILITY_SHIFT_POSSIBLE = 'POSSIBLE';
const LIABILITY_SHIFT_NO = 'NO';
const LIABILITY_SHIFT_UNKNOWN = 'UNKNOWN';

View file

@ -107,14 +107,6 @@ class Order {
$this->id = $id;
$this->application_context = $application_context;
$this->purchase_units = array_values(
array_filter(
$purchase_units,
static function ( $unit ): bool {
return is_a( $unit, PurchaseUnit::class );
}
)
);
$this->payer = $payer;
$this->order_status = $order_status;
$this->intent = ( 'CAPTURE' === $intent ) ? 'CAPTURE' : 'AUTHORIZE';
@ -236,9 +228,6 @@ class Order {
if ( $this->application_context() ) {
$order['application_context'] = $this->application_context()->to_array();
}
if ( $this->payment_source() ) {
$order['payment_source'] = $this->payment_source()->to_array();
}
return $order;
}

View file

@ -15,14 +15,15 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
* Class OrderStatus
*/
class OrderStatus {
const INTERNAL = 'INTERNAL';
const CREATED = 'CREATED';
const SAVED = 'SAVED';
const APPROVED = 'APPROVED';
const VOIDED = 'VOIDED';
const COMPLETED = 'COMPLETED';
const PENDING_APPROVAL = 'PENDING_APPROVAL';
const VALID_STATUS = array(
const INTERNAL = 'INTERNAL';
const CREATED = 'CREATED';
const SAVED = 'SAVED';
const APPROVED = 'APPROVED';
const VOIDED = 'VOIDED';
const COMPLETED = 'COMPLETED';
const PENDING_APPROVAL = 'PENDING_APPROVAL';
const PAYER_ACTION_REQUIRED = 'PAYER_ACTION_REQUIRED';
const VALID_STATUS = array(
self::INTERNAL,
self::CREATED,
self::SAVED,
@ -30,6 +31,7 @@ class OrderStatus {
self::VOIDED,
self::COMPLETED,
self::PENDING_APPROVAL,
self::PAYER_ACTION_REQUIRED,
);
/**

View file

@ -9,74 +9,53 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use stdClass;
/**
* Class PaymentSource
*/
class PaymentSource {
/**
* The card.
* Payment source name.
*
* @var PaymentSourceCard|null
* @var string
*/
private $card;
private $name;
/**
* The wallet.
* Payment source properties.
*
* @var PaymentSourceWallet|null
* @var object
*/
private $wallet;
private $properties;
/**
* PaymentSource constructor.
*
* @param PaymentSourceCard|null $card The card.
* @param PaymentSourceWallet|null $wallet The wallet.
* @param string $name Payment source name.
* @param object $properties Payment source properties.
*/
public function __construct(
PaymentSourceCard $card = null,
PaymentSourceWallet $wallet = null
) {
$this->card = $card;
$this->wallet = $wallet;
public function __construct( string $name, object $properties ) {
$this->name = $name;
$this->properties = $properties;
}
/**
* Returns the card.
* Payment source name.
*
* @return PaymentSourceCard|null
* @return string
*/
public function card() {
return $this->card;
public function name(): string {
return $this->name;
}
/**
* Returns the wallet.
* Payment source properties.
*
* @return PaymentSourceWallet|null
* @return object
*/
public function wallet() {
return $this->wallet;
}
/**
* Returns the array of the object.
*
* @return array
*/
public function to_array(): array {
$data = array();
if ( $this->card() ) {
$data['card'] = $this->card()->to_array();
}
if ( $this->wallet() ) {
$data['wallet'] = $this->wallet()->to_array();
}
return $data;
public function properties(): object {
return $this->properties;
}
}

View file

@ -1,123 +0,0 @@
<?php
/**
* The PaymentSourceCard object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentSourceCard
*/
class PaymentSourceCard {
/**
* The last digits of the card.
*
* @var string
*/
private $last_digits;
/**
* The brand.
*
* @var string
*/
private $brand;
/**
* The type.
*
* @var string
*/
private $type;
/**
* The authentication result.
*
* @var CardAuthenticationResult|null
*/
private $authentication_result;
/**
* PaymentSourceCard constructor.
*
* @param string $last_digits The last digits of the card.
* @param string $brand The brand of the card.
* @param string $type The type of the card.
* @param CardAuthenticationResult|null $authentication_result The authentication result.
*/
public function __construct(
string $last_digits,
string $brand,
string $type,
CardAuthenticationResult $authentication_result = null
) {
$this->last_digits = $last_digits;
$this->brand = $brand;
$this->type = $type;
$this->authentication_result = $authentication_result;
}
/**
* Returns the last digits.
*
* @return string
*/
public function last_digits(): string {
return $this->last_digits;
}
/**
* Returns the brand.
*
* @return string
*/
public function brand(): string {
return $this->brand;
}
/**
* Returns the type.
*
* @return string
*/
public function type(): string {
return $this->type;
}
/**
* Returns the authentication result.
*
* @return CardAuthenticationResult|null
*/
public function authentication_result() {
return $this->authentication_result;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
$data = array(
'last_digits' => $this->last_digits(),
'brand' => $this->brand(),
'type' => $this->type(),
);
if ( $this->authentication_result() ) {
$data['authentication_result'] = $this->authentication_result()->to_array();
}
return $data;
}
}

View file

@ -1,25 +0,0 @@
<?php
/**
* The PaymentSourcewallet.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentSourceWallet
*/
class PaymentSourceWallet {
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array();
}
}

View file

@ -14,7 +14,7 @@ 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\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;

View file

@ -0,0 +1,33 @@
<?php
/**
* The card authentication result factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
/**
* Class CardAuthenticationResultFactory
*/
class CardAuthenticationResultFactory {
/**
* Returns a card authentication result from the given response object.
*
* @param stdClass $authentication_result The authentication result object.
* @return CardAuthenticationResult
*/
public function from_paypal_response( stdClass $authentication_result ): CardAuthenticationResult {
return new CardAuthenticationResult(
$authentication_result->liability_shift ?? '',
$authentication_result->three_d_secure->enrollment_status ?? '',
$authentication_result->three_d_secure->authentication_status ?? ''
);
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
@ -48,13 +49,6 @@ class OrderFactory {
*/
private $application_context_factory;
/**
* The PaymentSource factory.
*
* @var PaymentSourceFactory
*/
private $payment_source_factory;
/**
* OrderFactory constructor.
*
@ -62,21 +56,18 @@ class OrderFactory {
* @param PayerFactory $payer_factory The Payer factory.
* @param ApplicationContextRepository $application_context_repository The Application Context repository.
* @param ApplicationContextFactory $application_context_factory The Application Context factory.
* @param PaymentSourceFactory $payment_source_factory The Payment Source factory.
*/
public function __construct(
PurchaseUnitFactory $purchase_unit_factory,
PayerFactory $payer_factory,
ApplicationContextRepository $application_context_repository,
ApplicationContextFactory $application_context_factory,
PaymentSourceFactory $payment_source_factory
ApplicationContextFactory $application_context_factory
) {
$this->purchase_unit_factory = $purchase_unit_factory;
$this->payer_factory = $payer_factory;
$this->application_context_repository = $application_context_repository;
$this->application_context_factory = $application_context_factory;
$this->payment_source_factory = $payment_source_factory;
}
/**
@ -152,9 +143,23 @@ class OrderFactory {
$application_context = ( isset( $order_data->application_context ) ) ?
$this->application_context_factory->from_paypal_response( $order_data->application_context )
: null;
$payment_source = ( isset( $order_data->payment_source ) ) ?
$this->payment_source_factory->from_paypal_response( $order_data->payment_source ) :
null;
$payment_source = null;
if ( isset( $order_data->payment_source ) ) {
$json_encoded_payment_source = wp_json_encode( $order_data->payment_source );
if ( $json_encoded_payment_source ) {
$payment_source_as_array = json_decode( $json_encoded_payment_source, true );
if ( $payment_source_as_array ) {
$name = array_key_first( $payment_source_as_array );
if ( $name ) {
$payment_source = new PaymentSource(
$name,
$order_data->payment_source->$name
);
}
}
}
}
return new Order(
$order_data->id,

View file

@ -1,53 +0,0 @@
<?php
/**
* The PaymentSource factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSourceCard;
/**
* Class PaymentSourceFactory
*/
class PaymentSourceFactory {
/**
* Returns a PaymentSource for a PayPal Response.
*
* @param \stdClass $data The JSON object.
*
* @return PaymentSource
*/
public function from_paypal_response( \stdClass $data ): PaymentSource {
$card = null;
$wallet = null;
if ( isset( $data->card ) ) {
$authentication_result = null;
if ( isset( $data->card->authentication_result ) ) {
$authentication_result = new CardAuthenticationResult(
isset( $data->card->authentication_result->liability_shift ) ?
(string) $data->card->authentication_result->liability_shift : '',
isset( $data->card->authentication_result->three_d_secure->enrollment_status ) ?
(string) $data->card->authentication_result->three_d_secure->enrollment_status : '',
isset( $data->card->authentication_result->three_d_secure->authentication_status ) ?
(string) $data->card->authentication_result->three_d_secure->authentication_status : ''
);
}
$card = new PaymentSourceCard(
isset( $data->card->last_digits ) ? (string) $data->card->last_digits : '',
isset( $data->card->brand ) ? (string) $data->card->brand : '',
isset( $data->card->type ) ? (string) $data->card->type : '',
$authentication_result
);
}
return new PaymentSource( $card, $wallet );
}
}

View file

@ -49,7 +49,9 @@ return array(
// Domain validation.
$domain_validation_text = __( 'Status: Domain validation failed ❌', 'woocommerce-paypal-payments' );
if ( $container->get( 'applepay.is_validated' ) ) {
if ( ! $container->get( 'applepay.has_validated' ) ) {
$domain_validation_text = __( 'The domain has not yet been validated. Use the Apple Pay button to validate the domain ❌', 'woocommerce-paypal-payments' );
} elseif ( $container->get( 'applepay.is_validated' ) ) {
$domain_validation_text = __( 'Status: Domain successfully validated ✔️', 'woocommerce-paypal-payments' );
}
@ -157,6 +159,7 @@ return array(
->action_visible( 'applepay_button_color' )
->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' )
->action_visible( 'applepay_checkout_data_mode' )
->to_array(),
)
),

View file

@ -1,63 +1,44 @@
#applepay-container, .ppcp-button-applepay {
.ppcp-button-applepay {
// Should replicate apm-button.scss sizes.
--apple-pay-button-height: 45px;
--apple-pay-button-min-height: 40px;
--apple-pay-button-min-height: 35px;
--apple-pay-button-width: 100%;
--apple-pay-button-max-width: 750px;
--apple-pay-button-border-radius: 4px;
--apple-pay-button-overflow: hidden;
margin:7px 0;
.ppcp-width-min & {
--apple-pay-button-height: 35px;
}
.ppcp-width-300 & {
--apple-pay-button-height: 45px;
}
.ppcp-width-500 & {
--apple-pay-button-height: 55px;
}
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
&.ppcp-button-minicart {
--apple-pay-button-display: block;
--apple-pay-button-height: 40px;
}
}
.woocommerce-checkout {
#applepay-container, .ppcp-button-applepay {
margin-top: 0.5em;
--apple-pay-button-border-radius: 4px;
--apple-pay-button-height: 45px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {
.ppcp-button-applepay {
--apple-pay-button-margin: 0;
.ppcp-has-applepay-block {
.wp-block-woocommerce-checkout {
#applepay-container, .ppcp-button-applepay {
--apple-pay-button-margin: 0;
--apple-pay-button-height: 40px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.wp-block-woocommerce-cart {
#applepay-container, .ppcp-button-applepay {
--apple-pay-button-margin: 0;
--apple-pay-button-height: 40px;
}
/* Workaround for blocks grid */
.wc-block-components-express-payment__event-buttons {
--apple-pay-button-display: block;
li[id*="express-payment-method-ppcp-"] {
--apple-pay-button-padding-bottom: 0;
}
apple-pay-button {
min-width: 0;
width: 100%;
--apple-pay-button-width-default: 100%;
}
}
}
.wp-admin {
.ppcp-button-applepay {
pointer-events: none;
}
&.ppcp-non-ios-device {
.ppcp-button-applepay {
apple-pay-button {

View file

@ -5,10 +5,13 @@ import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/Butto
import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator";
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import {apmButtonsInit} from "../../../ppcp-button/resources/js/modules/Helper/ApmButtons";
class ApplepayButton {
constructor(context, externalHandler, buttonConfig, ppcpConfig) {
apmButtonsInit(ppcpConfig);
this.isInitialized = false;
this.context = context;
@ -60,7 +63,7 @@ class ApplepayButton {
this.initEventHandlers();
this.isInitialized = true;
this.applePayConfig = config;
const isEligible = this.applePayConfig.isEligible;
const isEligible = (this.applePayConfig.isEligible && window.ApplePaySession) || this.buttonConfig.is_admin;
if (isEligible) {
this.fetchTransactionInfo().then(() => {
@ -84,6 +87,10 @@ class ApplepayButton {
});
}
});
} else {
jQuery('#' + this.buttonConfig.button.wrapper).hide();
jQuery('#' + this.buttonConfig.button.mini_cart_wrapper).hide();
jQuery('#express-payment-method-ppcp-applepay').hide();
}
}
@ -107,7 +114,7 @@ class ApplepayButton {
let config = {
wrapper: this.buttonConfig.button.wrapper,
ppcpStyle: this.ppcpConfig.button.style,
//buttonStyle: this.buttonConfig.button.style,
buttonStyle: this.buttonConfig.button.style,
ppcpButtonWrapper: this.ppcpConfig.button.wrapper
}
@ -119,7 +126,7 @@ class ApplepayButton {
}
if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) {
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway';
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway-paypal';
}
return config;
@ -167,14 +174,8 @@ class ApplepayButton {
addButton() {
this.log('addButton', this.context);
const wrapper =
(this.context === 'mini-cart')
? this.buttonConfig.button.mini_cart_wrapper
: this.buttonConfig.button.wrapper;
const shape =
(this.context === 'mini-cart')
? this.ppcpConfig.button.mini_cart_style.shape
: this.ppcpConfig.button.style.shape;
const { wrapper, ppcpStyle } = this.contextConfig();
const appleContainer = document.getElementById(wrapper);
const type = this.buttonConfig.button.type;
const language = this.buttonConfig.button.lang;
@ -185,8 +186,13 @@ class ApplepayButton {
appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`;
}
jQuery('#' + wrapper).addClass('ppcp-button-' + shape);
jQuery(wrapper).append(appleContainer);
const $wrapper = jQuery('#' + wrapper);
$wrapper.addClass('ppcp-button-' + ppcpStyle.shape);
if (ppcpStyle.height) {
$wrapper.css('--apple-pay-button-height', `${ppcpStyle.height}px`)
$wrapper.css('height', `${ppcpStyle.height}px`)
}
}
//------------------------

View file

@ -65,7 +65,7 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
buttonConfig.button.wrapper = selector.replace('#', '');
applyConfigOptions(buttonConfig);
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-applepay"></div>`;
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-applepay"></div>`;
if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement);

View file

@ -23,12 +23,6 @@ const ApplePayComponent = () => {
const manager = new ApplepayManager(buttonConfig, ppcpConfig);
manager.init();
};
useEffect(() => {
const bodyClass = 'ppcp-has-applepay-block';
if (!document.body.classList.contains(bodyClass)) {
document.body.classList.add(bodyClass);
}
}, []);
useEffect(() => {
// Load ApplePay SDK
@ -50,14 +44,13 @@ const ApplePayComponent = () => {
}, [paypalLoaded, applePayLoaded]);
return (
<div id={buttonConfig.button.wrapper.replace('#', '')}></div>
<div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-apm ppcp-button-applepay"></div>
);
}
const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
registerMethod({
registerExpressPaymentMethod({
name: buttonData.id,
label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>,
content: <ApplePayComponent isEditing={false}/>,

View file

@ -62,6 +62,11 @@ return array(
);
},
'applepay.has_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' );
},
'applepay.is_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false;

View file

@ -968,7 +968,7 @@ class ApplePayButton implements ButtonInterface {
add_action(
$render_placeholder,
function () {
echo '<span id="applepay-container-minicart" class="ppcp-button-applepay ppcp-button-minicart"></span>';
echo '<span id="applepay-container-minicart" class="ppcp-button-apm ppcp-button-applepay ppcp-button-minicart"></span>';
},
21
);
@ -981,7 +981,7 @@ class ApplePayButton implements ButtonInterface {
*/
protected function applepay_button(): void {
?>
<div id="applepay-container">
<div id="applepay-container" class="ppcp-button-apm ppcp-button-applepay">
<?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?>
</div>
<?php

View file

@ -149,6 +149,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
@ -204,6 +205,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
@ -252,6 +254,7 @@ class DataToAppleButtonScripts {
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => true,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),

View file

@ -1,6 +1,7 @@
<?php
/**
* ApmApplies helper.
* Checks if ApplePay is available for a given country and currency.
*
* @package WooCommerce\PayPalCommerce\ApplePay\Helper
*/
@ -15,7 +16,7 @@ namespace WooCommerce\PayPalCommerce\Applepay\Helper;
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for DCC.
* The matrix which countries and currency combinations can be used for ApplePay.
*
* @var array
*/
@ -38,7 +39,7 @@ class ApmApplies {
/**
* ApmApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for ApplePay.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/

View file

@ -0,0 +1,15 @@
@use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button;
li[id^="express-payment-method-ppcp-"] {
line-height: 0;
// Set min-width to 0 as the buttons need to fit in a tight grid.
.paypal-buttons {
min-width: 0 !important;
}
}
.ppcp-button-apm {
@include apm-button.button;
}

View file

@ -76,8 +76,8 @@ export const paypalShippingToWc = (shipping) => {
* @returns {Object}
*/
export const paypalPayerToWc = (payer) => {
const firstName = payer.name.given_name;
const lastName = payer.name.surname;
const firstName = payer?.name?.given_name ?? '';
const lastName = payer?.name?.surname ?? '';
const address = payer.address ? paypalAddressToWc(payer.address) : {};
return {
...address,
@ -100,10 +100,12 @@ export const paypalOrderToWcShippingAddress = (order) => {
const res = paypalShippingToWc(shipping);
// use the name from billing if the same, to avoid possible mistakes when splitting full_name
const billingAddress = paypalPayerToWc(order.payer);
if (`${res.first_name} ${res.last_name}` === `${billingAddress.first_name} ${billingAddress.last_name}`) {
res.first_name = billingAddress.first_name;
res.last_name = billingAddress.last_name;
if (order.payer) {
const billingAddress = paypalPayerToWc(order.payer);
if (`${res.first_name} ${res.last_name}` === `${billingAddress.first_name} ${billingAddress.last_name}`) {
res.first_name = billingAddress.first_name;
res.last_name = billingAddress.last_name;
}
}
return res;
@ -116,10 +118,13 @@ export const paypalOrderToWcShippingAddress = (order) => {
*/
export const paypalOrderToWcAddresses = (order) => {
const shippingAddress = paypalOrderToWcShippingAddress(order);
let billingAddress = paypalPayerToWc(order.payer);
// no billing address, such as if billing address retrieval is not allowed in the merchant account
if (!billingAddress.address_line_1) {
billingAddress = {...shippingAddress, ...paypalPayerToWc(order.payer)};
let billingAddress = shippingAddress;
if (order.payer) {
billingAddress = paypalPayerToWc(order.payer);
// no billing address, such as if billing address retrieval is not allowed in the merchant account
if (!billingAddress.address_line_1) {
billingAddress = {...shippingAddress, ...paypalPayerToWc(order.payer)};
}
}
return {billingAddress, shippingAddress};

View file

@ -1,13 +1,20 @@
import {useEffect, useState} from '@wordpress/element';
import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry';
import {mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address";
import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
import {
loadPaypalScriptPromise
} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
import {
normalizeStyleForFundingSource
} from '../../../ppcp-button/resources/js/modules/Helper/Style'
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
const config = wc.wcSettings.getSetting('ppcp-gateway_data');
window.ppcpFundingSource = config.fundingSource;
let registeredContext = false;
const PayPalComponent = ({
onClick,
onClose,
@ -18,46 +25,37 @@ const PayPalComponent = ({
activePaymentMethod,
shippingData,
isEditing,
fundingSource,
}) => {
const {onPaymentSetup, onCheckoutFail, onCheckoutValidation} = eventRegistration;
const {responseTypes} = emitResponse;
const [paypalOrder, setPaypalOrder] = useState(null);
const [gotoContinuationOnError, setGotoContinuationOnError] = useState(false);
const methodId = fundingSource ? `${config.id}-${fundingSource}` : config.id;
useEffect(() => {
// fill the form if in continuation (for product or mini-cart buttons)
if (!config.scriptData.continuation || !config.scriptData.continuation.order || window.ppcpContinuationFilled) {
return;
}
const paypalAddresses = paypalOrderToWcAddresses(config.scriptData.continuation.order);
const wcAddresses = wp.data.select('wc/store/cart').getCustomerData();
const addresses = mergeWcAddress(wcAddresses, paypalAddresses);
wp.data.dispatch('wc/store/cart').setBillingAddress(addresses.billingAddress);
if (shippingData.needsShipping) {
wp.data.dispatch('wc/store/cart').setShippingAddress(addresses.shippingAddress);
try {
const paypalAddresses = paypalOrderToWcAddresses(config.scriptData.continuation.order);
const wcAddresses = wp.data.select('wc/store/cart').getCustomerData();
const addresses = mergeWcAddress(wcAddresses, paypalAddresses);
wp.data.dispatch('wc/store/cart').setBillingAddress(addresses.billingAddress);
if (shippingData.needsShipping) {
wp.data.dispatch('wc/store/cart').setShippingAddress(addresses.shippingAddress);
}
} catch (err) {
// sometimes the PayPal address is missing, skip in this case.
console.log(err);
}
// this useEffect should run only once, but adding this in case of some kind of full re-rendering
window.ppcpContinuationFilled = true;
}, [])
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (!loaded) {
loadPaypalScript(config.scriptData, () => {
setLoaded(true);
buttonModuleWatcher.registerContextBootstrap(config.scriptData.context, {
createOrder: () => {
return createOrder();
},
onApprove: (data, actions) => {
return handleApprove(data, actions);
},
});
});
}
}, [loaded]);
const createOrder = async () => {
try {
const res = await fetch(config.scriptData.ajax.create_order.endpoint, {
@ -155,6 +153,7 @@ const PayPalComponent = ({
if (config.finalReviewEnabled) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError(true);
onSubmit();
}
} catch (err) {
@ -173,7 +172,7 @@ const PayPalComponent = ({
if (config.scriptData.continuation) {
return true;
}
if (wp.data.select('wc/store/validation').hasValidationErrors()) {
if (gotoContinuationOnError && wp.data.select('wc/store/validation').hasValidationErrors()) {
location.href = getCheckoutRedirectUrl();
return { type: responseTypes.ERROR };
}
@ -181,7 +180,7 @@ const PayPalComponent = ({
return true;
});
return unsubscribe;
}, [onCheckoutValidation] );
}, [onCheckoutValidation, gotoContinuationOnError] );
const handleClick = (data, actions) => {
if (isEditing) {
@ -233,7 +232,7 @@ const PayPalComponent = ({
}
useEffect(() => {
if (activePaymentMethod !== config.id) {
if (activePaymentMethod !== methodId) {
return;
}
@ -269,7 +268,7 @@ const PayPalComponent = ({
}, [onPaymentSetup, paypalOrder, activePaymentMethod]);
useEffect(() => {
if (activePaymentMethod !== config.id) {
if (activePaymentMethod !== methodId) {
return;
}
const unsubscribe = onCheckoutFail(({ processingResponse }) => {
@ -296,15 +295,26 @@ const PayPalComponent = ({
)
}
if (!loaded) {
return null;
if (!registeredContext) {
buttonModuleWatcher.registerContextBootstrap(config.scriptData.context, {
createOrder: () => {
return createOrder();
},
onApprove: (data, actions) => {
return handleApprove(data, actions);
},
});
registeredContext = true;
}
const style = normalizeStyleForFundingSource(config.scriptData.button.style, fundingSource);
const PayPalButton = window.paypal.Buttons.driver("react", { React, ReactDOM });
return (
<PayPalButton
style={config.scriptData.button.style}
fundingSource={fundingSource}
style={style}
onClick={handleClick}
onCancel={onClose}
onError={onClose}
@ -316,20 +326,61 @@ const PayPalComponent = ({
}
const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
if (config.scriptData.continuation) {
features.push('ppcp_continuation');
registerMethod = registerPaymentMethod;
if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !config.scriptData.continuation) {
let descriptionElement = <div dangerouslySetInnerHTML={{__html: config.description}}></div>;
if (config.placeOrderButtonDescription) {
descriptionElement = <div>
<p dangerouslySetInnerHTML={{__html: config.description}}></p>
<p style={{textAlign: 'center'}} className={'ppcp-place-order-description'} dangerouslySetInnerHTML={{__html: config.placeOrderButtonDescription}}></p>
</div>;
}
registerPaymentMethod({
name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: descriptionElement,
edit: descriptionElement,
placeOrderButtonLabel: config.placeOrderButtonText,
ariaLabel: config.title,
canMakePayment: () => config.enabled,
supports: {
features: features,
},
});
}
registerMethod({
name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false}/>,
edit: <PayPalComponent isEditing={true}/>,
ariaLabel: config.title,
canMakePayment: () => config.enabled,
supports: {
features: features,
},
});
if (config.scriptData.continuation) {
registerPaymentMethod({
name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false}/>,
edit: <PayPalComponent isEditing={true}/>,
ariaLabel: config.title,
canMakePayment: () => true,
supports: {
features: [...features, 'ppcp_continuation'],
},
});
} else if (!config.usePlaceOrder) {
const paypalScriptPromise = loadPaypalScriptPromise(config.scriptData);
for (const fundingSource of ['paypal', ...config.enabledFundingSources]) {
registerExpressPaymentMethod({
name: `${config.id}-${fundingSource}`,
paymentMethodId: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false} fundingSource={fundingSource}/>,
edit: <PayPalComponent isEditing={true} fundingSource={fundingSource}/>,
ariaLabel: config.title,
canMakePayment: async () => {
await paypalScriptPromise;
return paypal.Buttons({fundingSource}).isEligible();
},
supports: {
features: features,
},
});
}
}

View file

@ -34,7 +34,12 @@ return array(
$container->get( 'wcgateway.paypal-gateway' ),
$container->get( 'blocks.settings.final_review_enabled' ),
$container->get( 'session.cancellation.view' ),
$container->get( 'session.handler' )
$container->get( 'session.handler' ),
$container->get( 'blocks.add-place-order-method' ),
$container->get( 'wcgateway.use-place-order-button' ),
$container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'wcgateway.place-order-button-description' ),
$container->get( 'wcgateway.all-funding-sources' )
);
},
'blocks.settings.final_review_enabled' => static function ( ContainerInterface $container ): bool {
@ -54,4 +59,14 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'blocks.add-place-order-method' => function ( ContainerInterface $container ) : bool {
/**
* Whether to create a non-express method with the standard "Place order" button redirecting to PayPal.
*/
return apply_filters(
'woocommerce_paypal_payments_blocks_add_place_order_method',
true
);
},
);

View file

@ -96,6 +96,27 @@ class BlocksModule implements ServiceModule, ExtendingModule, ExecutableModule {
}
);
// Enqueue frontend scripts.
add_action(
'wp_enqueue_scripts',
static function () use ( $c ) {
if ( ! has_block( 'woocommerce/checkout' ) && ! has_block( 'woocommerce/cart' ) ) {
return;
}
$module_url = $c->get( 'blocks.url' );
$asset_version = $c->get( 'ppcp.asset-version' );
wp_register_style(
'wc-ppcp-blocks',
untrailingslashit( $module_url ) . '/assets/css/gateway.css',
array(),
$asset_version
);
wp_enqueue_style( 'wc-ppcp-blocks' );
}
);
return true;
}
}

View file

@ -87,6 +87,41 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
*/
private $session_handler;
/**
* Whether to create a non-express method with the standard "Place order" button.
*
* @var bool
*/
protected $add_place_order_method;
/**
* Whether to use the standard "Place order" button instead of PayPal buttons.
*
* @var bool
*/
protected $use_place_order;
/**
* The text for the standard "Place order" button.
*
* @var string
*/
protected $place_order_button_text;
/**
* The text for additional "Place order" description.
*
* @var string
*/
protected $place_order_button_description;
/**
* All existing funding sources for PayPal buttons.
*
* @var array
*/
private $all_funding_sources;
/**
* Assets constructor.
*
@ -99,6 +134,11 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
* @param bool $final_review_enabled Whether the final review is enabled.
* @param CancelView $cancellation_view The cancellation view.
* @param SessionHandler $session_handler The Session handler.
* @param bool $add_place_order_method Whether to create a non-express method with the standard "Place order" button.
* @param bool $use_place_order Whether to use the standard "Place order" button instead of PayPal buttons.
* @param string $place_order_button_text The text for the standard "Place order" button.
* @param string $place_order_button_description The text for additional "Place order" description.
* @param array $all_funding_sources All existing funding sources for PayPal buttons.
*/
public function __construct(
string $module_url,
@ -109,18 +149,28 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
PayPalGateway $gateway,
bool $final_review_enabled,
CancelView $cancellation_view,
SessionHandler $session_handler
SessionHandler $session_handler,
bool $add_place_order_method,
bool $use_place_order,
string $place_order_button_text,
string $place_order_button_description,
array $all_funding_sources
) {
$this->name = PayPalGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->smart_button = $smart_button;
$this->plugin_settings = $plugin_settings;
$this->settings_status = $settings_status;
$this->gateway = $gateway;
$this->final_review_enabled = $final_review_enabled;
$this->cancellation_view = $cancellation_view;
$this->session_handler = $session_handler;
$this->name = PayPalGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->smart_button = $smart_button;
$this->plugin_settings = $plugin_settings;
$this->settings_status = $settings_status;
$this->gateway = $gateway;
$this->final_review_enabled = $final_review_enabled;
$this->cancellation_view = $cancellation_view;
$this->session_handler = $session_handler;
$this->add_place_order_method = $add_place_order_method;
$this->use_place_order = $use_place_order;
$this->place_order_button_text = $place_order_button_text;
$this->place_order_button_description = $place_order_button_description;
$this->all_funding_sources = $all_funding_sources;
}
/**
@ -174,20 +224,33 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
}
}
$disabled_funding_sources = explode( ',', $script_data['url_params']['disable-funding'] ?? '' ) ?: array();
$funding_sources = array_values(
array_diff(
array_keys( $this->all_funding_sources ),
$disabled_funding_sources
)
);
return array(
'id' => $this->gateway->id,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ),
'fundingSource' => $this->session_handler->funding_source(),
'finalReviewEnabled' => $this->final_review_enabled,
'ajax' => array(
'id' => $this->gateway->id,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ),
'fundingSource' => $this->session_handler->funding_source(),
'finalReviewEnabled' => $this->final_review_enabled,
'addPlaceOrderMethod' => $this->add_place_order_method,
'usePlaceOrder' => $this->use_place_order,
'placeOrderButtonText' => $this->place_order_button_text,
'placeOrderButtonDescription' => $this->place_order_button_description,
'enabledFundingSources' => $funding_sources,
'ajax' => array(
'update_shipping' => array(
'endpoint' => WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( UpdateShippingEndpoint::nonce() ),
),
),
'scriptData' => $script_data,
'scriptData' => $script_data,
);
}
}

View file

@ -9,7 +9,8 @@ module.exports = {
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'checkout-block': path.resolve('./resources/js/checkout-block.js')
'checkout-block': path.resolve('./resources/js/checkout-block.js'),
"gateway": path.resolve('./resources/css/gateway.scss')
},
output: {
path: path.resolve(__dirname, 'assets/'),

View file

@ -1,3 +1,5 @@
@use "mixins/apm-button" as apm-button;
#place_order.ppcp-hidden {
display: none !important;
}
@ -15,3 +17,24 @@
.ppc-button-wrapper #ppcp-messages:first-child {
padding-top: 10px;
}
// Prevents spacing after button group.
#ppc-button-ppcp-gateway {
line-height: 0;
div[class^="item-"] {
margin-top: 14px;
&:first-child {
margin-top: 0;
}
}
}
#ppc-button-minicart {
line-height: 0;
display: block;
}
.ppcp-button-apm {
@include apm-button.button;
}

View file

@ -0,0 +1,42 @@
@mixin button {
overflow: hidden;
min-width: 0;
max-width: 750px;
line-height: 0;
border-radius: 4px;
// Defaults
height: 45px;
margin-top: 14px;
&.ppcp-button-pill {
border-radius: 50px;
}
&.ppcp-button-minicart {
display: block;
}
.ppcp-width-min & {
height: 35px;
}
.ppcp-width-300 & {
height: 45px;
}
.ppcp-width-500 & {
height: 55px;
}
// No margin on block layout.
.wp-block-woocommerce-checkout &, .wp-block-woocommerce-cart & {
margin: 0;
min-width: 0;
}
.wp-admin & {
pointer-events: none;
}
}

View file

@ -22,6 +22,7 @@ import FormValidator from "./modules/Helper/FormValidator";
import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
import buttonModuleWatcher from "./modules/ButtonModuleWatcher";
import MessagesBootstrap from "./modules/ContextBootstrap/MessagesBootstap";
import {apmButtonsInit} from "./modules/Helper/ApmButtons";
// TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough.
@ -145,6 +146,7 @@ const bootstrap = () => {
};
const onSmartButtonsInit = () => {
jQuery(document).trigger('ppcp-smart-buttons-init', this);
buttonsSpinner.unblock();
};
const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit);
@ -217,6 +219,8 @@ const bootstrap = () => {
messageRenderer,
);
messagesBootstrap.init();
apmButtonsInit(PayPalCommerceGateway);
};
document.addEventListener(
@ -279,11 +283,12 @@ document.addEventListener(
});
let bootstrapped = false;
let failed = false;
hideOrderButtonIfPpcpGateway();
jQuery(document.body).on('updated_checkout payment_method_selected', () => {
if (bootstrapped) {
if (bootstrapped || failed) {
return;
}
@ -294,6 +299,12 @@ document.addEventListener(
bootstrapped = true;
bootstrap();
}, () => {
failed = true;
setVisibleByClass(ORDER_BUTTON_SELECTOR, true, 'ppcp-hidden');
buttonsSpinner.unblock();
cardsSpinner.unblock();
});
},
);

View file

@ -2,6 +2,7 @@ import 'formdata-polyfill';
import onApprove from '../OnApproveHandler/onApproveForPayNow.js';
import {payerData} from "../Helper/PayerData";
import {getCurrentPaymentMethod} from "../Helper/CheckoutMethodState";
import validateCheckoutForm from "../Helper/CheckoutFormValidation";
class CheckoutActionHandler {
@ -13,7 +14,13 @@ class CheckoutActionHandler {
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
createSubscription: async (data, actions) => {
try {
await validateCheckoutForm(this.config);
} catch (error) {
throw {type: 'form-validation-error'};
}
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
@ -56,6 +63,8 @@ class CheckoutActionHandler {
const paymentMethod = getCurrentPaymentMethod();
const fundingSource = window.ppcpFundingSource;
const savePaymentMethod = !!document.getElementById('wc-ppcp-credit-card-gateway-new-payment-method')?.checked;
return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST',
headers: {
@ -72,7 +81,8 @@ class CheckoutActionHandler {
funding_source: fundingSource,
// send as urlencoded string to handle complex fields via PHP functions the same as normal form submit
form_encoded: new URLSearchParams(formData).toString(),
createaccount: createaccount
createaccount: createaccount,
save_payment_method: savePaymentMethod
})
}).then(function (res) {
return res.json();

View file

@ -26,7 +26,7 @@ const storeToken = (token) => {
sessionStorage.setItem(storageKey, JSON.stringify(token));
}
const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
const dataClientIdAttributeHandler = (scriptOptions, config, callback, errorCallback = null) => {
fetch(config.endpoint, {
method: 'POST',
headers: {
@ -51,6 +51,10 @@ const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
if (typeof callback === 'function') {
callback(paypal);
}
}).catch(err => {
if (typeof errorCallback === 'function') {
errorCallback(err);
}
});
});
}

View file

@ -0,0 +1,114 @@
export const apmButtonsInit = (config, selector = '.ppcp-button-apm') => {
let selectorInContainer = selector;
if (window.ppcpApmButtons) {
return;
}
if (config && config.button) {
// If it's separate gateways, modify wrapper to account for the individual buttons as individual APMs.
const wrapper = config.button.wrapper;
const isSeparateGateways = jQuery(wrapper).children('div[class^="item-"]').length > 0;
if (isSeparateGateways) {
selector += `, ${wrapper} div[class^="item-"]`;
selectorInContainer += `, div[class^="item-"]`;
}
}
window.ppcpApmButtons = new ApmButtons(selector, selectorInContainer);
}
export class ApmButtons {
constructor(selector, selectorInContainer) {
this.selector = selector;
this.selectorInContainer = selectorInContainer;
this.containers = [];
// Reloads button containers.
this.reloadContainers();
// Refresh button layout.
jQuery(window).resize(() => {
this.refresh();
}).resize();
jQuery(document).on('ppcp-smart-buttons-init', () => {
this.refresh();
});
// Observes for new buttons.
(new MutationObserver(this.observeElementsCallback.bind(this)))
.observe(document.body, { childList: true, subtree: true });
}
observeElementsCallback(mutationsList, observer) {
const observeSelector = this.selector + ', .widget_shopping_cart, .widget_shopping_cart_content';
let shouldReload = false;
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.matches && node.matches(observeSelector)) {
shouldReload = true;
}
});
}
}
if (shouldReload) {
this.reloadContainers();
this.refresh();
}
};
reloadContainers() {
jQuery(this.selector).each((index, el) => {
const parent = jQuery(el).parent();
if (!this.containers.some($el => $el.is(parent))) {
this.containers.push(parent);
}
});
console.log('this.containers', this.containers);
}
refresh() {
for (const container of this.containers) {
const $container = jQuery(container);
// Check width and add classes
const width = $container.width();
$container.removeClass('ppcp-width-500 ppcp-width-300 ppcp-width-min');
if (width >= 500) {
$container.addClass('ppcp-width-500');
} else if (width >= 300) {
$container.addClass('ppcp-width-300');
} else {
$container.addClass('ppcp-width-min');
}
// Check first apm button
const $firstElement = $container.children(':visible').first();
// Assign margins to buttons
$container.find(this.selectorInContainer).each((index, el) => {
const $el = jQuery(el);
if ($el.is($firstElement)) {
$el.css('margin-top', `0px`);
return true;
}
const height = $el.height();
$el.css('margin-top', `${Math.round(height * 0.3)}px`);
});
}
}
}

View file

@ -0,0 +1,50 @@
export const cardFieldStyles = (field) => {
const allowedProperties = [
'appearance',
'color',
'direction',
'font',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-variant-alternates',
'font-variant-caps',
'font-variant-east-asian',
'font-variant-ligatures',
'font-variant-numeric',
'font-weight',
'letter-spacing',
'line-height',
'opacity',
'outline',
'padding',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
'text-shadow',
'transition',
'-moz-appearance',
'-moz-osx-font-smoothing',
'-moz-tap-highlight-color',
'-moz-transition',
'-webkit-appearance',
'-webkit-osx-font-smoothing',
'-webkit-tap-highlight-color',
'-webkit-transition',
];
const stylesRaw = window.getComputedStyle(field);
const styles = {};
Object.values(stylesRaw).forEach((prop) => {
if (!stylesRaw[prop] || !allowedProperties.includes(prop)) {
return;
}
styles[prop] = '' + stylesRaw[prop];
});
return styles;
}

View file

@ -0,0 +1,48 @@
import Spinner from "./Spinner";
import FormValidator from "./FormValidator";
import ErrorHandler from "../ErrorHandler";
const validateCheckoutForm = function (config) {
return new Promise(async (resolve, reject) => {
try {
const spinner = new Spinner();
const errorHandler = new ErrorHandler(
config.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const formSelector = config.context === 'checkout' ? 'form.checkout' : 'form#order_review';
const formValidator = config.early_checkout_validation_enabled ?
new FormValidator(
config.ajax.validate_checkout.endpoint,
config.ajax.validate_checkout.nonce,
) : null;
if (!formValidator) {
resolve();
return;
}
formValidator.validate(document.querySelector(formSelector)).then((errors) => {
if (errors.length > 0) {
spinner.unblock();
errorHandler.clear();
errorHandler.messages(errors);
// fire WC event for other plugins
jQuery( document.body ).trigger( 'checkout_error' , [ errorHandler.currentHtml() ] );
reject();
} else {
resolve();
}
});
} catch (error) {
console.error(error);
reject();
}
});
}
export default validateCheckoutForm;

View file

@ -7,10 +7,11 @@ import {keysToCamelCase} from "./Utils";
// This component may be used by multiple modules. This assures that options are shared between all instances.
let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || {
isLoading: false,
onLoadedCallbacks: []
onLoadedCallbacks: [],
onErrorCallbacks: [],
};
export const loadPaypalScript = (config, onLoaded) => {
export const loadPaypalScript = (config, onLoaded, onError = null) => {
// If PayPal is already loaded call the onLoaded callback and return.
if (typeof paypal !== 'undefined') {
onLoaded();
@ -19,6 +20,9 @@ export const loadPaypalScript = (config, onLoaded) => {
// Add the onLoaded callback to the onLoadedCallbacks stack.
options.onLoadedCallbacks.push(onLoaded);
if (onError) {
options.onErrorCallbacks.push(onError);
}
// Return if it's still loading.
if (options.isLoading) {
@ -26,6 +30,12 @@ export const loadPaypalScript = (config, onLoaded) => {
}
options.isLoading = true;
const resetState = () => {
options.isLoading = false;
options.onLoadedCallbacks = [];
options.onErrorCallbacks = [];
}
// Callback to be called once the PayPal script is loaded.
const callback = (paypal) => {
widgetBuilder.setPaypal(paypal);
@ -34,8 +44,14 @@ export const loadPaypalScript = (config, onLoaded) => {
onLoadedCallback();
}
options.isLoading = false;
options.onLoadedCallbacks = [];
resetState();
}
const errorCallback = (err) => {
for (const onErrorCallback of options.onErrorCallbacks) {
onErrorCallback(err);
}
resetState();
}
// Build the PayPal script options.
@ -44,12 +60,26 @@ export const loadPaypalScript = (config, onLoaded) => {
// Load PayPal script for special case with data-client-token
if (config.data_client_id.set_attribute) {
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback);
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback);
return;
}
// Adds data-user-id-token to script options.
const userIdToken = config?.save_payment_methods?.id_token;
if(userIdToken) {
scriptOptions['data-user-id-token'] = userIdToken;
}
// Load PayPal script
loadScript(scriptOptions).then(callback);
loadScript(scriptOptions)
.then(callback)
.catch(errorCallback);
}
export const loadPaypalScriptPromise = (config) => {
return new Promise((resolve, reject) => {
loadPaypalScript(config, resolve, reject)
});
}
export const loadPaypalJsScript = (options, buttons, container) => {
@ -57,3 +87,11 @@ export const loadPaypalJsScript = (options, buttons, container) => {
paypal.Buttons(buttons).render(container);
});
}
export const loadPaypalJsScriptPromise = (options) => {
return new Promise((resolve, reject) => {
loadScript(options)
.then(resolve)
.catch(reject);
});
}

View file

@ -0,0 +1,20 @@
export const normalizeStyleForFundingSource = (style, fundingSource) => {
const commonProps = {};
['shape', 'height'].forEach(prop => {
if (style[prop]) {
commonProps[prop] = style[prop];
}
});
switch (fundingSource) {
case 'paypal':
return style;
case 'paylater':
return {
color: style.color,
...commonProps
};
default:
return commonProps;
}
}

View file

@ -1,4 +1,5 @@
import {show} from "../Helper/Hiding";
import {cardFieldStyles} from "../Helper/CardFieldsHelper";
class CardFieldsRenderer {
@ -53,28 +54,28 @@ class CardFieldsRenderer {
if (cardField.isEligible()) {
const nameField = document.getElementById('ppcp-credit-card-gateway-card-name');
if (nameField) {
let styles = this.cardFieldStyles(nameField);
let styles = cardFieldStyles(nameField);
cardField.NameField({style: {'input': styles}}).render(nameField.parentNode);
nameField.remove();
}
const numberField = document.getElementById('ppcp-credit-card-gateway-card-number');
if (numberField) {
let styles = this.cardFieldStyles(numberField);
let styles = cardFieldStyles(numberField);
cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode);
numberField.remove();
}
const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry');
if (expiryField) {
let styles = this.cardFieldStyles(expiryField);
let styles = cardFieldStyles(expiryField);
cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode);
expiryField.remove();
}
const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc');
if (cvvField) {
let styles = this.cardFieldStyles(cvvField);
let styles = cardFieldStyles(cvvField);
cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode);
cvvField.remove();
}
@ -91,65 +92,35 @@ class CardFieldsRenderer {
this.spinner.block();
this.errorHandler.clear();
const paymentToken = document.querySelector('input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked')?.value
if(paymentToken && paymentToken !== 'new') {
fetch(this.defaultConfig.ajax.capture_card_payment.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.defaultConfig.ajax.capture_card_payment.nonce,
payment_token: paymentToken
})
}).then((res) => {
return res.json();
}).then((data) => {
document.querySelector('#place_order').click();
});
return;
}
cardField.submit()
.catch((error) => {
this.spinner.unblock();
console.error(error)
this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid);
})
});
});
}
cardFieldStyles(field) {
const allowedProperties = [
'appearance',
'color',
'direction',
'font',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-variant-alternates',
'font-variant-caps',
'font-variant-east-asian',
'font-variant-ligatures',
'font-variant-numeric',
'font-weight',
'letter-spacing',
'line-height',
'opacity',
'outline',
'padding',
'padding-bottom',
'padding-left',
'padding-right',
'padding-top',
'text-shadow',
'transition',
'-moz-appearance',
'-moz-osx-font-smoothing',
'-moz-tap-highlight-color',
'-moz-transition',
'-webkit-appearance',
'-webkit-osx-font-smoothing',
'-webkit-tap-highlight-color',
'-webkit-transition',
];
const stylesRaw = window.getComputedStyle(field);
const styles = {};
Object.values(stylesRaw).forEach((prop) => {
if (!stylesRaw[prop] || !allowedProperties.includes(prop)) {
return;
}
styles[prop] = '' + stylesRaw[prop];
});
return styles;
}
disableFields() {}
enableFields() {}
}
export default CardFieldsRenderer;

View file

@ -2,6 +2,7 @@ import merge from "deepmerge";
import {loadScript} from "@paypal/paypal-js";
import {keysToCamelCase} from "../Helper/Utils";
import widgetBuilder from "./WidgetBuilder";
import {normalizeStyleForFundingSource} from "../Helper/Style";
class Renderer {
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
@ -36,16 +37,7 @@ class Renderer {
} else {
// render each button separately
for (const fundingSource of paypal.getFundingSources().filter(s => !(s in enabledSeparateGateways))) {
let style = settings.button.style;
if (fundingSource !== 'paypal') {
style = {
shape: style.shape,
color: style.color,
};
if (fundingSource !== 'paylater') {
delete style.color;
}
}
const style = normalizeStyleForFundingSource(settings.button.style, fundingSource);
this.renderButtons(
settings.button.wrapper,

View file

@ -15,8 +15,10 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -68,8 +70,41 @@ return array(
return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' );
},
// This service may not work correctly when called too early.
'button.context' => static function ( ContainerInterface $container ): string {
$obj = new class() {
use ContextTrait;
/**
* Session handler.
*
* @var SessionHandler
*/
protected $session_handler;
/** Constructor. */
public function __construct() {
// phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic
$this->session_handler = new SessionHandler();
}
/**
* Wrapper for a non-public function.
*/
public function get_context(): string {
// phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundInStatic
return $this->context();
}
};
return $obj->get_context();
},
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' );
if ( $container->get( 'wcgateway.use-place-order-button' )
&& in_array( $container->get( 'button.context' ), array( 'checkout', 'pay-now' ), true )
) {
return new DisabledSmartButton();
}
if ( $state->current_state() !== State::STATE_ONBOARDED ) {
return new DisabledSmartButton();
}
@ -84,7 +119,7 @@ return array(
$request_data = $container->get( 'button.request-data' );
$client_id = $container->get( 'button.client_id' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$messages_apply = $container->get( 'button.helper.messages-apply' );
$environment = $container->get( 'onboarding.environment' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
@ -259,8 +294,10 @@ return array(
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger );
return new ThreeDSecure(
$container->get( 'api.factory.card-authentication-result-factory' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply(

View file

@ -33,8 +33,8 @@ use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
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\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
@ -272,6 +272,8 @@ class SmartButton implements SmartButtonInterface {
* @return bool
*/
public function render_wrapper(): bool {
$this->init_context();
if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) {
$this->render_button_wrapper_registrar();
$this->render_message_wrapper_registrar();
@ -303,7 +305,13 @@ class SmartButton implements SmartButtonInterface {
add_filter(
'woocommerce_credit_card_form_fields',
function ( array $default_fields, $id ) use ( $subscription_helper ) : array {
if ( is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && CreditCardGateway::ID === $id ) {
if (
is_user_logged_in()
&& $this->settings->has( 'vault_enabled_dcc' )
&& $this->settings->get( 'vault_enabled_dcc' )
&& CreditCardGateway::ID === $id
&& apply_filters( 'woocommerce_paypal_payments_should_render_card_custom_fields', true )
) {
$default_fields['card-vault'] = sprintf(
'<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>',
@ -638,7 +646,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' )
&& $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' )
&& $this->dcc_applies->for_country_currency()
&& in_array( $this->context(), array( 'checkout', 'pay-now' ), true );
&& in_array( $this->context(), array( 'checkout', 'pay-now', 'add-payment-method' ), true );
}
/**
@ -1049,30 +1057,39 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
'mini_cart_wrapper' => '#ppc-button-minicart',
'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ),
'cancel_wrapper' => '#ppcp-cancel',
'mini_cart_style' => array(
'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ),
'shape' => $this->style_for_context( 'shape', 'mini-cart' ),
'label' => $this->style_for_context( 'label', 'mini-cart' ),
'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ),
'height' => $this->settings->has( 'button_mini-cart_height' ) && $this->settings->get( 'button_mini-cart_height' ) ? $this->normalize_height( (int) $this->settings->get( 'button_mini-cart_height' ) ) : 35,
'mini_cart_style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ),
'shape' => $this->style_for_context( 'shape', 'mini-cart' ),
'label' => $this->style_for_context( 'label', 'mini-cart' ),
'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ),
'height' => $this->normalize_height( $this->style_for_context( 'height', 'mini-cart', 35 ), 25, 55 ),
)
),
'style' => array(
'layout' => $this->style_for_context( 'layout', $this->context() ),
'color' => $this->style_for_context( 'color', $this->context() ),
'shape' => $this->style_for_context( 'shape', $this->context() ),
'label' => $this->style_for_context( 'label', $this->context() ),
'tagline' => $this->style_for_context( 'tagline', $this->context() ),
'style' => $this->normalize_style(
array(
'layout' => $this->style_for_context( 'layout', $this->context() ),
'color' => $this->style_for_context( 'color', $this->context() ),
'shape' => $this->style_for_context( 'shape', $this->context() ),
'label' => $this->style_for_context( 'label', $this->context() ),
'tagline' => $this->style_for_context( 'tagline', $this->context() ),
'height' => in_array( $this->context(), array( 'cart-block', 'checkout-block' ), true )
? $this->normalize_height( $this->style_for_context( 'height', $this->context(), 48 ), 40, 55 )
: null,
)
),
),
'separate_buttons' => array(
'card' => array(
'id' => CardButtonGateway::ID,
'wrapper' => '#ppc-button-' . CardButtonGateway::ID,
'style' => array(
'shape' => $this->style_for_apm( 'shape', 'card' ),
'color' => $this->style_for_apm( 'color', 'card', 'black' ),
'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal',
'style' => $this->normalize_style(
array(
'shape' => $this->style_for_apm( 'shape', 'card' ),
'color' => $this->style_for_apm( 'color', 'card', 'black' ),
'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal',
)
),
),
),
@ -1143,13 +1160,6 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$localize['pay_now'] = $this->pay_now_script_data();
}
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
$localize['button']['mini_cart_style']['tagline'] = false;
}
if ( $this->style_for_context( 'layout', $this->context() ) !== 'horizontal' ) {
$localize['button']['style']['tagline'] = false;
}
if ( $this->is_paypal_continuation() ) {
$order = $this->session_handler->order();
assert( $order !== null );
@ -1160,7 +1170,8 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
}
$this->request_data->dequeue_nonce_fix();
return $localize;
return apply_filters( 'woocommerce_paypal_payments_localized_script_data', $localize );
}
/**
@ -1269,9 +1280,12 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
}
if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) {
$disable_funding = array_diff(
array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' )
$disable_funding = array_merge(
$disable_funding,
array_diff(
array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' )
)
);
}
@ -1410,12 +1424,14 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
*
* @param string $style The name of the style property.
* @param string $context The context.
* @param ?mixed $default The default value.
*
* @return string
* @return string|int
*/
private function style_for_context( string $style, string $context ): string {
// Use the cart/checkout styles for blocks.
$context = str_replace( '-block', '', $context );
private function style_for_context( string $style, string $context, $default = null ) {
if ( $context === 'checkout-block' ) {
$context = 'checkout-block-express';
}
$defaults = array(
'layout' => 'vertical',
@ -1433,6 +1449,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $this->get_style_value( "button_{$context}_${style}" )
?? $this->get_style_value( "button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->normalize_style_value( $defaults[ $style ] ?? '' );
}
@ -1443,9 +1460,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* @param string $apm The APM name, such as 'card'.
* @param ?mixed $default The default value.
*
* @return string
* @return string|int
*/
private function style_for_apm( string $style, string $apm, $default = null ): string {
private function style_for_apm( string $style, string $apm, $default = null ) {
return $this->get_style_value( "${apm}_button_${style}" )
?? ( $default ? $this->normalize_style_value( $default ) : null )
?? $this->style_for_context( $style, 'checkout' );
@ -1455,9 +1472,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Returns the style property value or null.
*
* @param string $key The style property key in the settings.
* @return string|null
* @return string|int|null
*/
private function get_style_value( string $key ): ?string {
private function get_style_value( string $key ) {
if ( ! $this->settings->has( $key ) ) {
return null;
}
@ -1468,27 +1485,49 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* Converts the style property value to string.
*
* @param mixed $value The style property value.
* @return string
* @return string|int
*/
private function normalize_style_value( $value ): string {
private function normalize_style_value( $value ) {
if ( is_bool( $value ) ) {
$value = $value ? 'true' : 'false';
}
if ( is_int( $value ) ) {
return $value;
}
return (string) $value;
}
/**
* Returns a value between 25 and 55.
* Fixes the style.
*
* @param int $height The input value.
* @param array $style The style properties.
* @return array
*/
private function normalize_style( array $style ): array {
if ( array_key_exists( 'tagline', $style ) && ( ! array_key_exists( 'layout', $style ) || $style['layout'] !== 'horizontal' ) ) {
$style['tagline'] = false;
}
if ( array_key_exists( 'height', $style ) && ! $style['height'] ) {
unset( $style['height'] );
}
return $style;
}
/**
* Returns a number between min and max.
*
* @param mixed $height The input value.
* @param int $min The minimum value.
* @param int $max The maximum value.
* @return int The normalized output value.
*/
private function normalize_height( int $height ): int {
if ( $height < 25 ) {
return 25;
private function normalize_height( $height, int $min, int $max ): int {
$height = (int) $height;
if ( $height < $min ) {
return $min;
}
if ( $height > 55 ) {
return 55;
if ( $height > $max ) {
return $max;
}
return $height;

View file

@ -14,6 +14,7 @@ use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
@ -144,10 +145,11 @@ class ApproveOrderEndpoint implements EndpointInterface {
$order = $this->api_endpoint->order( $data['order_id'] );
if ( $order->payment_source() && $order->payment_source()->card() ) {
$payment_source = $order->payment_source();
if ( $payment_source && $payment_source->name() === 'card' ) {
if ( $this->settings->has( 'disable_cards' ) ) {
$disabled_cards = (array) $this->settings->get( 'disable_cards' );
$card = strtolower( $order->payment_source()->card()->brand() );
$card = strtolower( $payment_source->properties()->brand ?? '' );
if ( 'master_card' === $card ) {
$card = 'mastercard';
}

View file

@ -25,12 +25,11 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -305,7 +304,7 @@ class CreateOrderEndpoint implements EndpointInterface {
}
try {
$order = $this->create_paypal_order( $wc_order );
$order = $this->create_paypal_order( $wc_order, $payment_method, $data );
} catch ( Exception $exception ) {
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
throw $exception;
@ -416,6 +415,8 @@ class CreateOrderEndpoint implements EndpointInterface {
* Creates the order in the PayPal, uses data from WC order if provided.
*
* @param \WC_Order|null $wc_order WC order to get data from.
* @param string $payment_method WC payment method.
* @param array $data Request data.
*
* @return Order Created PayPal order.
*
@ -423,7 +424,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @throws PayPalApiException If create order request fails.
* phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
private function create_paypal_order( \WC_Order $wc_order = null ): Order {
private function create_paypal_order( \WC_Order $wc_order = null, string $payment_method = '', array $data = array() ): Order {
assert( $this->purchase_unit instanceof PurchaseUnit );
$funding_source = $this->parsed_request_data['funding_source'] ?? '';
@ -465,7 +466,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$payer,
null,
'',
$action
$action,
$payment_method,
$data
);
} catch ( PayPalApiException $exception ) {
// Looks like currently there is no proper way to validate the shipping address for PayPal,

View file

@ -12,6 +12,39 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
trait ContextTrait {
/**
* Initializes context preconditions like is_cart() and is_checkout().
*
* @return void
*/
protected function init_context(): void {
if ( ! apply_filters( 'woocommerce_paypal_payments_block_classic_compat', true ) ) {
return;
}
/**
* Activate is_checkout() on woocommerce/classic-shortcode checkout blocks.
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
'woocommerce_is_checkout',
function ( $is_checkout ) {
if ( $is_checkout ) {
return $is_checkout;
}
return has_block( 'woocommerce/classic-shortcode {"shortcode":"checkout"}' );
}
);
// Activate is_cart() on woocommerce/classic-shortcode cart blocks.
if ( ! is_cart() && is_callable( 'wc_maybe_define_constant' ) ) {
if ( has_block( 'woocommerce/classic-shortcode' ) && ! has_block( 'woocommerce/classic-shortcode {"shortcode":"checkout"}' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
}
}
/**
* Checks WC is_checkout() + WC checkout ajax requests.
*/
@ -94,6 +127,10 @@ trait ContextTrait {
return 'checkout';
}
if ( $this->is_add_payment_method_page() ) {
return 'add-payment-method';
}
return 'mini-cart';
}
@ -125,6 +162,11 @@ trait ContextTrait {
* @return bool
*/
private function is_paypal_continuation(): bool {
/**
* Property is already defined in trait consumers.
*
* @psalm-suppress UndefinedThisPropertyFetch
*/
$order = $this->session_handler->order();
if ( ! $order ) {
return false;
@ -137,7 +179,7 @@ trait ContextTrait {
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
if ( $source && $source->name() === 'card' ) {
return false; // Ignore for DCC.
}
@ -147,4 +189,22 @@ trait ContextTrait {
return true;
}
/**
* Checks whether current page is Add payment method.
*
* @return bool
*/
private function is_add_payment_method_page(): bool {
/**
* Needed for WordPress `query_vars`.
*
* @psalm-suppress InvalidGlobal
*/
global $wp;
$page_id = wc_get_page_id( 'myaccount' );
return $page_id && is_page( $page_id ) && isset( $wp->query_vars['add-payment-method'] );
}
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult as AuthResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
/**
* Class ThreeDSecure
@ -23,6 +24,13 @@ class ThreeDSecure {
const REJECT = 2;
const RETRY = 3;
/**
* Card authentication result factory.
*
* @var CardAuthenticationResultFactory
*/
private $card_authentication_result_factory;
/**
* The logger.
*
@ -33,10 +41,15 @@ class ThreeDSecure {
/**
* ThreeDSecure constructor.
*
* @param LoggerInterface $logger The logger.
* @param CardAuthenticationResultFactory $card_authentication_result_factory Card authentication result factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct( LoggerInterface $logger ) {
$this->logger = $logger;
public function __construct(
CardAuthenticationResultFactory $card_authentication_result_factory,
LoggerInterface $logger
) {
$this->logger = $logger;
$this->card_authentication_result_factory = $card_authentication_result_factory;
}
/**
@ -49,29 +62,36 @@ class ThreeDSecure {
* @return int
*/
public function proceed_with_order( Order $order ): int {
if ( ! $order->payment_source() ) {
return self::NO_DECISION;
}
if ( ! $order->payment_source()->card() ) {
return self::NO_DECISION;
}
if ( ! $order->payment_source()->card()->authentication_result() ) {
$payment_source = $order->payment_source();
if ( ! $payment_source ) {
return self::NO_DECISION;
}
$result = $order->payment_source()->card()->authentication_result();
$this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) );
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
return self::PROCCEED;
if ( ! $payment_source->properties()->brand ?? '' ) {
return self::NO_DECISION;
}
if ( ! $payment_source->properties()->authentication_result ?? '' ) {
return self::NO_DECISION;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
return self::RETRY;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
return $this->no_liability_shift( $result );
$authentication_result = $payment_source->properties()->authentication_result ?? null;
if ( $authentication_result ) {
$result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result );
$this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) );
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
return self::PROCCEED;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) {
return self::RETRY;
}
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) {
return $this->no_liability_shift( $result );
}
}
return self::NO_DECISION;
}

View file

@ -89,6 +89,12 @@ class CardFieldsModule implements ModuleInterface {
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ): array {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$payment_method = wc_clean( wp_unslash( $_POST['payment_method'] ?? '' ) );
if ( $payment_method !== CreditCardGateway::ID ) {
return $data;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );

View file

@ -27,7 +27,7 @@ return array(
},
'compat.ppec.subscriptions-handler' => static function ( ContainerInterface $container ) {
$ppcp_renewal_handler = $container->get( 'subscription.renewal-handler' );
$ppcp_renewal_handler = $container->get( 'wc-subscriptions.renewal-handler' );
$gateway = $container->get( 'compat.ppec.mock-gateway' );
return new PPEC\SubscriptionsHandler( $ppcp_renewal_handler, $gateway );

View file

@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\Compat\PPEC;
use Automattic\WooCommerce\Utilities\OrderUtil;
use stdClass;
use WooCommerce\PayPalCommerce\Subscription\RenewalHandler;
use WooCommerce\PayPalCommerce\WcSubscriptions\RenewalHandler;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
/**

View file

@ -1,52 +1,9 @@
.ppcp-button-googlepay {
margin: 7px 0;
overflow: hidden;
min-height: 40px;
height: 45px;
&.ppcp-button-pill {
border-radius: 50px;
}
&.ppcp-button-minicart {
display: block;
height: 40px;
}
}
.woocommerce-checkout {
.ppcp-button-googlepay {
margin-top: 0;
}
}
.ppcp-has-googlepay-block {
.wp-block-woocommerce-checkout {
.ppcp-button-googlepay {
margin: 0;
height: 40px;
}
}
.wp-block-woocommerce-cart {
.ppcp-button-googlepay {
margin: 0;
height: 40px;
}
/* Workaround for blocks grid */
.wc-block-components-express-payment__event-buttons {
display: block;
li[id*="express-payment-method-ppcp-"] {
padding-bottom: 0;
}
}
}
}
.wp-admin {
.ppcp-button-googlepay {
pointer-events: none;
.wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {
.gpay-button {
min-width: 0 !important;
}
}

View file

@ -3,10 +3,13 @@ import {setVisible} from '../../../ppcp-button/resources/js/modules/Helper/Hidin
import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import UpdatePaymentData from "./Helper/UpdatePaymentData";
import {apmButtonsInit} from "../../../ppcp-button/resources/js/modules/Helper/ApmButtons";
class GooglepayButton {
constructor(context, externalHandler, buttonConfig, ppcpConfig) {
apmButtonsInit(ppcpConfig);
this.isInitialized = false;
this.context = context;
@ -111,7 +114,7 @@ class GooglepayButton {
}
if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) {
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway';
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway-paypal';
}
return config;
@ -168,6 +171,10 @@ class GooglepayButton {
this.waitForWrapper(wrapper, () => {
jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape);
if (ppcpStyle.height) {
jQuery(wrapper).css('height', `${ppcpStyle.height}px`)
}
const button =
this.paymentsClient.createButton({
onClick: this.onButtonClick.bind(this),

View file

@ -67,7 +67,7 @@ import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/Wi
buttonConfig.button.wrapper = selector;
applyConfigOptions(buttonConfig);
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-googlepay"></div>`;
const wrapperElement = `<div id="${selector.replace('#', '')}" class="ppcp-button-apm ppcp-button-googlepay"></div>`;
if (!jQuery(selector).length) {
jQuery(ppcpConfig.button.wrapper).after(wrapperElement);

View file

@ -24,13 +24,6 @@ const GooglePayComponent = () => {
manager.init();
};
useEffect(() => {
const bodyClass = 'ppcp-has-googlepay-block';
if (!document.body.classList.contains(bodyClass)) {
document.body.classList.add(bodyClass);
}
}, []);
useEffect(() => {
// Load GooglePay SDK
loadCustomScript({ url: buttonConfig.sdk_url }).then(() => {
@ -51,14 +44,13 @@ const GooglePayComponent = () => {
}, [paypalLoaded, googlePayLoaded]);
return (
<div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-googlepay"></div>
<div id={buttonConfig.button.wrapper.replace('#', '')} className="ppcp-button-apm ppcp-button-googlepay"></div>
);
}
const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
registerMethod({
registerExpressPaymentMethod({
name: buttonData.id,
label: <div dangerouslySetInnerHTML={{__html: buttonData.title}}/>,
content: <GooglePayComponent isEditing={false}/>,

View file

@ -311,7 +311,7 @@ class Button implements ButtonInterface {
add_action(
$render_placeholder,
function () {
echo '<span id="ppc-button-googlepay-container-minicart" class="ppcp-button-googlepay ppcp-button-minicart"></span>';
echo '<span id="ppc-button-googlepay-container-minicart" class="ppcp-button-apm ppcp-button-googlepay ppcp-button-minicart"></span>';
},
21
);
@ -325,7 +325,7 @@ class Button implements ButtonInterface {
*/
private function googlepay_button(): void {
?>
<div id="ppc-button-googlepay-container" class="ppcp-button-googlepay">
<div id="ppc-button-googlepay-container" class="ppcp-button-apm ppcp-button-googlepay">
<?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?>
</div>
<?php

View file

@ -1,6 +1,7 @@
<?php
/**
* Properties of the GooglePay module.
* ApmApplies helper.
* Checks if GooglePay is available for a given country and currency.
*
* @package WooCommerce\PayPalCommerce\Googlepay\Helper
*/
@ -15,7 +16,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper;
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for DCC.
* The matrix which countries and currency combinations can be used for GooglePay.
*
* @var array
*/
@ -38,7 +39,7 @@ class ApmApplies {
/**
* DccApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for GooglePay.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/

View file

@ -9,7 +9,7 @@ document.addEventListener(
const smartButtonLocationsSelector = '#field-smart_button_locations';
const smartButtonLocationsSelect = smartButtonLocationsSelector + ' select';
const smartButtonSelectableLocations = ['product', 'cart', 'checkout', 'mini-cart'];
const smartButtonSelectableLocations = ['product', 'cart', 'checkout', 'mini-cart', 'cart-block', 'checkout-block-express'];
const groupToggle = (selector, group) => {
const toggleElement = document.querySelector(selector);
@ -317,6 +317,7 @@ document.addEventListener(
'#field-button' + locationPrefix + '_label',
'#field-button' + locationPrefix + '_color',
'#field-button' + locationPrefix + '_shape',
'#field-button' + locationPrefix + '_height',
'#field-button' + locationPrefix + '_preview',
]
@ -324,11 +325,7 @@ document.addEventListener(
inputSelectors.push('#field-button_' + location + '_heading');
}
if (location === 'mini-cart') {
inputSelectors.push('#field-button' + locationPrefix + '_height');
}
return inputSelectors
return inputSelectors.filter(selector => document.querySelector(selector));
}
const allPayLaterMessaginginputSelectors = () => {

View file

@ -71,6 +71,12 @@ return array(
'api.paypal-host-sandbox' => static function( ContainerInterface $container ) : string {
return PAYPAL_SANDBOX_API_URL;
},
'api.paypal-website-url-production' => static function( ContainerInterface $container ) : string {
return PAYPAL_URL;
},
'api.paypal-website-url-sandbox' => static function( ContainerInterface $container ) : string {
return PAYPAL_SANDBOX_URL;
},
'api.partner_merchant_id-production' => static function( ContainerInterface $container ) : string {
return CONNECT_WOO_MERCHANT_ID;
},
@ -89,6 +95,15 @@ return array(
}
return $container->get( 'api.paypal-host-production' );
},
'api.paypal-website-url' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
if ( $environment->current_environment_is( Environment::SANDBOX ) ) {
return $container->get( 'api.paypal-website-url-sandbox' );
}
return $container->get( 'api.paypal-website-url-production' );
},
'api.bearer' => static function ( ContainerInterface $container ): Bearer {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-paypal-subscriptions",
"type": "dhii-mod",
"description": "Module for PayPal Subscriptions API integration",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\PayPalSubscriptions\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,14 @@
<?php
/**
* The PayPalSubscriptions module extensions.
*
* @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array();

View file

@ -0,0 +1,16 @@
<?php
/**
* The PayPalSubscriptions module.
*
* @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new PayPalSubscriptionsModule();
};

View file

@ -1,5 +1,5 @@
{
"name": "ppcp-subscription",
"name": "ppcp-paypal-subscriptions",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [

View file

@ -0,0 +1,43 @@
<?php
/**
* The PayPalSubscriptions module services.
*
* @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'paypal-subscriptions.deactivate-plan-endpoint' => static function ( ContainerInterface $container ): DeactivatePlanEndpoint {
return new DeactivatePlanEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.billing-plans' )
);
},
'paypal-subscriptions.api-handler' => static function( ContainerInterface $container ): SubscriptionsApiHandler {
return new SubscriptionsApiHandler(
$container->get( 'api.endpoint.catalog-products' ),
$container->get( 'api.factory.product' ),
$container->get( 'api.endpoint.billing-plans' ),
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' ),
$container->get( 'api.shop.currency' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'paypal-subscriptions.module.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-paypal-subscriptions/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
);

View file

@ -2,12 +2,12 @@
/**
* The deactivate Subscription Plan Endpoint.
*
* @package WooCommerce\PayPalCommerce\OrderTracking\Endpoint
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Exception;
use WC_Product;

View file

@ -1,50 +1,40 @@
<?php
/**
* The subscription module.
* The PayPalSubscriptions module.
*
* @package WooCommerce\PayPalCommerce\Subscription
* @package WooCommerce\PayPalCommerce\PayPalSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use ActionScheduler_Store;
use Exception;
use WC_Order;
use WC_Product;
use WC_Product_Subscription;
use WC_Product_Subscription_Variation;
use WC_Product_Variable;
use WC_Product_Variable_Subscription;
use WC_Subscription;
use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Subscription;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WP_Post;
/**
* Class SubscriptionModule
* Class SavedPaymentCheckerModule
*/
class SubscriptionModule implements ModuleInterface {
use TransactionIdHandlingTrait;
class PayPalSubscriptionsModule implements ModuleInterface {
/**
* {@inheritDoc}
@ -60,470 +50,6 @@ class SubscriptionModule implements ModuleInterface {
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
add_action(
'woocommerce_scheduled_subscription_payment_' . PayPalGateway::ID,
function ( $amount, $order ) use ( $c ) {
$this->renew( $order, $c );
},
10,
2
);
add_action(
'woocommerce_scheduled_subscription_payment_' . CreditCardGateway::ID,
function ( $amount, $order ) use ( $c ) {
$this->renew( $order, $c );
},
10,
2
);
add_action(
'woocommerce_subscription_payment_complete',
function ( $subscription ) use ( $c ) {
if ( ! in_array( $subscription->get_payment_method(), array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID ), true ) ) {
return;
}
$paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $paypal_subscription_id ) {
return;
}
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$this->add_payment_token_id( $subscription, $payment_token_repository, $logger );
if ( count( $subscription->get_related_orders() ) === 1 ) {
$parent_order = $subscription->get_parent();
if ( is_a( $parent_order, WC_Order::class ) ) {
$order_repository = $c->get( 'api.repository.order' );
$order = $order_repository->for_wc_order( $parent_order );
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id );
$subscription->save();
}
}
}
}
);
add_filter(
'woocommerce_gateway_description',
function ( $description, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'subscription.helper' );
return $this->display_saved_paypal_payments( $settings, (string) $id, $payment_token_repository, (string) $description, $subscription_helper );
},
10,
2
);
add_filter(
'woocommerce_credit_card_form_fields',
function ( $default_fields, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'subscription.helper' );
return $this->display_saved_credit_cards( $settings, $id, $payment_token_repository, $default_fields, $subscription_helper );
},
20,
2
);
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) );
if ( ! $subscription_id ) {
return $data;
}
$subscription = wc_get_order( $subscription_id );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $data;
}
if (
$wc_order_action === 'wcs_process_renewal' && $subscription->get_payment_method() === CreditCardGateway::ID
&& isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN'
&& isset( $data['payment_source']['token']['source']->card )
) {
$data['payment_source'] = array(
'card' => array(
'vault_id' => $data['payment_source']['token']['id'],
'stored_credential' => array(
'payment_initiator' => 'MERCHANT',
'payment_type' => 'RECURRING',
'usage' => 'SUBSEQUENT',
),
),
);
$previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' );
if ( $previous_transaction_reference ) {
$data['payment_source']['card']['stored_credential']['previous_transaction_reference'] = $previous_transaction_reference;
}
}
return $data;
}
);
$this->subscriptions_api_integration( $c );
add_action(
'admin_enqueue_scripts',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $hook ) use ( $c ) {
if ( ! is_string( $hook ) ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
$subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $hook !== 'post.php' || $subscription_mode !== 'subscriptions_api' ) {
return;
}
//phpcs:disable WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$product = wc_get_product( $post_id );
if ( ! ( is_a( $product, WC_Product::class ) ) ) {
return;
}
$subscriptions_helper = $c->get( 'subscription.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
! $subscriptions_helper->plugin_is_active()
|| ! (
is_a( $product, WC_Product_Subscription::class )
|| is_a( $product, WC_Product_Variable_Subscription::class )
|| is_a( $product, WC_Product_Subscription_Variation::class )
)
|| ! WC_Subscriptions_Product::is_subscription( $product )
) {
return;
}
$module_url = $c->get( 'subscription.module.url' );
wp_enqueue_script(
'ppcp-paypal-subscription',
untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
$products = array( $this->set_product_config( $product ) );
if ( $product->get_type() === 'variable-subscription' ) {
$products = array();
/**
* Suppress pslam.
*
* @psalm-suppress TypeDoesNotContainType
*
* WC_Product_Variable_Subscription extends WC_Product_Variable.
*/
assert( $product instanceof WC_Product_Variable );
$available_variations = $product->get_available_variations();
foreach ( $available_variations as $variation ) {
/**
* The method is defined in WooCommerce.
*
* @psalm-suppress UndefinedMethod
*/
$variation = wc_get_product_object( 'variation', $variation['variation_id'] );
$products[] = $this->set_product_config( $variation );
}
}
wp_localize_script(
'ppcp-paypal-subscription',
'PayPalCommerceGatewayPayPalSubscriptionProducts',
$products
);
}
);
$endpoint = $c->get( 'subscription.deactivate-plan-endpoint' );
assert( $endpoint instanceof DeactivatePlanEndpoint );
add_action(
'wc_ajax_' . DeactivatePlanEndpoint::ENDPOINT,
array( $endpoint, 'handle_request' )
);
add_action(
'add_meta_boxes',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( string $post_type, $post_or_order_object ) use ( $c ) {
if ( ! function_exists( 'wcs_get_subscription' ) ) {
return;
}
$order = ( $post_or_order_object instanceof WP_Post )
? wc_get_order( $post_or_order_object->ID )
: $post_or_order_object;
if ( ! is_a( $order, WC_Order::class ) ) {
return;
}
$subscription = wcs_get_subscription( $order->get_id() );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return;
}
$screen_id = wc_get_page_screen_id( 'shop_subscription' );
remove_meta_box( 'woocommerce-subscription-schedule', $screen_id, 'side' );
$environment = $c->get( 'onboarding.environment' );
add_meta_box(
'ppcp_paypal_subscription',
__( 'PayPal Subscription', 'woocommerce-paypal-payments' ),
function() use ( $subscription_id, $environment ) {
$host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
$url = trailingslashit( $host ) . 'billing/subscriptions/' . $subscription_id;
echo '<p>' . esc_html__( 'This subscription is linked to a PayPal Subscription, Cancel it to unlink.', 'woocommerce-paypal-payments' ) . '</p>';
echo '<p><strong>' . esc_html__( 'Subscription:', 'woocommerce-paypal-payments' ) . '</strong> <a href="' . esc_url( $url ) . '" target="_blank">' . esc_attr( $subscription_id ) . '</a></p>';
},
$post_type,
'side'
);
},
30,
2
);
add_action(
'action_scheduler_before_execute',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $action_id ) {
/**
* Class exist in WooCommerce.
*
* @psalm-suppress UndefinedClass
*/
$store = ActionScheduler_Store::instance();
$action = $store->fetch_action( $action_id );
$subscription_id = $action->get_args()['subscription_id'] ?? null;
if ( $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( is_a( $subscription, WC_Subscription::class ) ) {
$paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $paypal_subscription_id ) {
as_unschedule_action( $action->get_hook(), $action->get_args() );
}
}
}
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
/**
* Handles a Subscription product renewal.
*
* @param \WC_Order $order WooCommerce order.
* @param ContainerInterface|null $container The container.
* @return void
*/
protected function renew( $order, $container ) {
if ( ! ( $order instanceof \WC_Order ) ) {
return;
}
$handler = $container->get( 'subscription.renewal-handler' );
$handler->renew( $order );
}
/**
* Adds Payment token ID to subscription.
*
* @param \WC_Subscription $subscription The subscription.
* @param PaymentTokenRepository $payment_token_repository The payment repository.
* @param LoggerInterface $logger The logger.
*/
protected function add_payment_token_id(
\WC_Subscription $subscription,
PaymentTokenRepository $payment_token_repository,
LoggerInterface $logger
) {
try {
$tokens = $payment_token_repository->all_for_user_id( $subscription->get_customer_id() );
if ( $tokens ) {
$latest_token_id = end( $tokens )->id() ? end( $tokens )->id() : '';
$subscription->update_meta_data( 'payment_token_id', $latest_token_id );
$subscription->save();
}
} catch ( RuntimeException $error ) {
$message = sprintf(
// translators: %1$s is the payment token Id, %2$s is the error message.
__(
'Could not add token Id to subscription %1$s: %2$s',
'woocommerce-paypal-payments'
),
$subscription->get_id(),
$error->getMessage()
);
$logger->log( 'warning', $message );
}
}
/**
* Displays saved PayPal payments.
*
* @param Settings $settings The settings.
* @param string $id The payment gateway Id.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param string $description The payment gateway description.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @return string
*/
protected function display_saved_paypal_payments(
Settings $settings,
string $id,
PaymentTokenRepository $payment_token_repository,
string $description,
SubscriptionHelper $subscription_helper
): string {
if ( $settings->has( 'vault_enabled' )
&& $settings->get( 'vault_enabled' )
&& PayPalGateway::ID === $id
&& $subscription_helper->is_subscription_change_payment()
) {
$tokens = $payment_token_repository->all_for_user_id( get_current_user_id() );
if ( ! $tokens || ! $payment_token_repository->tokens_contains_paypal( $tokens ) ) {
return esc_html__(
'No PayPal payments saved, in order to use a saved payment you first need to create it through a purchase.',
'woocommerce-paypal-payments'
);
}
$output = sprintf(
'<p class="form-row form-row-wide"><label>%1$s</label><select id="saved-paypal-payment" name="saved_paypal_payment">',
esc_html__( 'Select a saved PayPal payment', 'woocommerce-paypal-payments' )
);
foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) {
$output .= sprintf(
'<option value="%1$s">%2$s</option>',
$token->id(),
$token->source()->paypal->payer->email_address
);
}
}
$output .= '</select></p>';
return $output;
}
return $description;
}
/**
* Displays saved credit cards.
*
* @param Settings $settings The settings.
* @param string $id The payment gateway Id.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param array $default_fields Default payment gateway fields.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @return array|mixed|string
* @throws NotFoundException When setting was not found.
*/
protected function display_saved_credit_cards(
Settings $settings,
string $id,
PaymentTokenRepository $payment_token_repository,
array $default_fields,
SubscriptionHelper $subscription_helper
) {
if ( $settings->has( 'vault_enabled_dcc' )
&& $settings->get( 'vault_enabled_dcc' )
&& $subscription_helper->is_subscription_change_payment()
&& CreditCardGateway::ID === $id
) {
$tokens = $payment_token_repository->all_for_user_id( get_current_user_id() );
if ( ! $tokens || ! $payment_token_repository->tokens_contains_card( $tokens ) ) {
$default_fields = array();
$default_fields['saved-credit-card'] = esc_html__(
'No Credit Card saved, in order to use a saved Credit Card you first need to create it through a purchase.',
'woocommerce-paypal-payments'
);
return $default_fields;
}
$output = sprintf(
'<p class="form-row form-row-wide"><label>%1$s</label><select id="saved-credit-card" name="saved_credit_card">',
esc_html__( 'Select a saved Credit Card payment', 'woocommerce-paypal-payments' )
);
foreach ( $tokens as $token ) {
if ( isset( $token->source()->card ) ) {
$output .= sprintf(
'<option value="%1$s">%2$s ...%3$s</option>',
$token->id(),
$token->source()->card->brand,
$token->source()->card->last_digits
);
}
}
$output .= '</select></p>';
$default_fields = array();
$default_fields['saved-credit-card'] = $output;
return $default_fields;
}
return $default_fields;
}
/**
* Adds PayPal subscriptions API integration.
*
* @param ContainerInterface $c The container.
* @return void
* @throws Exception When something went wrong.
*/
protected function subscriptions_api_integration( ContainerInterface $c ): void {
add_action(
'save_post',
/**
@ -554,7 +80,7 @@ class SubscriptionModule implements ModuleInterface {
return;
}
$subscriptions_api_handler = $c->get( 'subscription.api-handler' );
$subscriptions_api_handler = $c->get( 'paypal-subscriptions.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
$this->update_subscription_product_meta( $product, $subscriptions_api_handler );
},
@ -571,7 +97,7 @@ class SubscriptionModule implements ModuleInterface {
function( $variation_id ) use ( $c ) {
$wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) );
$subscriptions_helper = $c->get( 'subscription.helper' );
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
@ -588,7 +114,7 @@ class SubscriptionModule implements ModuleInterface {
return;
}
$subscriptions_api_handler = $c->get( 'subscription.api-handler' );
$subscriptions_api_handler = $c->get( 'paypal-subscriptions.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
$this->update_subscription_product_meta( $product, $subscriptions_api_handler );
},
@ -947,6 +473,236 @@ class SubscriptionModule implements ModuleInterface {
10,
3
);
add_action(
'admin_enqueue_scripts',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $hook ) use ( $c ) {
if ( ! is_string( $hook ) ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
$subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $hook !== 'post.php' || $subscription_mode !== 'subscriptions_api' ) {
return;
}
//phpcs:disable WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$product = wc_get_product( $post_id );
if ( ! ( is_a( $product, WC_Product::class ) ) ) {
return;
}
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
! $subscriptions_helper->plugin_is_active()
|| ! (
is_a( $product, WC_Product_Subscription::class )
|| is_a( $product, WC_Product_Variable_Subscription::class )
|| is_a( $product, WC_Product_Subscription_Variation::class )
)
|| ! WC_Subscriptions_Product::is_subscription( $product )
) {
return;
}
$module_url = $c->get( 'paypal-subscriptions.module.url' );
wp_enqueue_script(
'ppcp-paypal-subscription',
untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
$products = array( $this->set_product_config( $product ) );
if ( $product->get_type() === 'variable-subscription' ) {
$products = array();
/**
* Suppress pslam.
*
* @psalm-suppress TypeDoesNotContainType
*
* WC_Product_Variable_Subscription extends WC_Product_Variable.
*/
assert( $product instanceof WC_Product_Variable );
$available_variations = $product->get_available_variations();
foreach ( $available_variations as $variation ) {
/**
* The method is defined in WooCommerce.
*
* @psalm-suppress UndefinedMethod
*/
$variation = wc_get_product_object( 'variation', $variation['variation_id'] );
$products[] = $this->set_product_config( $variation );
}
}
wp_localize_script(
'ppcp-paypal-subscription',
'PayPalCommerceGatewayPayPalSubscriptionProducts',
$products
);
}
);
$endpoint = $c->get( 'paypal-subscriptions.deactivate-plan-endpoint' );
assert( $endpoint instanceof DeactivatePlanEndpoint );
add_action(
'wc_ajax_' . DeactivatePlanEndpoint::ENDPOINT,
array( $endpoint, 'handle_request' )
);
add_action(
'add_meta_boxes',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( string $post_type, $post_or_order_object ) use ( $c ) {
if ( ! function_exists( 'wcs_get_subscription' ) ) {
return;
}
$order = ( $post_or_order_object instanceof WP_Post )
? wc_get_order( $post_or_order_object->ID )
: $post_or_order_object;
if ( ! is_a( $order, WC_Order::class ) ) {
return;
}
$subscription = wcs_get_subscription( $order->get_id() );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return;
}
$screen_id = wc_get_page_screen_id( 'shop_subscription' );
remove_meta_box( 'woocommerce-subscription-schedule', $screen_id, 'side' );
$host = $c->get( 'api.paypal-website-url' );
add_meta_box(
'ppcp_paypal_subscription',
__( 'PayPal Subscription', 'woocommerce-paypal-payments' ),
function() use ( $subscription_id, $host ) {
$url = trailingslashit( $host ) . 'billing/subscriptions/' . $subscription_id;
echo '<p>' . esc_html__( 'This subscription is linked to a PayPal Subscription, Cancel it to unlink.', 'woocommerce-paypal-payments' ) . '</p>';
echo '<p><strong>' . esc_html__( 'Subscription:', 'woocommerce-paypal-payments' ) . '</strong> <a href="' . esc_url( $url ) . '" target="_blank">' . esc_attr( $subscription_id ) . '</a></p>';
},
$post_type,
'side'
);
},
30,
2
);
add_action(
'action_scheduler_before_execute',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $action_id ) {
/**
* Class exist in WooCommerce.
*
* @psalm-suppress UndefinedClass
*/
$store = ActionScheduler_Store::instance();
$action = $store->fetch_action( $action_id );
$subscription_id = $action->get_args()['subscription_id'] ?? null;
if ( $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( is_a( $subscription, WC_Subscription::class ) ) {
$paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $paypal_subscription_id ) {
as_unschedule_action( $action->get_hook(), $action->get_args() );
}
}
}
}
);
}
/**
* Updates subscription product meta.
*
* @param WC_Product $product The product.
* @param SubscriptionsApiHandler $subscriptions_api_handler The subscription api handler.
* @return void
*/
private function update_subscription_product_meta( WC_Product $product, SubscriptionsApiHandler $subscriptions_api_handler ): void {
// phpcs:ignore WordPress.Security.NonceVerification
$enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) );
$product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product );
$product->save();
if ( ( $product->get_type() === 'subscription' || $product->get_type() === 'subscription_variation' ) && $enable_subscription_product === 'yes' ) {
if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
$subscriptions_api_handler->update_product( $product );
$subscriptions_api_handler->update_plan( $product );
return;
}
if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) {
$subscriptions_api_handler->create_product( $product );
}
if ( $product->meta_exists( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) {
// phpcs:ignore WordPress.Security.NonceVerification
$subscription_plan_name = wc_clean( wp_unslash( $_POST['_ppcp_subscription_plan_name'] ?? '' ) );
if ( ! is_string( $subscription_plan_name ) ) {
return;
}
$product->update_meta_data( '_ppcp_subscription_plan_name', $subscription_plan_name );
$product->save();
$subscriptions_api_handler->create_plan( $subscription_plan_name, $product );
}
}
}
/**
* Returns subscription product configuration.
*
* @param WC_Product $product The product.
* @return array
*/
private function set_product_config( WC_Product $product ): array {
$plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array();
$plan_id = $plan['id'] ?? '';
return array(
'product_connected' => $product->get_meta( '_ppcp_enable_subscription_product' ) ?? '',
'plan_id' => $plan_id,
'product_id' => $product->get_id(),
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
);
}
/**
@ -1019,66 +775,4 @@ class SubscriptionModule implements ModuleInterface {
);
}
}
/**
* Updates subscription product meta.
*
* @param WC_Product $product The product.
* @param SubscriptionsApiHandler $subscriptions_api_handler The subscription api handler.
* @return void
*/
private function update_subscription_product_meta( WC_Product $product, SubscriptionsApiHandler $subscriptions_api_handler ): void {
// phpcs:ignore WordPress.Security.NonceVerification
$enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) );
$product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product );
$product->save();
if ( ( $product->get_type() === 'subscription' || $product->get_type() === 'subscription_variation' ) && $enable_subscription_product === 'yes' ) {
if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
$subscriptions_api_handler->update_product( $product );
$subscriptions_api_handler->update_plan( $product );
return;
}
if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) {
$subscriptions_api_handler->create_product( $product );
}
if ( $product->meta_exists( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) {
// phpcs:ignore WordPress.Security.NonceVerification
$subscription_plan_name = wc_clean( wp_unslash( $_POST['_ppcp_subscription_plan_name'] ?? '' ) );
if ( ! is_string( $subscription_plan_name ) ) {
return;
}
$product->update_meta_data( '_ppcp_subscription_plan_name', $subscription_plan_name );
$product->save();
$subscriptions_api_handler->create_plan( $subscription_plan_name, $product );
}
}
}
/**
* Returns subscription product configuration.
*
* @param WC_Product $product The product.
* @return array
*/
private function set_product_config( WC_Product $product ): array {
$plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array();
$plan_id = $plan['id'] ?? '';
return array(
'product_connected' => $product->get_meta( '_ppcp_enable_subscription_product' ) ?? '',
'plan_id' => $plan_id,
'product_id' => $product->get_id(),
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
);
}
}

View file

@ -2,12 +2,12 @@
/**
* The subscription module.
*
* @package WooCommerce\PayPalCommerce\Subscription
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Psr\Log\LoggerInterface;
use WC_Product;

View file

@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.25.0"
}
],
[
"@babel/preset-react"
]
]
}

View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -1,7 +1,7 @@
{
"name": "woocommerce/ppcp-subscription",
"name": "woocommerce/ppcp-save-payment-methods",
"type": "dhii-mod",
"description": "Subscription module for PPCP",
"description": "Save payment methods module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
@ -9,7 +9,7 @@
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\Subscription\\": "src"
"WooCommerce\\PayPalCommerce\\SavePaymentMethods\\": "src"
}
},
"minimum-stability": "dev",

View file

@ -0,0 +1,14 @@
<?php
/**
* The save payment methods module extensions.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array();

View file

@ -0,0 +1,16 @@
<?php
/**
* The save payment methods module.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new SavePaymentMethodsModule();
};

View file

@ -0,0 +1,34 @@
{
"name": "ppcp-save-payment-methods",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0",
"@paypal/paypal-js": "^6.0.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,182 @@
import {
getCurrentPaymentMethod,
ORDER_BUTTON_SELECTOR,
PaymentMethods
} from "../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState";
import {loadScript} from "@paypal/paypal-js";
import {
setVisible,
setVisibleByClass
} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
import ErrorHandler from "../../../ppcp-button/resources/js/modules/ErrorHandler";
import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper";
const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const init = () => {
setVisibleByClass(ORDER_BUTTON_SELECTOR, getCurrentPaymentMethod() !== PaymentMethods.PAYPAL, 'ppcp-hidden');
setVisible(`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`, getCurrentPaymentMethod() === PaymentMethods.PAYPAL);
}
document.addEventListener(
'DOMContentLoaded',
() => {
jQuery(document.body).on('click init_add_payment_method', '.payment_methods input.input-radio', function () {
init()
});
setTimeout(() => {
loadScript({
clientId: ppcp_add_payment_method.client_id,
merchantId: ppcp_add_payment_method.merchant_id,
dataUserIdToken: ppcp_add_payment_method.id_token,
components: 'buttons,card-fields',
})
.then((paypal) => {
errorHandler.clear();
paypal.Buttons(
{
createVaultSetupToken: async () => {
const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce,
})
})
const result = await response.json()
if (result.data.id) {
return result.data.id
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
})
})
const result = await response.json();
if(result.success === true) {
window.location.href = ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onError: (error) => {
console.error(error)
errorHandler.message(ppcp_add_payment_method.error_message);
}
},
).render(`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`);
const cardField = paypal.CardFields({
createVaultSetupToken: async () => {
const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce,
payment_method: PaymentMethods.CARDS,
verification_method: ppcp_add_payment_method.verification_method
})
})
const result = await response.json()
if (result.data.id) {
return result.data.id
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
payment_method: PaymentMethods.CARDS
})
})
const result = await response.json();
if(result.success === true) {
window.location.href = ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message(ppcp_add_payment_method.error_message);
},
onError: (error) => {
console.error(error)
errorHandler.message(ppcp_add_payment_method.error_message);
}
});
if (cardField.isEligible()) {
const nameField = document.getElementById('ppcp-credit-card-gateway-card-name');
if (nameField) {
let styles = cardFieldStyles(nameField);
cardField.NameField({style: {'input': styles}}).render(nameField.parentNode);
nameField.hidden = true;
}
const numberField = document.getElementById('ppcp-credit-card-gateway-card-number');
if (numberField) {
let styles = cardFieldStyles(numberField);
cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode);
numberField.hidden = true;
}
const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry');
if (expiryField) {
let styles = cardFieldStyles(expiryField);
cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode);
expiryField.hidden = true;
}
const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc');
if (cvvField) {
let styles = cardFieldStyles(cvvField);
cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode);
cvvField.hidden = true;
}
}
document.querySelector('#place_order').addEventListener("click", (event) => {
event.preventDefault();
cardField.submit()
.catch((error) => {
console.error(error)
});
});
})
}, 1000)
}
);

View file

@ -0,0 +1,90 @@
<?php
/**
* The save payment methods module services.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'save-payment-methods.eligible' => static function ( ContainerInterface $container ): bool {
$save_payment_methods_applies = $container->get( 'save-payment-methods.helpers.save-payment-methods-applies' );
assert( $save_payment_methods_applies instanceof SavePaymentMethodsApplies );
return $save_payment_methods_applies->for_country_currency();
},
'save-payment-methods.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : SavePaymentMethodsApplies {
return new SavePaymentMethodsApplies(
$container->get( 'save-payment-methods.supported-country-currency-matrix' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
'save-payment-methods.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
return apply_filters(
'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix',
array(
'US' => array(
'AUD',
'CAD',
'EUR',
'GBP',
'JPY',
'USD',
),
)
);
},
'save-payment-methods.module.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-save-payment-methods/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'save-payment-methods.endpoint.create-setup-token' => static function ( ContainerInterface $container ): CreateSetupToken {
return new CreateSetupToken(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-method-tokens' )
);
},
'save-payment-methods.wc-payment-tokens' => static function( ContainerInterface $container ): WooCommercePaymentTokens {
return new WooCommercePaymentTokens(
$container->get( 'vaulting.payment-token-helper' ),
$container->get( 'vaulting.payment-token-factory' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'save-payment-methods.endpoint.create-payment-token' => static function ( ContainerInterface $container ): CreatePaymentToken {
return new CreatePaymentToken(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-method-tokens' ),
$container->get( 'save-payment-methods.wc-payment-tokens' )
);
},
'save-payment-methods.endpoint.capture-card-payment' => static function( ContainerInterface $container ): CaptureCardPayment {
return new CaptureCardPayment(
$container->get( 'button.request-data' ),
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,219 @@
<?php
/**
* The Capture Card Payment endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Psr\Log\LoggerInterface;
use RuntimeException;
use stdClass;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WP_Error;
/**
* Class CaptureCardPayment
*/
class CaptureCardPayment implements EndpointInterface {
use RequestTrait;
const ENDPOINT = 'ppc-capture-card-payment';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The order factory.
*
* @var OrderFactory
*/
private $order_factory;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CaptureCardPayment constructor.
*
* @param RequestData $request_data The request data.
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param OrderFactory $order_factory The order factory.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
string $host,
Bearer $bearer,
OrderFactory $order_factory,
PurchaseUnitFactory $purchase_unit_factory,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->host = $host;
$this->bearer = $bearer;
$this->order_factory = $order_factory;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->order_endpoint = $order_endpoint;
$this->logger = $logger;
$this->session_handler = $session_handler;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() );
foreach ( $tokens as $token ) {
if ( $token->get_id() === (int) $data['payment_token'] ) {
try {
$order = $this->create_order( $token->get_token() );
$id = $order->id ?? '';
$status = $order->status ?? '';
$payment_source = isset( $order->payment_source->card ) ? 'card' : '';
if ( $id && $status && $payment_source ) {
WC()->session->set(
'ppcp_saved_payment_card',
array(
'order_id' => $id,
'status' => $status,
'payment_source' => $payment_source,
)
);
wp_send_json_success();
return true;
}
} catch ( RuntimeException $exception ) {
wp_send_json_error();
return false;
}
}
}
wp_send_json_error();
return false;
}
/**
* Creates PayPal order from the given card vault id.
*
* @param string $vault_id Vault id.
* @return stdClass
* @throws RuntimeException When request fails.
*/
private function create_order( string $vault_id ): stdClass {
$items = array( $this->purchase_unit_factory->from_wc_cart() );
$data = array(
'intent' => 'CAPTURE',
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array( true, false );
},
$items
),
'payment_source' => array(
'card' => array(
'vault_id' => $vault_id,
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
return json_decode( $response['body'] );
}
}

View file

@ -0,0 +1,143 @@
<?php
/**
* The Create Payment Token endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\SavePaymentMethods\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class CreatePaymentToken
*/
class CreatePaymentToken implements EndpointInterface {
const ENDPOINT = 'ppc-create-payment-token';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The payment method tokens endpoint.
*
* @var PaymentMethodTokensEndpoint
*/
private $payment_method_tokens_endpoint;
/**
* The WC payment tokens.
*
* @var WooCommercePaymentTokens
*/
private $wc_payment_tokens;
/**
* CreatePaymentToken constructor.
*
* @param RequestData $request_data The request data.
* @param PaymentMethodTokensEndpoint $payment_method_tokens_endpoint The payment method tokens endpoint.
* @param WooCommercePaymentTokens $wc_payment_tokens The WC payment tokens.
*/
public function __construct(
RequestData $request_data,
PaymentMethodTokensEndpoint $payment_method_tokens_endpoint,
WooCommercePaymentTokens $wc_payment_tokens
) {
$this->request_data = $request_data;
$this->payment_method_tokens_endpoint = $payment_method_tokens_endpoint;
$this->wc_payment_tokens = $wc_payment_tokens;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws Exception On Error.
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
/**
* Suppress ArgumentTypeCoercion
*
* @psalm-suppress ArgumentTypeCoercion
*/
$payment_source = new PaymentSource(
'token',
(object) array(
'id' => $data['vault_setup_token'],
'type' => 'SETUP_TOKEN',
)
);
$result = $this->payment_method_tokens_endpoint->payment_tokens( $payment_source );
if ( is_user_logged_in() && isset( $result->customer->id ) ) {
update_user_meta( get_current_user_id(), '_ppcp_target_customer_id', $result->customer->id );
if ( isset( $result->payment_source->paypal ) ) {
$email = '';
if ( isset( $result->payment_source->paypal->email_address ) ) {
$email = $result->payment_source->paypal->email_address;
}
$this->wc_payment_tokens->create_payment_token_paypal(
get_current_user_id(),
$result->id,
$email
);
}
if ( isset( $result->payment_source->card ) ) {
$token = new \WC_Payment_Token_CC();
$token->set_token( $result->id );
$token->set_user_id( get_current_user_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $result->payment_source->card->last_digits ?? '' );
$expiry = explode( '-', $result->payment_source->card->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$brand = $result->payment_source->card->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
if ( $brand ) {
$token->set_card_type( $brand );
}
$token->save();
}
}
wp_send_json_success( $result );
return true;
} catch ( Exception $exception ) {
wp_send_json_error();
return false;
}
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* The Create Setup Token endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
/**
* Class CreateSetupToken
*/
class CreateSetupToken implements EndpointInterface {
const ENDPOINT = 'ppc-create-setup-token';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* Payment Method Tokens endpoint.
*
* @var PaymentMethodTokensEndpoint
*/
private $payment_method_tokens_endpoint;
/**
* CreateSetupToken constructor.
*
* @param RequestData $request_data The request data helper.
* @param PaymentMethodTokensEndpoint $payment_method_tokens_endpoint Payment Method Tokens endpoint.
*/
public function __construct(
RequestData $request_data,
PaymentMethodTokensEndpoint $payment_method_tokens_endpoint
) {
$this->request_data = $request_data;
$this->payment_method_tokens_endpoint = $payment_method_tokens_endpoint;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws Exception On Error.
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$payment_source = new PaymentSource(
'paypal',
(object) array(
'usage_type' => 'MERCHANT',
'experience_context' => (object) array(
'return_url' => esc_url( wc_get_account_endpoint_url( 'payment-methods' ) ),
'cancel_url' => esc_url( wc_get_account_endpoint_url( 'add-payment-method' ) ),
),
)
);
$payment_method = $data['payment_method'] ?? '';
if ( $payment_method === CreditCardGateway::ID ) {
$properties = (object) array();
$verification_method = $data['verification_method'] ?? '';
if ( $verification_method === 'SCA_WHEN_REQUIRED' || $verification_method === 'SCA_ALWAYS' ) {
$properties = (object) array(
'verification_method' => $verification_method,
'usage_type' => 'MERCHANT',
'experience_context' => (object) array(
'return_url' => esc_url( wc_get_account_endpoint_url( 'payment-methods' ) ),
'cancel_url' => esc_url( wc_get_account_endpoint_url( 'add-payment-method' ) ),
),
);
}
$payment_source = new PaymentSource(
'card',
$properties
);
}
$result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source );
wp_send_json_success( $result );
return true;
} catch ( Exception $exception ) {
wp_send_json_error();
return false;
}
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* Properties of the Save Payment Methods module.
*
* @package WooCommerce\PayPalCommerce\SavePaymentMethods\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Helper;
/**
* Class SavePaymentMethodsApplies
*/
class SavePaymentMethodsApplies {
/**
* The matrix which countries and currency combinations can be used for Save Payment Methods.
*
* @var array
*/
private $allowed_country_currency_matrix;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* SavePaymentMethodsApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for Save Payment Methods.
* @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,
string $currency,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
}
/**
* Returns whether Save Payment Methods can be used in the current country and the current currency used.
*
* @return bool
*/
public function for_country_currency(): bool {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
}
}

View file

@ -0,0 +1,377 @@
<?php
/**
* The save payment methods module.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class SavePaymentMethodsModule
*/
class SavePaymentMethodsModule implements ModuleInterface {
use ContextTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
if ( ! $c->get( 'save-payment-methods.eligible' ) ) {
return;
}
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function( array $localized_script_data ) use ( $c ) {
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$localized_script_data = $this->add_id_token_to_script_data( $api, $logger, $localized_script_data );
$localized_script_data['ajax']['capture_card_payment'] = array(
'endpoint' => \WC_AJAX::get_endpoint( CaptureCardPayment::ENDPOINT ),
'nonce' => wp_create_nonce( CaptureCardPayment::nonce() ),
);
return $localized_script_data;
}
);
// Adds attributes needed to save payment method.
add_filter(
'ppcp_create_order_request_body_data',
function( array $data, string $payment_method, array $request_data ): array {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
if ( $wc_order_action === 'wcs_process_renewal' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) );
$subscription = wcs_get_subscription( (int) $subscription_id );
if ( $subscription ) {
$customer_id = $subscription->get_customer_id();
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
foreach ( $wc_tokens as $token ) {
$data['payment_source'] = array(
'paypal' => array(
'vault_id' => $token->get_token(),
),
);
return $data;
}
}
}
if ( $payment_method === CreditCardGateway::ID ) {
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( $save_payment_method ) {
$data['payment_source'] = array(
'card' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( $target_customer_id ) {
$data['payment_source']['card']['attributes']['customer'] = array(
'id' => $target_customer_id,
);
}
}
}
if ( $payment_method === PayPalGateway::ID ) {
$data['payment_source'] = array(
'paypal' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
),
),
),
);
}
return $data;
},
10,
3
);
add_action(
'woocommerce_paypal_payments_after_order_processor',
function( WC_Order $wc_order, Order $order ) use ( $c ) {
$payment_source = $order->payment_source();
assert( $payment_source instanceof PaymentSource );
$payment_vault_attributes = $payment_source->properties()->attributes->vault ?? null;
if ( $payment_vault_attributes ) {
$customer_id = $payment_vault_attributes->customer->id ?? '';
$token_id = $payment_vault_attributes->id ?? '';
if ( ! $customer_id || ! $token_id ) {
return;
}
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
$wc_payment_tokens = $c->get( 'save-payment-methods.wc-payment-tokens' );
assert( $wc_payment_tokens instanceof WooCommercePaymentTokens );
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$token = new \WC_Payment_Token_CC();
$token->set_token( $token_id );
$token->set_user_id( $wc_order->get_customer_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $payment_source->properties()->last_digits ?? '' );
$expiry = explode( '-', $payment_source->properties()->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$token->set_card_type( $payment_source->properties()->brand ?? '' );
$token->save();
}
if ( $wc_order->get_payment_method() === PayPalGateway::ID ) {
$wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$token_id,
$payment_source->properties()->email_address ?? ''
);
}
}
},
10,
2
);
add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' );
add_filter( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', '__return_false' );
add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' );
add_action(
'wp_enqueue_scripts',
function() use ( $c ) {
if ( ! is_user_logged_in() || ! $this->is_add_payment_method_page() ) {
return;
}
$module_url = $c->get( 'save-payment-methods.module.url' );
wp_enqueue_script(
'ppcp-add-payment-method',
untrailingslashit( $module_url ) . '/assets/js/add-payment-method.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
try {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
}
$id_token = $api->id_token( $target_customer_id );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$verification_method = $settings->has( '3d_secure_contingency' ) ? $settings->get( '3d_secure_contingency' ) : '';
wp_localize_script(
'ppcp-add-payment-method',
'ppcp_add_payment_method',
array(
'client_id' => $c->get( 'button.client_id' ),
'merchant_id' => $c->get( 'api.merchant_id' ),
'id_token' => $id_token,
'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ),
'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ),
'verification_method' => $verification_method,
'ajax' => array(
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),
),
'create_payment_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ),
),
),
)
);
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
}
);
add_action(
'woocommerce_add_payment_method_form_bottom',
function () {
if ( ! is_user_logged_in() || ! is_add_payment_method_page() ) {
return;
}
echo '<div id="ppc-button-' . esc_attr( PayPalGateway::ID ) . '-save-payment-method"></div>';
}
);
add_action(
'wc_ajax_' . CreateSetupToken::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' );
assert( $endpoint instanceof CreateSetupToken );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . CreatePaymentToken::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' );
assert( $endpoint instanceof CreatePaymentToken );
$endpoint->handle_request();
}
);
add_action(
'woocommerce_paypal_payments_before_delete_payment_token',
function( string $token_id ) use ( $c ) {
try {
$endpoint = $c->get( 'api.endpoint.payment-tokens' );
assert( $endpoint instanceof PaymentTokensEndpoint );
$endpoint->delete( $token_id );
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
}
);
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_vault_supports',
function( array $supports ) use ( $c ): array {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
if ( $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) ) {
$supports[] = 'tokenization';
}
return $supports;
}
);
add_action(
'wc_ajax_' . CaptureCardPayment::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.capture-card-payment' );
assert( $endpoint instanceof CaptureCardPayment );
$endpoint->handle_request();
}
);
}
/**
* Adds id token to localized script data.
*
* @param UserIdToken $api User id token api.
* @param LoggerInterface $logger The logger.
* @param array $localized_script_data The localized script data.
* @return array
*/
private function add_id_token_to_script_data(
UserIdToken $api,
LoggerInterface $logger,
array $localized_script_data
): array {
try {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
}
$id_token = $api->id_token( $target_customer_id );
$localized_script_data['save_payment_methods'] = array(
'id_token' => $id_token,
);
$localized_script_data['data_client_id']['set_attribute'] = false;
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
return $localized_script_data;
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* Service to create WC Payment Tokens.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenPayPal;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class WooCommercePaymentTokens
*/
class WooCommercePaymentTokens {
/**
* The payment token helper.
*
* @var PaymentTokenHelper
*/
private $payment_token_helper;
/**
* The payment token factory.
*
* @var PaymentTokenFactory
*/
private $payment_token_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* WooCommercePaymentTokens constructor.
*
* @param PaymentTokenHelper $payment_token_helper The payment token helper.
* @param PaymentTokenFactory $payment_token_factory The payment token factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
PaymentTokenHelper $payment_token_helper,
PaymentTokenFactory $payment_token_factory,
LoggerInterface $logger
) {
$this->payment_token_helper = $payment_token_helper;
$this->payment_token_factory = $payment_token_factory;
$this->logger = $logger;
}
/**
* Creates a WC Payment Token for PayPal payment.
*
* @param int $customer_id The WC customer ID.
* @param string $token The PayPal payment token.
* @param string $email The PayPal customer email.
*
* @return void
*/
public function create_payment_token_paypal(
int $customer_id,
string $token,
string $email
): void {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID );
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token ) ) {
return;
}
$payment_token_paypal = $this->payment_token_factory->create( 'paypal' );
assert( $payment_token_paypal instanceof PaymentTokenPayPal );
$payment_token_paypal->set_token( $token );
$payment_token_paypal->set_user_id( $customer_id );
$payment_token_paypal->set_gateway_id( PayPalGateway::ID );
if ( $email && is_email( $email ) ) {
$payment_token_paypal->set_email( $email );
}
try {
$payment_token_paypal->save();
} catch ( Exception $exception ) {
$this->logger->error(
"Could not create WC payment token PayPal for customer {$customer_id}. " . $exception->getMessage()
);
}
}
}

View file

@ -0,0 +1,38 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'add-payment-method': path.resolve('./resources/js/add-payment-method.js')
},
output: {
path: path.resolve(__dirname, 'assets/'),
filename: 'js/[name].js',
},
module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
}
},
{loader:'sass-loader'}
]
}]
}
};

File diff suppressed because it is too large Load diff

View file

@ -10,12 +10,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavedPaymentChecker;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
$insert_after = function( array $array, string $key, array $new ): array {

View file

@ -16,7 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;

View file

@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\SavedPaymentChecker;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
@ -43,7 +43,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_filter(
'woocommerce_paypal_payments_order_intent',
function( string $intent ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' );
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
if ( $subscription_helper->cart_contains_subscription() || $subscription_helper->current_product_is_subscription() ) {
@ -60,7 +60,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_action(
'woocommerce_paypal_payments_before_handle_payment_success',
function( WC_Order $wc_order ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' );
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
if ( $subscription_helper->has_subscription( $wc_order->get_id() ) ) {
@ -93,7 +93,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_action(
'woocommerce_email_before_order_table',
function( WC_Order $order ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' );
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
@ -119,7 +119,7 @@ class SavedPaymentCheckerModule implements ModuleInterface {
add_action(
'woocommerce_email_after_order_table',
function( WC_Order $order ) use ( $c ) {
$subscription_helper = $c->get( 'subscription.helper' );
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );

View file

@ -9,7 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\StatusReport;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
@ -65,7 +65,7 @@ class StatusReportModule implements ModuleInterface {
$messages_apply = $c->get( 'button.helper.messages-apply' );
/* @var SubscriptionHelper $subscription_helper The subscription helper class. */
$subscription_helper = $c->get( 'subscription.helper' );
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
$last_webhook_storage = $c->get( 'webhook.last-webhook-storage' );
assert( $last_webhook_storage instanceof WebhookEventStorage );

View file

@ -1,74 +0,0 @@
<?php
/**
* The services
*
* @package WooCommerce\PayPalCommerce\Subscription
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
return array(
'subscription.helper' => static function ( ContainerInterface $container ): SubscriptionHelper {
return new SubscriptionHelper( $container->get( 'wcgateway.settings' ) );
},
'subscription.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$repository = $container->get( 'vaulting.repository.payment-token' );
$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' );
$settings = $container->get( 'wcgateway.settings' );
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
return new RenewalHandler(
$logger,
$repository,
$endpoint,
$purchase_unit_factory,
$container->get( 'api.factory.shipping-preference' ),
$payer_factory,
$environment,
$settings,
$authorized_payments_processor
);
},
'subscription.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 );
},
'subscription.api-handler' => static function( ContainerInterface $container ): SubscriptionsApiHandler {
return new SubscriptionsApiHandler(
$container->get( 'api.endpoint.catalog-products' ),
$container->get( 'api.factory.product' ),
$container->get( 'api.endpoint.billing-plans' ),
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' ),
$container->get( 'api.shop.currency' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'subscription.module.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-subscription/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'subscription.deactivate-plan-endpoint' => static function ( ContainerInterface $container ): DeactivatePlanEndpoint {
return new DeactivatePlanEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.billing-plans' )
);
},
);

View file

@ -31,7 +31,7 @@ return array(
},
'vaulting.credit-card-handler' => function( ContainerInterface $container ): VaultedCreditCardHandler {
return new VaultedCreditCardHandler(
$container->get( 'subscription.helper' ),
$container->get( 'wc-subscriptions.helper' ),
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.factory.payer' ),

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