Merge pull request #1779 from woocommerce/PCP-1393-update-to-vault-v-3

Save payment methods (Vault v3) integration (1393)
This commit is contained in:
Emili Castells 2023-12-05 11:23:09 +01:00 committed by GitHub
commit 45516d0075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 5204 additions and 1181 deletions

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,39 +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.paypal-website-url' => function( ContainerInterface $container ) : 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 {
'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 {
'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' );
@ -122,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' ),
@ -133,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' ),
@ -146,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' ),
@ -156,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' ),
@ -164,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' );
@ -176,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' );
@ -189,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(
@ -198,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' );
@ -212,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' ),
@ -227,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' ),
@ -242,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' ),
@ -251,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(
@ -306,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,
@ -314,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' );
@ -334,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,
@ -367,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' ),
@ -455,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;
@ -468,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' ),
@ -488,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(
@ -546,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',
@ -579,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.
*/
@ -901,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.
*/
@ -1070,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',
@ -1102,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 );
@ -1124,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

@ -27,7 +27,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;
@ -178,8 +178,10 @@ class OrderEndpoint {
* @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 $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.
*
* @return Order
* @throws RuntimeException If the request fails.
@ -190,7 +192,9 @@ 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()
): Order {
$bearer = $this->bearer->bearer();
$data = array(
@ -221,7 +225,7 @@ class OrderEndpoint {
/**
* 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 +264,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 stdClass
*/
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 stdClass $properties Payment source properties.
*/
public function __construct(
PaymentSourceCard $card = null,
PaymentSourceWallet $wallet = null
) {
$this->card = $card;
$this->wallet = $wallet;
public function __construct( string $name, stdClass $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 stdClass
*/
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(): stdClass {
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

@ -56,6 +56,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 +74,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

@ -64,6 +64,12 @@ export const loadPaypalScript = (config, onLoaded, onError = null) => {
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)

View file

@ -91,12 +91,30 @@ 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 !== '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);
})
});
});
}
@ -150,6 +168,9 @@ class CardFieldsRenderer {
return styles;
}
disableFields() {}
enableFields() {}
}
export default CardFieldsRenderer;

View file

@ -119,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' );
@ -294,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;
@ -303,7 +303,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 +644,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 );
}
/**
@ -1162,7 +1168,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 );
}
/**

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

@ -94,6 +94,10 @@ trait ContextTrait {
return 'checkout';
}
if ( $this->is_add_payment_method_page() ) {
return 'add-payment-method';
}
return 'mini-cart';
}
@ -125,6 +129,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 +146,7 @@ trait ContextTrait {
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
if ( $source && $source->name() === 'card' ) {
return false; // Ignore for DCC.
}
@ -147,4 +156,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

@ -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,33 @@
{
"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"
},
"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,74 @@
import {
getCurrentPaymentMethod,
ORDER_BUTTON_SELECTOR,
PaymentMethods
} from "../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState";
import {
setVisible,
setVisibleByClass
} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
import {loadPaypalJsScript} from "../../../ppcp-button/resources/js/modules/Helper/ScriptLoading";
loadPaypalJsScript(
{
clientId: ppcp_add_payment_method.client_id,
merchantId: ppcp_add_payment_method.merchant_id,
dataUserIdToken: ppcp_add_payment_method.id_token,
},
{
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
}
},
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()
console.log(result)
},
onError: (error) => {
console.error(error)
}
},
`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`
);
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()
});
}
);

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,120 @@
<?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;
/**
* 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 );
$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
);
}
wp_send_json_success( $result );
return true;
} catch ( Exception $exception ) {
wp_send_json_error();
return false;
}
}
}

View file

@ -0,0 +1,97 @@
<?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;
/**
* 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 {
$this->request_data->read_request( $this->nonce() );
/**
* Suppress ArgumentTypeCoercion
*
* @psalm-suppress ArgumentTypeCoercion
*/
$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' ) ),
),
)
);
$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,369 @@
<?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;
/**
* 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 );
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,
'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' ),

View file

@ -13,7 +13,7 @@ use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\AlreadyVaultedException;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
/**
* Class CustomerApprovalListener

View file

@ -19,8 +19,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
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\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;

View file

@ -51,7 +51,7 @@ class VaultingModule implements ModuleInterface {
$listener->listen();
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
add_action(
'woocommerce_created_customer',
function( int $customer_id ) use ( $subscription_helper ) {
@ -137,6 +137,8 @@ class VaultingModule implements ModuleInterface {
}
try {
do_action( 'woocommerce_paypal_payments_before_delete_payment_token', $token->get_token() );
$payment_token_endpoint = $container->get( 'api.endpoint.payment-token' );
$payment_token_endpoint->delete_token_by_id( $token->get_token() );
} catch ( RuntimeException $exception ) {
@ -191,7 +193,10 @@ class VaultingModule implements ModuleInterface {
'woocommerce_available_payment_gateways',
function( array $methods ): array {
global $wp;
if ( isset( $wp->query_vars['add-payment-method'] ) ) {
if (
isset( $wp->query_vars['add-payment-method'] )
&& apply_filters( 'woocommerce_paypal_payments_disable_add_payment_method', true )
) {
unset( $methods[ PayPalGateway::ID ] );
}

View file

@ -60,7 +60,6 @@ return array(
$source
);
},
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
$files = array(
'paypal-smart-button-fields.php',

View file

@ -21,7 +21,7 @@ use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
@ -76,7 +76,7 @@ return array(
$refund_processor = $container->get( 'wcgateway.processor.refunds' );
$state = $container->get( 'onboarding.state' );
$transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
$environment = $container->get( 'onboarding.environment' );
@ -111,7 +111,7 @@ return array(
$refund_processor = $container->get( 'wcgateway.processor.refunds' );
$state = $container->get( 'onboarding.state' );
$transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$payments_endpoint = $container->get( 'api.endpoint.payments' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$vaulted_credit_card_handler = $container->get( 'vaulting.credit-card-handler' );
@ -139,7 +139,7 @@ return array(
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'onboarding.state' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'subscription.helper' ),
$container->get( 'wc-subscriptions.helper' ),
$container->get( 'wcgateway.settings.allow_card_button_gateway.default' ),
$container->get( 'onboarding.environment' ),
$container->get( 'vaulting.repository.payment-token' ),
@ -152,7 +152,7 @@ return array(
$session_handler = $container->get( 'session.handler' );
$settings = $container->get( 'wcgateway.settings' );
$settings_status = $container->get( 'wcgateway.settings.status' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
return new DisableGateways( $session_handler, $settings, $settings_status, $subscription_helper );
},
@ -344,7 +344,7 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
$environment = $container->get( 'onboarding.environment' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$order_helper = $container->get( 'api.order-helper' );
return new OrderProcessor(
$session_handler,
@ -375,7 +375,7 @@ return array(
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$notice = $container->get( 'wcgateway.notice.authorize-order-action' );
$settings = $container->get( 'wcgateway.settings' );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
return new AuthorizedPaymentsProcessor(
$order_endpoint,
$payments_endpoint,
@ -455,7 +455,7 @@ return array(
$onboarding_options_renderer = $container->get( 'onboarding.render-options' );
assert( $onboarding_options_renderer instanceof OnboardingOptionsRenderer );
$subscription_helper = $container->get( 'subscription.helper' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
$fields = array(

View file

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;

View file

@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Checkout;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;

View file

@ -17,8 +17,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
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\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException;

View file

@ -11,14 +11,14 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Customer;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
@ -32,7 +32,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
*/
class CreditCardGateway extends \WC_Payment_Gateway_CC {
use ProcessPaymentTrait, GatewaySettingsRendererTrait;
use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait;
const ID = 'ppcp-credit-card-gateway';
@ -181,18 +181,25 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
);
if ( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) ) {
array_push(
$supports = apply_filters(
'woocommerce_paypal_payments_credit_card_gateway_vault_supports',
array(
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
)
);
$this->supports = array_merge(
$this->supports,
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions'
$supports
);
}
}
@ -358,6 +365,17 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
);
}
$saved_payment_card = WC()->session->get( 'ppcp_saved_payment_card' );
if ( $saved_payment_card ) {
if ( $saved_payment_card['payment_source'] === 'card' && $saved_payment_card['status'] === 'COMPLETED' ) {
$this->update_transaction_id( $saved_payment_card['order_id'], $wc_order );
$wc_order->payment_complete();
WC()->session->set( 'ppcp_saved_payment_card', null );
return $this->handle_payment_success( $wc_order );
}
}
/**
* If customer has chosen a saved credit card payment.
*/

View file

@ -20,8 +20,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
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\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException;

View file

@ -22,7 +22,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;

View file

@ -59,12 +59,7 @@ trait OrderMetaTrait {
private function get_payment_source( Order $order ): ?string {
$source = $order->payment_source();
if ( $source ) {
if ( $source->card() ) {
return 'card';
}
if ( $source->wallet() ) {
return 'wallet';
}
return $source->name();
}
return null;

View file

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
@ -25,7 +26,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -265,6 +266,8 @@ class OrderProcessor {
if ( $this->capture_authorized_downloads( $order ) ) {
$this->authorized_payments_processor->capture_authorized_payment( $wc_order );
}
do_action( 'woocommerce_paypal_payments_after_order_processor', $wc_order, $order );
}
/**
@ -349,12 +352,16 @@ class OrderProcessor {
* @return bool
*/
private function order_is_ready_for_process( Order $order ): bool {
if ( $order->status()->is( OrderStatus::APPROVED ) || $order->status()->is( OrderStatus::CREATED ) ) {
return true;
}
if ( ! $order->payment_source() || ! $order->payment_source()->card() ) {
$payment_source = $order->payment_source();
if ( ! $payment_source ) {
return false;
}
if ( $payment_source->name() !== 'card' ) {
return false;
}

View file

@ -13,7 +13,7 @@ use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
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 WC_Order;
@ -177,7 +177,7 @@ class WCGatewayModule implements ModuleInterface {
$assets = new SettingsPageAssets(
$c->get( 'wcgateway.url' ),
$c->get( 'ppcp.asset-version' ),
$c->get( 'subscription.helper' ),
$c->get( 'wc-subscriptions.helper' ),
$c->get( 'button.client_id_for_admin' ),
$c->get( 'api.shop.currency' ),
$c->get( 'api.shop.country' ),

View file

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

View file

@ -2,7 +2,7 @@
/**
* The extensions.
*
* @package WooCommerce\PayPalCommerce\Subscription
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);

View file

@ -2,15 +2,15 @@
/**
* The module.
*
* @package WooCommerce\PayPalCommerce\Subscription
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new SubscriptionModule();
return new WcSubscriptionsModule();
};

View file

@ -0,0 +1,48 @@
<?php
/**
* The services
*
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WooCommerce\PayPalCommerce\PayPalSubscriptions\DeactivatePlanEndpoint;
use WooCommerce\PayPalCommerce\PayPalSubscriptions\SubscriptionsApiHandler;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
return array(
'wc-subscriptions.helper' => static function ( ContainerInterface $container ): SubscriptionHelper {
return new SubscriptionHelper( $container->get( 'wcgateway.settings' ) );
},
'wc-subscriptions.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
);
},
'wc-subscriptions.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 );
},
);

View file

@ -2,12 +2,12 @@
/**
* Helper trait for the subscriptions handling.
*
* @package WooCommerce\PayPalCommerce\Subscription
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WC_Order;
use WC_Subscriptions_Product;

View file

@ -4,12 +4,12 @@
* whether the cart contains a subscription, the current product is
* a subscription or the subscription plugin is activated in the first place.
*
* @package WooCommerce\PayPalCommerce\Subscription\Helper
* @package WooCommerce\PayPalCommerce\WcSubscriptions\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription\Helper;
namespace WooCommerce\PayPalCommerce\WcSubscriptions\Helper;
use WC_Product;
use WC_Product_Subscription_Variation;

View file

@ -2,12 +2,12 @@
/**
* Handles subscription renewals.
*
* @package WooCommerce\PayPalCommerce\Subscription
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WC_Subscription;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
@ -181,13 +181,8 @@ class RenewalHandler {
* @throws \Exception If customer cannot be read/found.
*/
private function process_order( \WC_Order $wc_order ): void {
$user_id = (int) $wc_order->get_customer_id();
$customer = new \WC_Customer( $user_id );
$token = $this->get_token_for_customer( $customer, $wc_order );
if ( ! $token ) {
return;
}
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$payer = $this->payer_factory->from_customer( $customer );
@ -196,36 +191,38 @@ class RenewalHandler {
'renewal'
);
$token = $this->get_token_for_customer( $customer, $wc_order );
if ( $token ) {
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
$payer,
$token
);
$this->handle_paypal_order( $wc_order, $order );
$this->logger->info(
sprintf(
'Renewal for order %d is completed.',
$wc_order->get_id()
)
);
return;
}
if ( apply_filters( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', true ) ) {
return;
}
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
$payer,
$token
$payer
);
$this->add_paypal_meta( $wc_order, $order, $this->environment );
if ( $order->intent() === 'AUTHORIZE' ) {
$order = $this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' );
}
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$this->update_transaction_id( $transaction_id, $wc_order );
$subscriptions = wcs_get_subscriptions_for_order( $wc_order->get_id(), array( 'order_type' => 'any' ) );
foreach ( $subscriptions as $id => $subscription ) {
$subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id );
$subscription->save();
}
}
$this->handle_new_order_status( $order, $wc_order );
if ( $this->capture_authorized_downloads( $order ) ) {
$this->authorized_payments_processor->capture_authorized_payment( $wc_order );
}
$this->handle_paypal_order( $wc_order, $order );
$this->logger->info(
sprintf(
@ -317,4 +314,38 @@ class RenewalHandler {
}
return true;
}
/**
* Handles PayPal order creation and updates WC order accordingly.
*
* @param \WC_Order $wc_order WC order.
* @param Order $order PayPal order.
* @return void
* @throws NotFoundException When something goes wrong while handling the order.
*/
private function handle_paypal_order( \WC_Order $wc_order, Order $order ): void {
$this->add_paypal_meta( $wc_order, $order, $this->environment );
if ( $order->intent() === 'AUTHORIZE' ) {
$order = $this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' );
}
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$this->update_transaction_id( $transaction_id, $wc_order );
$subscriptions = wcs_get_subscriptions_for_order( $wc_order->get_id(), array( 'order_type' => 'any' ) );
foreach ( $subscriptions as $id => $subscription ) {
$subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id );
$subscription->save();
}
}
$this->handle_new_order_status( $order, $wc_order );
if ( $this->capture_authorized_downloads( $order ) ) {
$this->authorized_payments_processor->capture_authorized_payment( $wc_order );
}
}
}

View file

@ -2,12 +2,12 @@
/**
* Helper trait for the free trial subscriptions handling.
*
* @package WooCommerce\PayPalCommerce\Subscription
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WC_Subscriptions;

View file

@ -0,0 +1,353 @@
<?php
/**
* The subscription module.
*
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Subscription;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
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\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Class SubscriptionModule
*/
class WcSubscriptionsModule implements ModuleInterface {
use TransactionIdHandlingTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
add_action(
'woocommerce_scheduled_subscription_payment_' . PayPalGateway::ID,
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $amount, $order ) use ( $c ) {
$this->renew( $order, $c );
},
10,
2
);
add_action(
'woocommerce_scheduled_subscription_payment_' . CreditCardGateway::ID,
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $amount, $order ) use ( $c ) {
$this->renew( $order, $c );
},
10,
2
);
add_action(
'woocommerce_subscription_payment_complete',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $subscription ) use ( $c ) {
$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',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $description, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'wc-subscriptions.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',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $default_fields, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'wc-subscriptions.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;
}
);
}
/**
* 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 $container The container.
* @return void
*/
protected function renew( WC_Order $order, ContainerInterface $container ) {
$handler = $container->get( 'wc-subscriptions.renewal-handler' );
assert( $handler instanceof RenewalHandler );
$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
): void {
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;
}
}

View file

@ -71,7 +71,6 @@ class BillingPlanPricingChangeActivated implements RequestHandler {
$plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) );
if ( $plan_id && ! empty( $request['resource']['billing_cycles'] ) ) {
$this->logger->info( 'Starting stuff...' );
$args = array(
// phpcs:ignore WordPress.DB.SlowDBQuery
'meta_key' => 'ppcp_subscription_plan',

View file

@ -14,7 +14,8 @@
"install:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn install",
"install:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn install",
"install:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn install",
"install:modules:ppcp-subscription": "cd modules/ppcp-subscription && yarn install",
"install:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn install",
"install:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn install",
"install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
"install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install",
@ -25,7 +26,8 @@
"build:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run build",
"build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build",
"build:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run build",
"build:modules:ppcp-subscription": "cd modules/ppcp-subscription && yarn run build",
"build:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run build",
"build:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run build",
"build:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run build",
"build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build",
"build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build",
@ -37,7 +39,8 @@
"watch:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run watch",
"watch:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run watch",
"watch:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run watch",
"watch:modules:ppcp-subscription": "cd modules/ppcp-subscription && yarn run watch",
"watch:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run watch",
"watch:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run watch",
"watch:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run watch",
"watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch",
"watch:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run watch",

View file

@ -26,7 +26,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\ErrorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use Mockery;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use function Brain\Monkey\Functions\expect;
@ -1149,6 +1149,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$logger->shouldReceive('warning');
$applicationContext = Mockery::mock(ApplicationContext::class);
$applicationContext
->expects('to_array')

View file

@ -34,9 +34,6 @@ class OrderTest extends TestCase
->expects('to_array')
->andReturn(['applicationContext']);
$paymentSource = Mockery::mock(PaymentSource::class);
$paymentSource
->expects('to_array')
->andReturn(['paymentSource']);
$testee = new Order(
$id,
@ -68,8 +65,7 @@ class OrderTest extends TestCase
'create_time' => $createTime->format($this->dateFormat),
'update_time' => $updateTime->format($this->dateFormat),
'payer' => ['payer'],
'application_context' => ['applicationContext'],
'payment_source' => ['paymentSource']
'application_context' => ['applicationContext']
];
$this->assertEquals($expected, $testee->to_array());
}

View file

@ -3,15 +3,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSourceCard;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery\Mock;
use function Brain\Monkey\Functions\when;
class ThreeDSecureTest extends TestCase
{
@ -23,21 +20,35 @@ class ThreeDSecureTest extends TestCase
*/
public function testDefault(int $expected, string $liabilityShift, string $authenticationResult, string $enrollment)
{
$result = \Mockery::mock(CardAuthenticationResult::class);
$result->shouldReceive('liability_shift')->andReturn($liabilityShift);
$result->shouldReceive('authentication_result')->andReturn($authenticationResult);
$result->shouldReceive('enrollment_status')->andReturn($enrollment);
$result->shouldReceive('to_array')->andReturn(['foo' => 'bar',]);
$card = \Mockery::mock(PaymentSourceCard::class);
$card->shouldReceive('authentication_result')->andReturn($result);
$source = \Mockery::mock(PaymentSource::class);
$source->shouldReceive('card')->andReturn($card);
$order = \Mockery::mock(Order::class);
$order->shouldReceive('payment_source')->andReturn($source);
$logger = \Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('info');
$authResult = \Mockery::mock(CardAuthenticationResult::class);
$authResult->shouldReceive('liability_shift')->andReturn($liabilityShift);
$authResult->shouldReceive('authentication_result')->andReturn($authenticationResult);
$authResult->shouldReceive('enrollment_status')->andReturn($enrollment);
$authResult->shouldReceive('to_array')->andReturn(['foo' => 'bar',]);
$testee = new ThreeDSecure($logger);
$authenticationResultFactory = \Mockery::mock(CardAuthenticationResultFactory::class);
$authenticationResultFactory->shouldReceive('from_paypal_response')
->andReturn($authResult);
$source = \Mockery::mock(PaymentSource::class);
$authentication_result = (object)[
'brand' => 'visa',
'authentication_result' => (object)array(
'liability_shift' => $liabilityShift,
'authentication_result' => $authenticationResult,
'enrollment_status' => $enrollment
),
];
$source->shouldReceive('properties')->andReturn($authentication_result);
$order = \Mockery::mock(Order::class);
$order->shouldReceive('payment_source')->andReturn($source);
$logger = \Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('info');
$testee = new ThreeDSecure($authenticationResultFactory, $logger);
$result = $testee->proceed_with_order($order);
$this->assertEquals($expected, $result);
}

View file

@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler;
@ -118,10 +118,11 @@ class VaultedCreditCardHandlerTest extends TestCase
$order = Mockery::mock(Order::class);
$order->shouldReceive('id')->andReturn('1');
$order->shouldReceive('intent')->andReturn('CAPTURE');
$paymentSource = Mockery::mock(PaymentSource::class);
$paymentSourceCard = Mockery::mock(PaymentSourceCard::class);
$paymentSource->shouldReceive('card')->andReturn($paymentSourceCard);
$paymentSource->shouldReceive('name')->andReturn('card');
$order->shouldReceive('payment_source')->andReturn($paymentSource);
$orderStatus = Mockery::mock(OrderStatus::class);
$orderStatus->shouldReceive('is')->andReturn(true);
$order->shouldReceive('status')->andReturn($orderStatus);

View file

@ -4,7 +4,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\when;
use Mockery;

View file

@ -10,7 +10,7 @@ use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
@ -83,6 +83,7 @@ class CreditCardGatewayTest extends TestCase
when('WC')->justReturn($woocommerce);
$woocommerce->session = $session;
$session->shouldReceive('set')->andReturn([]);
$session->shouldReceive('get')->andReturn('');
$this->orderProcessor->shouldReceive('process')
->with($wc_order)
@ -106,6 +107,7 @@ class CreditCardGatewayTest extends TestCase
when('WC')->justReturn($woocommerce);
$woocommerce->session = $session;
$session->shouldReceive('set')->andReturn([]);
$session->shouldReceive('get')->andReturn('');
$savedCreditCard = 'abc123';
$_POST['saved_credit_card'] = $savedCreditCard;

View file

@ -11,7 +11,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;

View file

@ -19,7 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use Mockery;

View file

@ -3,7 +3,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
@ -24,7 +23,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;

View file

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
namespace WooCommerce\PayPalCommerce\WcSubscriptions;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Dictionary;
use Exception;

View file

@ -0,0 +1,42 @@
const {test, expect} = require('@playwright/test');
const {loginAsCustomer} = require("./utils/user");
const {openPaypalPopup, loginIntoPaypal, completePaypalPayment} = require("./utils/paypal-popup");
const {fillCheckoutForm, expectOrderReceivedPage} = require("./utils/checkout");
const {
PRODUCT_URL,
} = process.env;
async function expectContinuation(page) {
await expect(page.locator('#payment_method_ppcp-gateway')).toBeChecked();
await expect(page.locator('.component-frame')).toHaveCount(0);
}
async function completeContinuation(page) {
await expectContinuation(page);
await Promise.all([
page.waitForNavigation(),
page.locator('#place_order').click(),
]);
}
test('Save during purchase', async ({page}) => {
await loginAsCustomer(page)
await page.goto(PRODUCT_URL);
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await completePaypalPayment(popup);
await fillCheckoutForm(page);
await completeContinuation(page);
await expectOrderReceivedPage(page);
});