diff --git a/composer.json b/composer.json
index 9a29348..b05b3bf 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,19 @@
"only": [
"wpackagist-plugin/*"
]
+ },
+ {
+ "type": "package",
+ "package": {
+ "name": "pronamic/woocommerce-subscriptions",
+ "version": "9.2.0",
+ "type": "wordpress-plugin",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pronamic/woocommerce-subscriptions.git",
+ "reference": "0a2c496cd263811707caa37c8bf01ffa450c6009"
+ }
+ }
}
],
"require": {
@@ -18,6 +31,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8",
"composer/installers": "^2.3",
+ "pronamic/woocommerce-subscriptions": "^9.2",
"roots/wordpress": "^7.0",
"wp-phpunit/wp-phpunit": "^7.0",
"wpackagist-plugin/woocommerce": "^11.0"
diff --git a/composer.lock b/composer.lock
index 740387a..c7e816a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "100daeaa732ad02c2952242d18b7d8f1",
+ "content-hash": "a4c1e95fb5e6b3f2fa1b051e281e06c0",
"packages": [],
"packages-dev": [
{
@@ -210,6 +210,16 @@
],
"time": "2024-06-24T20:46:46+00:00"
},
+ {
+ "name": "pronamic/woocommerce-subscriptions",
+ "version": "9.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pronamic/woocommerce-subscriptions.git",
+ "reference": "0a2c496cd263811707caa37c8bf01ffa450c6009"
+ },
+ "type": "wordpress-plugin"
+ },
{
"name": "roots/wordpress",
"version": "7.1.2",
diff --git a/functions.php b/functions.php
index f593b4c..0579b2f 100644
--- a/functions.php
+++ b/functions.php
@@ -19,3 +19,4 @@
require_once __DIR__ . '/inc/product.php';
require_once __DIR__ . '/inc/cpf-cnpj.php';
require_once __DIR__ . '/inc/checkout.php';
+require_once __DIR__ . '/inc/onboarding.php';
diff --git a/inc/account.php b/inc/account.php
index d779e97..5b54e12 100644
--- a/inc/account.php
+++ b/inc/account.php
@@ -148,9 +148,8 @@ function libresign_theme_render_lost_password_form() {
* - Any other account page → WooCommerce my-account shortcode (which uses
* the woocommerce/myaccount/form-login.php template override for guests
* and the standard dashboard for logged-in users).
- * - Shop / checkout → prepend the SaaS onboarding block pattern.
*/
-function libresign_theme_prepend_saas_onboarding_to_content( $content ) {
+function libresign_theme_render_account_content( $content ) {
if ( is_admin() || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
@@ -178,23 +177,9 @@ function libresign_theme_prepend_saas_onboarding_to_content( $content ) {
return function_exists( 'do_shortcode' ) ? do_shortcode( '[woocommerce_my_account]' ) : $content;
}
- $should_prepend = ( function_exists( 'is_shop' ) && is_shop() )
- || ( function_exists( 'is_checkout' ) && is_checkout() );
-
- if ( function_exists( 'is_order_received_page' ) && is_order_received_page() ) {
- $should_prepend = true;
- }
-
- if ( $should_prepend ) {
- $onboarding_block = '';
- if ( false === strpos( $content, 'libresign/saas-onboarding' ) ) {
- $content = do_blocks( $onboarding_block ) . $content;
- }
- }
-
return $content;
}
-add_filter( 'the_content', 'libresign_theme_prepend_saas_onboarding_to_content', 5 );
+add_filter( 'the_content', 'libresign_theme_render_account_content', 5 );
// ---------------------------------------------------------------------------
// Direct /lost-password/ route handler
diff --git a/inc/onboarding.php b/inc/onboarding.php
new file mode 100644
index 0000000..664e12b
--- /dev/null
+++ b/inc/onboarding.php
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 4480303..722a7a6 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -15,6 +15,9 @@ parameters:
bootstrapFiles:
- vendor-bin/phpstan/vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php
- vendor-bin/phpunit/vendor/autoload.php
+ scanFiles:
+ - vendor/test-plugins/woocommerce-subscriptions/includes/core/wcs-functions.php
+ - vendor/test-plugins/woocommerce-subscriptions/includes/core/abstracts/abstract-wcs-related-order-store.php
scanDirectories:
- vendor/wp-phpunit/wp-phpunit/includes
ignoreErrors:
diff --git a/readme.txt b/readme.txt
index 337b45d..dee67d7 100644
--- a/readme.txt
+++ b/readme.txt
@@ -67,8 +67,8 @@ test under `tests/E2E` follows the same names. `tests/Unit/StructureTest.php`
enforces this and also fails on a test whose file no longer exists. In CI, octocov fails the run when line
coverage is below 90% or below the last report of `main` (`.octocov.yml`).
-`composer install` brings in WordPress, the WordPress test suite and WooCommerce,
-so the tests only need a MySQL/MariaDB database they are allowed to wipe on every
+`composer install` brings in WordPress, the WordPress test suite, WooCommerce and
+WooCommerce Subscriptions, so the tests only need a MySQL/MariaDB database they are allowed to wipe on every
run. The connection comes from `WP_TESTS_DB_NAME` (`wordpress_test`),
`WP_TESTS_DB_USER` (`root`), `WP_TESTS_DB_PASSWORD` (`root`), `WP_TESTS_DB_HOST`
(`mariadb`) and `WP_TESTS_TABLE_PREFIX` (`wptests_`).
@@ -82,7 +82,8 @@ On the local SaaS stack:
wordpress-docker-wordpress-1 composer test
The browser tests in `tests/E2E` cover creating a workspace together with a plan,
-the CPF/CNPJ field and the policy consent at checkout, and the lost password page.
+the CPF/CNPJ field and the policy consent at checkout, the lost password page,
+and the onboarding on the shop, checkout and order received pages.
They need Docker, Node.js and a `composer install`, since the stack mounts
WooCommerce from `vendor/test-plugins`:
diff --git a/templates/archive-product.html b/templates/archive-product.html
index d546814..8b6e6b6 100644
--- a/templates/archive-product.html
+++ b/templates/archive-product.html
@@ -2,6 +2,8 @@
+
+
diff --git a/templates/page-checkout.html b/templates/page-checkout.html
index 92209f6..72566c6 100644
--- a/templates/page-checkout.html
+++ b/templates/page-checkout.html
@@ -2,6 +2,7 @@
+
diff --git a/tests/E2E/Patterns/SaasOnboarding.spec.ts b/tests/E2E/Patterns/SaasOnboarding.spec.ts
new file mode 100644
index 0000000..8199771
--- /dev/null
+++ b/tests/E2E/Patterns/SaasOnboarding.spec.ts
@@ -0,0 +1,41 @@
+import { expect, Page, test } from '@playwright/test';
+
+import { addThePlanToTheCart, fillTheBillingAddress, placeOrder } from '../support/store';
+
+async function expectTheOnboarding( page: Page ): Promise< void > {
+ await expect( page.getByRole( 'heading', { name: 'How your LibreSign workspace works' } ) ).toHaveCount( 1 );
+ await expect( page.getByRole( 'heading', { name: '1. Choose a plan' } ) ).toBeVisible();
+ await expect( page.getByRole( 'heading', { name: '2. Create your workspace' } ) ).toBeVisible();
+ await expect( page.getByRole( 'heading', { name: '3. Start signing' } ) ).toBeVisible();
+}
+
+test.describe( 'Showing how the workspace works while buying', () => {
+ test( 'on the shop', async ( { page } ) => {
+ await page.goto( '/shop/' );
+
+ await expectTheOnboarding( page );
+ } );
+
+ test( 'on the checkout', async ( { page } ) => {
+ await addThePlanToTheCart( page );
+ await page.goto( '/checkout/' );
+
+ await expectTheOnboarding( page );
+ } );
+
+ test( 'on the order received', async ( { page } ) => {
+ await addThePlanToTheCart( page );
+ await fillTheBillingAddress( page, 'Brazil', '529.982.247-25' );
+ await page.getByLabel( 'I agree to the terms and privacy policy before placing the order.' ).check();
+ await placeOrder( page );
+
+ await expect( page ).toHaveURL( /\/order-received\// );
+ await expectTheOnboarding( page );
+ } );
+
+ test( 'not on other pages', async ( { page } ) => {
+ await page.goto( '/product/basic/' );
+
+ await expect( page.getByRole( 'heading', { name: 'How your LibreSign workspace works' } ) ).toHaveCount( 0 );
+ } );
+} );
diff --git a/tests/Integration/Inc/AccountTest.php b/tests/Integration/Inc/AccountTest.php
index 2be5d3f..a43bc64 100644
--- a/tests/Integration/Inc/AccountTest.php
+++ b/tests/Integration/Inc/AccountTest.php
@@ -165,14 +165,14 @@ public function test_the_account_page_leaves_the_reset_confirmation_to_woocommer
public function test_keeps_the_content_of_other_pages() {
$this->in_the_loop_of( home_url() );
- $this->assertSame( 'Page
', libresign_theme_prepend_saas_onboarding_to_content( 'Page
' ) );
+ $this->assertSame( 'Page
', libresign_theme_render_account_content( 'Page
' ) );
}
public function test_keeps_the_content_outside_the_loop() {
add_filter( 'woocommerce_is_account_page', '__return_true' );
$this->go_to( home_url() );
- $this->assertSame( 'Page
', libresign_theme_prepend_saas_onboarding_to_content( 'Page
' ) );
+ $this->assertSame( 'Page
', libresign_theme_render_account_content( 'Page
' ) );
}
public function test_keeps_the_content_in_the_admin() {
@@ -180,7 +180,7 @@ public function test_keeps_the_content_in_the_admin() {
$this->in_the_loop_of( home_url() );
set_current_screen( 'dashboard' );
- $this->assertSame( 'Page
', libresign_theme_prepend_saas_onboarding_to_content( 'Page
' ) );
+ $this->assertSame( 'Page
', libresign_theme_render_account_content( 'Page
' ) );
}
/**
@@ -206,7 +206,7 @@ private function account_page_content( $query = array() ) {
add_filter( 'woocommerce_is_account_page', '__return_true' );
$this->in_the_loop_of( add_query_arg( $query, home_url( '/' ) ) );
- return libresign_theme_prepend_saas_onboarding_to_content( '' );
+ return libresign_theme_render_account_content( '' );
}
private function in_the_loop_of( $url ) {
diff --git a/tests/Integration/Inc/OnboardingTest.php b/tests/Integration/Inc/OnboardingTest.php
new file mode 100644
index 0000000..9e0c337
--- /dev/null
+++ b/tests/Integration/Inc/OnboardingTest.php
@@ -0,0 +1,93 @@
+store = new StoreFactory();
+ }
+
+ public function tear_down() {
+ unset( $GLOBALS['wp']->query_vars['order-pay'], $GLOBALS['wp']->query_vars['order-received'] );
+ set_query_var( 'order-received', '' );
+
+ parent::tear_down();
+ }
+
+ public function test_shows_the_onboarding_while_buying() {
+ $this->assertStringContainsString( self::HEADING, $this->onboarding() );
+ }
+
+ public function test_shows_the_onboarding_when_a_new_order_is_received() {
+ $this->receive( $this->order() );
+
+ $this->assertStringContainsString( self::HEADING, $this->onboarding() );
+ }
+
+ public function test_hides_the_onboarding_when_paying_an_existing_order() {
+ $GLOBALS['wp']->query_vars['order-pay'] = (string) $this->order()->get_id();
+
+ $this->assertSame( '', trim( $this->onboarding() ) );
+ }
+
+ /**
+ * @dataProvider provide_orders_of_existing_subscriptions
+ */
+ public function test_hides_the_onboarding_when_an_order_of_an_existing_subscription_is_received( $relation ) {
+ $order = $this->order();
+ WCS_Related_Order_Store::instance()->add_relation( $order, $this->subscription(), $relation );
+ $this->receive( $order );
+
+ $this->assertSame( '', trim( $this->onboarding() ) );
+ }
+
+ public static function provide_orders_of_existing_subscriptions() {
+ yield 'a renewal' => array( 'renewal' );
+ yield 'a switch' => array( 'switch' );
+ yield 'a resubscribe' => array( 'resubscribe' );
+ }
+
+ public function test_keeps_other_groups_while_hiding_the_onboarding() {
+ $GLOBALS['wp']->query_vars['order-pay'] = (string) $this->order()->get_id();
+
+ $this->assertStringContainsString( 'Other group', do_blocks( '' ) );
+ }
+
+ private function onboarding() {
+ return do_blocks( '' );
+ }
+
+ private function order() {
+ $order = wc_create_order();
+ $order->add_product( $this->store->plan() );
+ $order->save();
+
+ return $order;
+ }
+
+ private function subscription() {
+ return wcs_create_subscription(
+ array(
+ 'customer_id' => self::factory()->user->create(),
+ 'billing_period' => 'month',
+ 'billing_interval' => 1,
+ )
+ );
+ }
+
+ private function receive( $order ) {
+ $GLOBALS['wp']->query_vars['order-received'] = (string) $order->get_id();
+ set_query_var( 'order-received', (string) $order->get_id() );
+ }
+}
diff --git a/tests/Integration/Patterns/SaasOnboardingTest.php b/tests/Integration/Patterns/SaasOnboardingTest.php
new file mode 100644
index 0000000..1186830
--- /dev/null
+++ b/tests/Integration/Patterns/SaasOnboardingTest.php
@@ -0,0 +1,30 @@
+assertTrue( \WP_Block_Patterns_Registry::get_instance()->is_registered( 'libresign/saas-onboarding' ) );
+ }
+
+ /**
+ * @dataProvider provide_templates_of_the_purchase
+ */
+ public function test_the_template_shows_the_onboarding( $template ) {
+ $html = do_blocks( get_block_template( get_stylesheet() . '//' . $template )->content );
+
+ $this->assertSame( 1, substr_count( $html, 'How your LibreSign workspace works' ) );
+ $this->assertStringContainsString( '1. Choose a plan', $html );
+ $this->assertStringContainsString( '2. Create your workspace', $html );
+ $this->assertStringContainsString( '3. Start signing', $html );
+ }
+
+ public static function provide_templates_of_the_purchase() {
+ yield 'the shop' => array( 'archive-product' );
+ yield 'the checkout' => array( 'page-checkout' );
+ yield 'the order received' => array( 'order-confirmation' );
+ }
+}
diff --git a/tests/bootstrap-integration.php b/tests/bootstrap-integration.php
index 8663c88..1e95a58 100644
--- a/tests/bootstrap-integration.php
+++ b/tests/bootstrap-integration.php
@@ -32,6 +32,7 @@
'muplugins_loaded',
static function () use ( $test_plugins_dir ) {
require $test_plugins_dir . '/woocommerce/woocommerce.php';
+ require $test_plugins_dir . '/woocommerce-subscriptions/woocommerce-subscriptions.php';
register_theme_directory( dirname( __DIR__, 2 ) );
}
);
@@ -39,7 +40,10 @@ static function () use ( $test_plugins_dir ) {
tests_add_filter(
'pre_option_active_plugins',
static function () {
- return array( 'woocommerce/woocommerce.php' );
+ return array(
+ 'woocommerce/woocommerce.php',
+ 'woocommerce-subscriptions/woocommerce-subscriptions.php',
+ );
}
);