Merge pull request #1443 from woocommerce/PCP-991-v2-detach-vaulting-from-wc-subscriptions-support

PayPal Subscriptions API fixes and improvements (991)
This commit is contained in:
Emili Castells 2023-08-10 11:29:03 +02:00 committed by GitHub
commit 402b87face
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 3272 additions and 156 deletions

View file

@ -2084,3 +2084,26 @@ function wcs_find_matching_line_item($order, $subscription_item, $match_type = '
function wcs_order_contains_product($order, $product)
{
}
/**
* Get page ID for a specific WC resource.
*
* @param string $for Name of the resource.
*
* @return string Page ID. Empty string if resource not found.
*/
function wc_get_page_screen_id( $for ) {}
/**
* Subscription Product Variation Class
*
* The subscription product variation class extends the WC_Product_Variation product class
* to create subscription product variations.
*
* @class WC_Product_Subscription
* @package WooCommerce Subscriptions
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.3
*
*/
class WC_Product_Subscription_Variation extends WC_Product_Variation {}

View file

@ -226,4 +226,40 @@ class BillingPlans {
);
}
}
/**
* Deactivates a Subscription Plan.
*
* @param string $billing_plan_id The Plan ID.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function deactivate_plan( string $billing_plan_id ) {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $billing_plan_id . '/deactivate';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Could not deactivate plan.' );
}
$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

@ -214,4 +214,6 @@ class BillingSubscriptions {
return $json;
}
}

View file

@ -21,11 +21,11 @@ class SingleProductActionHandler {
this.cartHelper = null;
}
subscriptionsConfiguration() {
subscriptionsConfiguration(subscription_plan) {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
'plan_id': subscription_plan
});
},
onApprove: (data, actions) => {
@ -73,7 +73,7 @@ class SingleProductActionHandler {
getSubscriptionProducts()
{
const id = document.querySelector('[name="add-to-cart"]').value;
return [new Product(id, 1, null)];
return [new Product(id, 1, this.variations())];
}
configuration()

View file

@ -86,6 +86,12 @@ class CartBootstrap {
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration());
if(!PayPalCommerceGateway.subscription_product_allowed) {
this.gateway.button.is_disabled = true;
this.handleButtonStatus();
}
return;
}

View file

@ -106,6 +106,12 @@ class CheckoutBootstap {
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration(), {}, actionHandler.configuration());
if(!PayPalCommerceGateway.subscription_product_allowed) {
this.gateway.button.is_disabled = true;
this.handleButtonStatus();
}
return;
}

View file

@ -2,6 +2,8 @@ import UpdateCart from "../Helper/UpdateCart";
import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler";
import {hide, show} from "../Helper/Hiding";
import BootstrapHelper from "../Helper/BootstrapHelper";
import {loadPaypalJsScript} from "../Helper/ScriptLoading";
import {getPlanIdFromVariation} from "../Helper/Subscriptions"
import SimulateCart from "../Helper/SimulateCart";
import {strRemoveWord, strAddWord, throttle} from "../Helper/Utils";
@ -20,6 +22,8 @@ class SingleProductBootstap {
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
this.handleChange();
}, true);
this.subscriptionButtonsLoaded = false
}
form() {
@ -27,6 +31,8 @@ class SingleProductBootstap {
}
handleChange() {
this.subscriptionButtonsLoaded = false
if (!this.shouldRender()) {
this.renderer.disableSmartButtons(this.gateway.button.wrapper);
hide(this.gateway.button.wrapper, this.formSelector);
@ -141,6 +147,25 @@ class SingleProductBootstap {
|| document.querySelector('.wcsatt-options-prompt-label-subscription input[type="radio"]:checked') !== null; // grouped
}
variations() {
if (!this.hasVariations()) {
return null;
}
return [...document.querySelector('form.cart')?.querySelectorAll("[name^='attribute_']")].map(
(element) => {
return {
value: element.value,
name: element.name
}
}
);
}
hasVariations() {
return document.querySelector('form.cart')?.classList.contains('variations_form');
}
render() {
const actionHandler = new SingleProductActionHandler(
this.gateway,
@ -156,7 +181,29 @@ class SingleProductBootstap {
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration());
const buttonWrapper = document.getElementById('ppc-button-ppcp-gateway');
buttonWrapper.innerHTML = '';
const subscription_plan = this.variations() !== null
? getPlanIdFromVariation(this.variations())
: PayPalCommerceGateway.subscription_plan_id
if(!subscription_plan) {
return;
}
if(this.subscriptionButtonsLoaded) return
loadPaypalJsScript(
{
clientId: PayPalCommerceGateway.client_id,
currency: PayPalCommerceGateway.currency,
intent: 'subscription',
vault: true
},
actionHandler.subscriptionsConfiguration(subscription_plan),
this.gateway.button.wrapper
);
this.subscriptionButtonsLoaded = true
return;
}

View file

@ -25,3 +25,9 @@ export const loadPaypalScript = (config, onLoaded) => {
loadScript(scriptOptions).then(callback);
}
export const loadPaypalJsScript = (options, buttons, container) => {
loadScript(options).then((paypal) => {
paypal.Buttons(buttons).render(container);
});
}

View file

@ -2,3 +2,19 @@ export const isChangePaymentPage = () => {
const urlParams = new URLSearchParams(window.location.search)
return urlParams.has('change_payment_method');
}
export const getPlanIdFromVariation = (variation) => {
let subscription_plan = '';
PayPalCommerceGateway.variable_paypal_subscription_variations.forEach((element) => {
let obj = {};
variation.forEach(({name, value}) => {
Object.assign(obj, {[name.replace('attribute_', '')]: value});
})
if(JSON.stringify(obj) === JSON.stringify(element.attributes) && element.subscription_plan !== '') {
subscription_plan = element.subscription_plan;
}
});
return subscription_plan;
}

View file

@ -16,7 +16,6 @@ use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;

View file

@ -817,10 +817,12 @@ class SmartButton implements SmartButtonInterface {
$this->request_data->enqueue_nonce_fix();
$localize = array(
'url' => add_query_arg( $url_params, 'https://www.paypal.com/sdk/js' ),
'url_params' => $url_params,
'script_attributes' => $this->attributes(),
'data_client_id' => array(
'url' => add_query_arg( $url_params, 'https://www.paypal.com/sdk/js' ),
'url_params' => $url_params,
'script_attributes' => $this->attributes(),
'client_id' => $this->client_id,
'currency' => $this->currency,
'data_client_id' => array(
'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(),
'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ),
@ -828,9 +830,9 @@ class SmartButton implements SmartButtonInterface {
'has_subscriptions' => $this->has_subscriptions(),
'paypal_subscriptions_enabled' => $this->paypal_subscriptions_enabled(),
),
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'simulate_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ),
@ -867,14 +869,16 @@ class SmartButton implements SmartButtonInterface {
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
),
),
'subscription_plan_id' => $this->paypal_subscription_id(),
'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(),
'is_free_trial_cart' => $is_free_trial_cart,
'vaulted_paypal_email' => ( is_checkout() && $is_free_trial_cart ) ? $this->get_vaulted_paypal_email() : '',
'bn_codes' => $this->bn_codes(),
'payer' => $this->payerData(),
'button' => array(
'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(),
'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(),
'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(),
'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(),
'is_free_trial_cart' => $is_free_trial_cart,
'vaulted_paypal_email' => ( is_checkout() && $is_free_trial_cart ) ? $this->get_vaulted_paypal_email() : '',
'bn_codes' => $this->bn_codes(),
'payer' => $this->payerData(),
'button' => array(
'wrapper' => '#ppc-button-' . PayPalGateway::ID,
'is_disabled' => $this->is_button_disabled(),
'mini_cart_wrapper' => '#ppc-button-minicart',
@ -896,7 +900,7 @@ class SmartButton implements SmartButtonInterface {
'tagline' => $this->style_for_context( 'tagline', $this->context() ),
),
),
'separate_buttons' => array(
'separate_buttons' => array(
'card' => array(
'id' => CardButtonGateway::ID,
'wrapper' => '#ppc-button-' . CardButtonGateway::ID,
@ -906,7 +910,7 @@ class SmartButton implements SmartButtonInterface {
),
),
),
'hosted_fields' => array(
'hosted_fields' => array(
'wrapper' => '#ppcp-hosted-fields',
'labels' => array(
'credit_card_number' => '',
@ -929,8 +933,8 @@ class SmartButton implements SmartButtonInterface {
'valid_cards' => $this->dcc_applies->valid_cards(),
'contingency' => $this->get_3ds_contingency(),
),
'messages' => $this->message_values(),
'labels' => array(
'messages' => $this->message_values(),
'labels' => array(
'error' => array(
'generic' => __(
'Something went wrong. Please try again or choose another payment source.',
@ -957,12 +961,12 @@ class SmartButton implements SmartButtonInterface {
// phpcs:ignore WordPress.WP.I18n
'shipping_field' => _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ),
),
'order_id' => 'pay-now' === $this->context() ? $this->get_order_pay_id() : 0,
'single_product_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'product' ),
'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ),
'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled,
'early_checkout_validation_enabled' => $this->early_validation_enabled,
'funding_sources_without_redirect' => $this->funding_sources_without_redirect,
'order_id' => 'pay-now' === $this->context() ? $this->get_order_pay_id() : 0,
'single_product_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'product' ),
'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ),
'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled,
'early_checkout_validation_enabled' => $this->early_validation_enabled,
'funding_sources_without_redirect' => $this->funding_sources_without_redirect,
);
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
@ -1528,38 +1532,6 @@ class SmartButton implements SmartButtonInterface {
return false;
}
/**
* Returns PayPal subscription plan id from WC subscription product.
*
* @return string
*/
private function paypal_subscription_id(): string {
if ( $this->subscription_helper->current_product_is_subscription() ) {
$product = wc_get_product();
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
$cart = WC()->cart ?? null;
if ( ! $cart || $cart->is_empty() ) {
return '';
}
$items = $cart->get_cart_contents();
foreach ( $items as $item ) {
$product = wc_get_product( $item['product_id'] );
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
return '';
}
/**
* Returns the intent.
*

View file

@ -3,7 +3,7 @@ document.addEventListener(
() => {
const config = PayPalCommerceGatewayOrderTrackingInfo;
if (!typeof (PayPalCommerceGatewayOrderTrackingInfo)) {
console.error('trackign cannot be set.');
console.error('tracking cannot be set.');
return;
}

View file

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

View file

@ -0,0 +1,31 @@
{
"name": "ppcp-subscription",
"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-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,102 @@
document.addEventListener(
'DOMContentLoaded',
() => {
const variations = document.querySelector('.woocommerce_variations');
const disableFields = (productId) => {
if(variations) {
const children = variations.children;
for(let i=0; i < children.length; i++) {
const variableId = children[i].querySelector('h3').getElementsByClassName('variable_post_id')[0].value
if (parseInt(variableId) === productId) {
children[i].querySelector('.woocommerce_variable_attributes')
.getElementsByClassName('wc_input_subscription_period_interval')[0]
.setAttribute('disabled', 'disabled');
children[i].querySelector('.woocommerce_variable_attributes')
.getElementsByClassName('wc_input_subscription_period')[0]
.setAttribute('disabled', 'disabled');
children[i].querySelector('.woocommerce_variable_attributes')
.getElementsByClassName('wc_input_subscription_trial_length')[0]
.setAttribute('disabled', 'disabled');
children[i].querySelector('.woocommerce_variable_attributes')
.getElementsByClassName('wc_input_subscription_trial_period')[0]
.setAttribute('disabled', 'disabled');
children[i].querySelector('.woocommerce_variable_attributes')
.getElementsByClassName('wc_input_subscription_length')[0]
.setAttribute('disabled', 'disabled');
}
}
}
const periodInterval = document.querySelector('#_subscription_period_interval');
periodInterval.setAttribute('disabled', 'disabled');
const subscriptionPeriod = document.querySelector('#_subscription_period');
subscriptionPeriod.setAttribute('disabled', 'disabled');
const subscriptionLength = document.querySelector('._subscription_length_field');
subscriptionLength.style.display = 'none';
const subscriptionTrial = document.querySelector('._subscription_trial_length_field');
subscriptionTrial.style.display = 'none';
}
const setupProducts = () => {
PayPalCommerceGatewayPayPalSubscriptionProducts?.forEach((product) => {
if(product.product_connected === 'yes') {
disableFields(product.product_id);
}
const unlinkBtn = document.getElementById(`ppcp-unlink-sub-plan-${product.product_id}`);
unlinkBtn?.addEventListener('click', (event)=>{
event.preventDefault();
unlinkBtn.disabled = true;
const spinner = document.getElementById('spinner-unlink-plan');
spinner.style.display = 'inline-block';
fetch(product.ajax.deactivate_plan.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: product.ajax.deactivate_plan.nonce,
plan_id: product.plan_id,
product_id: product.product_id
})
}).then(function (res) {
return res.json();
}).then(function (data) {
if (!data.success) {
unlinkBtn.disabled = false;
spinner.style.display = 'none';
console.error(data);
throw Error(data.data.message);
}
const enableSubscription = document.getElementById('ppcp-enable-subscription');
const product = document.getElementById('pcpp-product');
const plan = document.getElementById('pcpp-plan');
enableSubscription.style.display = 'none';
product.style.display = 'none';
plan.style.display = 'none';
const enable_subscription_product = document.getElementById('ppcp_enable_subscription_product');
enable_subscription_product.disabled = true;
const planUnlinked = document.getElementById('pcpp-plan-unlinked');
planUnlinked.style.display = 'block';
setTimeout(() => {
location.reload();
}, 1000)
});
});
})
}
setupProducts();
jQuery( '#woocommerce-product-data' ).on('woocommerce_variations_loaded', () => {
setupProducts();
});
});

View file

@ -15,7 +15,7 @@ use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
return array(
'subscription.helper' => static function ( ContainerInterface $container ): SubscriptionHelper {
return new SubscriptionHelper();
return new SubscriptionHelper( $container->get( 'wcgateway.settings' ) );
},
'subscription.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
@ -54,4 +54,21 @@ return array(
$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

@ -0,0 +1,86 @@
<?php
/**
* The deactivate Subscription Plan Endpoint.
*
* @package WooCommerce\PayPalCommerce\OrderTracking\Endpoint
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\Subscription;
use Exception;
use WC_Product;
use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* Class DeactivatePlanEndpoint
*/
class DeactivatePlanEndpoint {
const ENDPOINT = 'ppc-deactivate-plan';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The billing plans.
*
* @var BillingPlans
*/
private $billing_plans;
/**
* DeactivatePlanEndpoint constructor.
*
* @param RequestData $request_data The request data.
* @param BillingPlans $billing_plans The billing plans.
*/
public function __construct( RequestData $request_data, BillingPlans $billing_plans ) {
$this->request_data = $request_data;
$this->billing_plans = $billing_plans;
}
/**
* Handles the request.
*
* @return void
*/
public function handle_request(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return;
}
try {
$data = $this->request_data->read_request( self::ENDPOINT );
$plan_id = $data['plan_id'] ?? '';
if ( $plan_id ) {
$this->billing_plans->deactivate_plan( $plan_id );
}
$product_id = $data['product_id'] ?? '';
if ( $product_id ) {
$product = wc_get_product( $product_id );
if ( is_a( $product, WC_Product::class ) && WC_Subscriptions_Product::is_subscription( $product ) ) {
$product->delete_meta_data( '_ppcp_enable_subscription_product' );
$product->delete_meta_data( '_ppcp_subscription_plan_name' );
$product->delete_meta_data( 'ppcp_subscription_product' );
$product->delete_meta_data( 'ppcp_subscription_plan' );
$product->save();
}
}
wp_send_json_success();
} catch ( Exception $error ) {
wp_send_json_error();
}
}
}

View file

@ -10,8 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WC_Order;
use WC_Product;
use WC_Subscription;
use WC_Subscriptions_Product;
use WC_Subscriptions_Synchroniser;
@ -58,7 +56,10 @@ trait FreeTrialHandlerTrait {
$product = wc_get_product();
if ( ! $product || ! WC_Subscriptions_Product::is_subscription( $product ) ) {
if (
! $product || ! WC_Subscriptions_Product::is_subscription( $product )
|| $product->get_meta( '_ppcp_enable_subscription_product' ) === 'yes'
) {
return false;
}

View file

@ -12,13 +12,32 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription\Helper;
use WC_Product;
use WC_Product_Subscription_Variation;
use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
/**
* Class SubscriptionHelper
*/
class SubscriptionHelper {
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* SubscriptionHelper constructor.
*
* @param Settings $settings The settings.
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* Whether the current product is a subscription.
*
@ -150,4 +169,109 @@ class SubscriptionHelper {
return false;
}
/**
* Checks if subscription product is allowed.
*
* @return bool
* @throws NotFoundException If setting is not found.
*/
public function checkout_subscription_product_allowed(): bool {
if (
! $this->paypal_subscription_id()
|| ! $this->cart_contains_only_one_item()
) {
return false;
}
return true;
}
/**
* Returns PayPal subscription plan id from WC subscription product.
*
* @return string
*/
public function paypal_subscription_id(): string {
if ( $this->current_product_is_subscription() ) {
$product = wc_get_product();
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
$cart = WC()->cart ?? null;
if ( ! $cart || $cart->is_empty() ) {
return '';
}
$items = $cart->get_cart_contents();
foreach ( $items as $item ) {
$product = wc_get_product( $item['product_id'] );
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
return '';
}
/**
* Returns variations for variable PayPal subscription product.
*
* @return array
*/
public function variable_paypal_subscription_variations(): array {
$variations = array();
if ( ! $this->current_product_is_subscription() ) {
return $variations;
}
$product = wc_get_product();
assert( $product instanceof WC_Product );
if ( $product->get_type() !== 'variable-subscription' ) {
return $variations;
}
$variation_ids = $product->get_children();
foreach ( $variation_ids as $id ) {
$product = wc_get_product( $id );
if ( ! is_a( $product, WC_Product_Subscription_Variation::class ) ) {
continue;
}
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array();
$variations[] = array(
'id' => $product->get_id(),
'attributes' => $product->get_attributes(),
'subscription_plan' => $subscription_plan['id'] ?? '',
);
}
return $variations;
}
/**
* Checks if cart contains only one item.
*
* @return bool
*/
public function cart_contains_only_one_item(): bool {
if ( ! $this->plugin_is_active() ) {
return false;
}
$cart = WC()->cart;
if ( ! $cart || $cart->is_empty() ) {
return false;
}
if ( count( $cart->get_cart() ) > 1 ) {
return false;
}
return true;
}
}

View file

@ -11,6 +11,10 @@ namespace WooCommerce\PayPalCommerce\Subscription;
use Exception;
use WC_Product;
use WC_Product_Subscription_Variation;
use WC_Product_Variable;
use WC_Product_Variable_Subscription;
use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
@ -28,6 +32,7 @@ use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WP_Post;
/**
* Class SubscriptionModule
@ -155,6 +160,109 @@ class SubscriptionModule implements ModuleInterface {
if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && PPCP_FLAG_SUBSCRIPTIONS_API ) {
$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 ) || 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();
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',
function( string $post_type, WP_Post $post ) use ( $c ) {
if ( $post_type !== 'shop_subscription' ) {
return;
}
$subscription = wcs_get_subscription( $post->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
);
}
/**
@ -362,40 +470,41 @@ class SubscriptionModule implements ModuleInterface {
return;
}
$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' && $enable_subscription_product === 'yes' ) {
$subscriptions_api_handler = $c->get( 'subscription.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
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' ) ) {
$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 );
}
}
$subscriptions_api_handler = $c->get( 'subscription.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
$this->update_subscription_product_meta( $product, $subscriptions_api_handler );
},
12
);
add_action(
'woocommerce_save_product_variation',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $variation_id ) use ( $c ) {
$wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) );
if (
! WC_Subscriptions_Product::is_subscription( $variation_id )
|| ! is_string( $wcsnonce_save_variations )
|| ! wp_verify_nonce( $wcsnonce_save_variations, 'wcs_subscription_variations' )
) {
return;
}
$product = wc_get_product( $variation_id );
if ( ! is_a( $product, WC_Product_Subscription_Variation::class ) ) {
return;
}
$subscriptions_api_handler = $c->get( 'subscription.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
$this->update_subscription_product_meta( $product, $subscriptions_api_handler );
}
);
add_action(
'woocommerce_process_shop_subscription_meta',
/**
@ -604,50 +713,6 @@ class SubscriptionModule implements ModuleInterface {
}
);
add_action(
'woocommerce_product_options_general_product_data',
function() use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
try {
$subscriptions_mode = $settings->get( 'subscriptions_mode' );
if ( $subscriptions_mode === 'subscriptions_api' ) {
/**
* Needed for getting global post object.
*
* @psalm-suppress InvalidGlobal
*/
global $post;
$product = wc_get_product( $post->ID );
if ( ! is_a( $product, WC_Product::class ) ) {
return;
}
$enable_subscription_product = $product->get_meta( '_ppcp_enable_subscription_product' );
$subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' );
echo '<div class="options_group subscription_pricing show_if_subscription hidden">';
echo '<p class="form-field"><label for="_ppcp_enable_subscription_product">Connect to PayPal</label><input type="checkbox" id="ppcp_enable_subscription_product" name="_ppcp_enable_subscription_product" value="yes" ' . checked( $enable_subscription_product, 'yes', false ) . '/><span class="description">Connect Product to PayPal Subscriptions Plan</span></p>';
$subscription_product = $product->get_meta( 'ppcp_subscription_product' );
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' );
if ( $subscription_product && $subscription_plan ) {
$environment = $c->get( 'onboarding.environment' );
$host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
echo '<p class="form-field"><label>Product</label><a href="' . esc_url( $host . '/billing/plans/products/' . $subscription_product['id'] ) . '" target="_blank">' . esc_attr( $subscription_product['id'] ) . '</a></p>';
echo '<p class="form-field"><label>Plan</label><a href="' . esc_url( $host . '/billing/plans/' . $subscription_plan['id'] ) . '" target="_blank">' . esc_attr( $subscription_plan['id'] ) . '</a></p>';
} else {
echo '<p class="form-field"><label for="_ppcp_subscription_plan_name">Plan Name</label><input type="text" class="short" id="ppcp_subscription_plan_name" name="_ppcp_subscription_plan_name" value="' . esc_attr( $subscription_plan_name ) . '"></p>';
}
echo '</div>';
}
} catch ( NotFoundException $exception ) {
return;
}
}
);
add_filter(
'woocommerce_order_data_store_cpt_get_orders_query',
/**
@ -724,5 +789,201 @@ class SubscriptionModule implements ModuleInterface {
}
}
);
add_action(
'woocommerce_product_options_general_product_data',
function() use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
try {
$subscriptions_mode = $settings->get( 'subscriptions_mode' );
if ( $subscriptions_mode === 'subscriptions_api' ) {
/**
* Needed for getting global post object.
*
* @psalm-suppress InvalidGlobal
*/
global $post;
$product = wc_get_product( $post->ID );
if ( ! is_a( $product, WC_Product::class ) ) {
return;
}
$environment = $c->get( 'onboarding.environment' );
echo '<div class="options_group subscription_pricing show_if_subscription hidden">';
$this->render_paypal_subscription_fields( $product, $environment );
echo '</div>';
}
} catch ( NotFoundException $exception ) {
return;
}
}
);
add_action(
'woocommerce_variation_options_pricing',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $loop, $variation_data, $variation ) use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
try {
$subscriptions_mode = $settings->get( 'subscriptions_mode' );
if ( $subscriptions_mode === 'subscriptions_api' ) {
$product = wc_get_product( $variation->ID );
if ( ! is_a( $product, WC_Product_Subscription_Variation::class ) ) {
return;
}
$environment = $c->get( 'onboarding.environment' );
$this->render_paypal_subscription_fields( $product, $environment );
}
} catch ( NotFoundException $exception ) {
return;
}
},
10,
3
);
}
/**
* Render PayPal Subscriptions fields.
*
* @param WC_Product $product WC Product.
* @param Environment $environment The environment.
* @return void
*/
private function render_paypal_subscription_fields( WC_Product $product, Environment $environment ): void {
$enable_subscription_product = $product->get_meta( '_ppcp_enable_subscription_product' );
$style = $product->get_type() === 'subscription_variation' ? 'float:left; width:150px;' : '';
echo '<p class="form-field">';
echo sprintf(
// translators: %1$s and %2$s are label open and close tags.
esc_html__( '%1$sConnect to PayPal%2$s', 'woocommerce-paypal-payments' ),
'<label for="_ppcp_enable_subscription_product" style="' . esc_attr( $style ) . '">',
'</label>'
);
echo '<input type="checkbox" id="ppcp_enable_subscription_product" name="_ppcp_enable_subscription_product" value="yes" ' . checked( $enable_subscription_product, 'yes', false ) . '/>';
echo sprintf(
// translators: %1$s and %2$s are label open and close tags.
esc_html__( '%1$sConnect Product to PayPal Subscriptions Plan%2$s', 'woocommerce-paypal-payments' ),
'<span class="description">',
'</span>'
);
echo wc_help_tip( esc_html__( 'Create a subscription product and plan to bill customers at regular intervals. Be aware that certain subscription settings cannot be modified once the PayPal Subscription is linked to this product. Unlink the product to edit disabled fields.', 'woocommerce-paypal-payments' ) );
echo '</p>';
$subscription_product = $product->get_meta( 'ppcp_subscription_product' );
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' );
$subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' );
if ( $subscription_product && $subscription_plan ) {
if ( $enable_subscription_product !== 'yes' ) {
echo sprintf(
// translators: %1$s and %2$s are button and wrapper html tags.
esc_html__( '%1$sUnlink PayPal Subscription Plan%2$s', 'woocommerce-paypal-payments' ),
'<p class="form-field" id="ppcp-enable-subscription"><label></label><button class="button" id="ppcp-unlink-sub-plan-' . esc_attr( (string) $product->get_id() ) . '">',
'</button><span class="spinner is-active" id="spinner-unlink-plan" style="float: none; display:none;"></span></p>'
);
echo sprintf(
// translators: %1$s and %2$s is open and closing paragraph tag.
esc_html__( '%1$sPlan unlinked successfully ✔️%2$s', 'woocommerce-paypal-payments' ),
'<p class="form-field" id="pcpp-plan-unlinked" style="display: none;">',
'</p>'
);
}
$host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
echo sprintf(
// translators: %1$s and %2$s are wrapper html tags.
esc_html__( '%1$sProduct%2$s', 'woocommerce-paypal-payments' ),
'<p class="form-field" id="pcpp-product"><label style="' . esc_attr( $style ) . '">',
'</label><a href="' . esc_url( $host . '/billing/plans/products/' . $subscription_product['id'] ) . '" target="_blank">' . esc_attr( $subscription_product['id'] ) . '</a></p>'
);
echo sprintf(
// translators: %1$s and %2$s are wrapper html tags.
esc_html__( '%1$sPlan%2$s', 'woocommerce-paypal-payments' ),
'<p class="form-field" id="pcpp-plan"><label style="' . esc_attr( $style ) . '">',
'</label><a href="' . esc_url( $host . '/billing/plans/' . $subscription_plan['id'] ) . '" target="_blank">' . esc_attr( $subscription_plan['id'] ) . '</a></p>'
);
} else {
echo sprintf(
// translators: %1$s and %2$s are wrapper html tags.
esc_html__( '%1$sPlan Name%2$s', 'woocommerce-paypal-payments' ),
'<p class="form-field"><label for="_ppcp_subscription_plan_name">',
'</label><input type="text" class="short" id="ppcp_subscription_plan_name" name="_ppcp_subscription_plan_name" value="' . esc_attr( $subscription_plan_name ) . '"></p>'
);
}
}
/**
* 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

@ -265,7 +265,7 @@ class SubscriptionsApiHandler {
'REGULAR',
array(
'fixed_price' => array(
'value' => $product->get_meta( '_subscription_price' ),
'value' => $product->get_meta( '_subscription_price' ) ?: $product->get_price(),
'currency_code' => $this->currency,
),
),

View file

@ -0,0 +1,35 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {
'paypal-subscription': path.resolve('./resources/js/paypal-subscription.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

@ -149,7 +149,8 @@ return array(
$session_handler = $container->get( 'session.handler' );
$settings = $container->get( 'wcgateway.settings' );
$settings_status = $container->get( 'wcgateway.settings.status' );
return new DisableGateways( $session_handler, $settings, $settings_status );
$subscription_helper = $container->get( 'subscription.helper' );
return new DisableGateways( $session_handler, $settings, $settings_status, $subscription_helper );
},
'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool {
@ -812,7 +813,14 @@ return array(
),
'paypal_saved_payments' => array(
'heading' => __( 'Saved payments', 'woocommerce-paypal-payments' ),
'description' => __( 'PayPal can save your customers payment methods.', 'woocommerce-paypal-payments' ),
'description' => sprintf(
// translators: %1$s, %2$s, %3$s and %4$s are a link tags.
__( 'PayPal can securely store your customers\' payment methods for %1$sfuture payments%2$s and %3$ssubscriptions%4$s, simplifying the checkout process and enabling recurring transactions on your website.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#vaulting-saving-a-payment-method" target="_blank">',
'</a>',
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq" target="_blank">',
'</a>'
),
'type' => 'ppcp-heading',
'screens' => array(
State::STATE_START,

View file

@ -11,6 +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\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -44,22 +45,32 @@ class DisableGateways {
*/
protected $settings_status;
/**
* The subscription helper.
*
* @var SubscriptionHelper
*/
private $subscription_helper;
/**
* DisableGateways constructor.
*
* @param SessionHandler $session_handler The Session Handler.
* @param ContainerInterface $settings The Settings.
* @param SettingsStatus $settings_status The Settings status helper.
* @param SubscriptionHelper $subscription_helper The subscription helper.
*/
public function __construct(
SessionHandler $session_handler,
ContainerInterface $settings,
SettingsStatus $settings_status
SettingsStatus $settings_status,
SubscriptionHelper $subscription_helper
) {
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->settings_status = $settings_status;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->settings_status = $settings_status;
$this->subscription_helper = $subscription_helper;
}
/**

View file

@ -12,6 +12,7 @@
"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-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",
@ -20,6 +21,7 @@
"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-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",
@ -29,6 +31,7 @@
"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-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

@ -1,7 +1,8 @@
{
"license": "MIT",
"dependencies": {
"@playwright/test": "^1.34.2",
"dotenv": "^16.0.3"
"@playwright/test": "^1.34.2",
"@woocommerce/woocommerce-rest-api": "^1.0.1",
"dotenv": "^16.0.3"
}
}

View file

@ -7,6 +7,7 @@ require('dotenv').config({ path: '.env' });
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
timeout: 60000,
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,

View file

@ -65,7 +65,7 @@ test.describe('Classic checkout', () => {
await expectOrderReceivedPage(page);
});
test('Advanced Credit and Debit Card (ACDC) place order from Checkout page', async ({page}) => {
test('Advanced Credit and Debit Card place order from Checkout page', async ({page}) => {
await page.goto(PRODUCT_URL);
await page.locator('.single_add_to_cart_button').click();

View file

@ -1,7 +1,9 @@
const {test, expect} = require('@playwright/test');
const {loginAsAdmin, loginAsCustomer} = require('./utils/user');
const {openPaypalPopup, loginIntoPaypal, completePaypalPayment} = require("./utils/paypal-popup");
const {fillCheckoutForm, expectOrderReceivedPage} = require("./utils/checkout");
const {createProduct, deleteProduct, updateProduct, updateProductUi} = require("./utils/products");
const {
AUTHORIZATION,
SUBSCRIPTION_URL,
@ -18,7 +20,7 @@ async function purchaseSubscriptionFromCart(page) {
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await popup.getByText('Continue', { exact: true }).click();
await popup.getByText('Continue', {exact: true}).click();
await popup.locator('#confirmButtonTop').click();
await fillCheckoutForm(page);
@ -92,7 +94,7 @@ test.describe.serial('Subscriptions Merchant', () => {
await loginAsAdmin(page);
await page.goto('/wp-admin/edit.php?post_type=product');
await page.getByRole('link', { name: productTitle, exact: true }).click();
await page.getByRole('link', {name: productTitle, exact: true}).click();
await page.fill('#title', `Updated ${productTitle}`);
await page.fill('#_subscription_price', '20');
@ -209,7 +211,7 @@ test.describe('Subscriber purchase a Subscription', () => {
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await popup.getByText('Continue', { exact: true }).click();
await popup.getByText('Continue', {exact: true}).click();
await Promise.all([
page.waitForNavigation(),
@ -226,7 +228,7 @@ test.describe('Subscriber purchase a Subscription', () => {
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await popup.getByText('Continue', { exact: true }).click();
await popup.getByText('Continue', {exact: true}).click();
await popup.locator('#confirmButtonTop').click();
await fillCheckoutForm(page);
@ -308,4 +310,69 @@ test.describe('Subscriber my account actions', () => {
details = await subscription.json();
await expect(details.status).toBe('CANCELLED');
});
}) ;
});
test.describe('Plan connected display buttons', () => {
test('Disable buttons if no plan connected', async ({page}) => {
const data = {
name: (Math.random() + 1).toString(36).substring(7),
type: 'subscription',
meta_data: [
{
key: '_subscription_price',
value: '10'
}
]
}
const productId = await createProduct(data)
// for some reason product meta is not updated in frontend,
// so we need to manually update the product
await updateProductUi(productId, page);
await page.goto(`/product/?p=${productId}`)
await expect(page.locator('#ppc-button-ppcp-gateway')).not.toBeVisible();
await page.locator('.single_add_to_cart_button').click();
await page.goto('/cart');
await expect(page.locator('#ppc-button-ppcp-gateway')).toBeVisible();
await expect(page.locator('#ppc-button-ppcp-gateway')).toHaveCSS('cursor', 'not-allowed')
await page.goto('/checkout');
await expect(page.locator('#ppc-button-ppcp-gateway')).toBeVisible();
await expect(page.locator('#ppc-button-ppcp-gateway')).toHaveCSS('cursor', 'not-allowed')
await deleteProduct(productId)
})
test('Enable buttons if plan connected', async ({page}) => {
const data = {
name: (Math.random() + 1).toString(36).substring(7),
type: 'subscription',
meta_data: [
{
key: '_subscription_price',
value: '10'
}
]
}
const productId = await createProduct(data)
await loginAsAdmin(page);
await page.goto(`/wp-admin/post.php?post=${productId}&action=edit`)
await page.locator('#ppcp_enable_subscription_product').check();
await page.locator('#ppcp_subscription_plan_name').fill('Plan name');
await page.locator('#publish').click();
await expect(page.getByText('Product updated.')).toBeVisible();
await page.goto(`/product/?p=${productId}`)
await expect(page.locator('#ppc-button-ppcp-gateway')).toBeVisible();
await page.locator('.single_add_to_cart_button').click();
await page.goto('/cart');
await expect(page.locator('#ppc-button-ppcp-gateway')).toBeVisible();
await deleteProduct(productId)
})
})

View file

@ -0,0 +1,59 @@
import {loginAsAdmin} from "./user";
import {expect} from "@playwright/test";
const wcApi = require('@woocommerce/woocommerce-rest-api').default;
const {
BASEURL,
WP_MERCHANT_USER,
WP_MERCHANT_PASSWORD,
} = process.env;
const wc = () => {
return new wcApi({
url: BASEURL,
consumerKey: WP_MERCHANT_USER,
consumerSecret: WP_MERCHANT_PASSWORD,
version: 'wc/v3',
});
}
export const createProduct = async (data) => {
const api = wc();
return await api.post('products', data)
.then((response) => {
return response.data.id
}).catch((error) => {
console.log(error)
})
}
export const updateProduct = async (data, id) => {
const api = wc();
return await api.put(`products/${id}`, data)
.then((response) => {
return response.data.id
}).catch((error) => {
console.log(error)
})
}
export const deleteProduct = async (id) => {
const api = wc();
return await api.delete(`products/${id}`)
.then((response) => {
return response.data.id
}).catch((error) => {
console.log(error)
})
}
export const updateProductUi = async (id, page) => {
await loginAsAdmin(page);
await page.goto(`/wp-admin/post.php?post=${id}&action=edit`)
await page.locator('#publish').click();
await expect(page.getByText('Product updated.')).toBeVisible();
}