Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
Expand Down
12 changes: 11 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
19 changes: 2 additions & 17 deletions inc/account.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = '<!-- wp:pattern {"slug":"libresign/saas-onboarding"} /-->';
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
Expand Down
27 changes: 27 additions & 0 deletions inc/onboarding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* SaaS onboarding: shown only while buying a new plan.
*
* @package libresign
*/

defined( 'ABSPATH' ) || exit;

function libresign_theme_show_saas_onboarding_only_on_new_purchases( $block_content, $block ) {
$class_names = explode( ' ', $block['attrs']['className'] ?? '' );
if ( ! in_array( 'libresign-saas-onboarding', $class_names, true ) ) {
return $block_content;
}

if ( function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'order-pay' ) ) {
return '';
}

$order_id = absint( get_query_var( 'order-received' ) );
if ( $order_id && function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id, array( 'renewal', 'switch', 'resubscribe' ) ) ) {
return '';
}

return $block_content;
}
add_filter( 'render_block_core/group', 'libresign_theme_show_saas_onboarding_only_on_new_purchases', 10, 2 );
55 changes: 55 additions & 0 deletions patterns/saas-onboarding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
/**
* Title: SaaS onboarding
* Slug: libresign/saas-onboarding
* Inserter: no
*/
?>

<!-- wp:group {"align":"wide","className":"libresign-saas-onboarding","style":{"spacing":{"margin":{"bottom":"var:preset|spacing|40"}}},"layout":{"type":"default"}} -->
<div class="wp-block-group alignwide libresign-saas-onboarding" style="margin-bottom:var(--wp--preset--spacing--40)">
<!-- wp:heading {"level":2,"fontSize":"large"} -->
<h2 class="wp-block-heading has-large-font-size"><?php echo esc_html__( 'How your LibreSign workspace works', 'libresign' ); ?></h2>
<!-- /wp:heading -->

<!-- wp:columns -->
<div class="wp-block-columns">
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":3,"fontSize":"medium"} -->
<h3 class="wp-block-heading has-medium-font-size"><?php echo esc_html__( '1. Choose a plan', 'libresign' ); ?></h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p><?php echo esc_html__( 'Pick the plan that fits your team.', 'libresign' ); ?></p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":3,"fontSize":"medium"} -->
<h3 class="wp-block-heading has-medium-font-size"><?php echo esc_html__( '2. Create your workspace', 'libresign' ); ?></h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p><?php echo esc_html__( 'Set your email and password and complete the payment.', 'libresign' ); ?></p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":3,"fontSize":"medium"} -->
<h3 class="wp-block-heading has-medium-font-size"><?php echo esc_html__( '3. Start signing', 'libresign' ); ?></h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p><?php echo esc_html__( 'Sign in to LibreSign with the same email and password.', 'libresign' ); ?></p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
</div>
<!-- /wp:group -->
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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_`).
Expand All @@ -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`:

Expand Down
2 changes: 2 additions & 0 deletions templates/archive-product.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

<!-- wp:group {"tagName":"main","layout":{"inherit":true,"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:pattern {"slug":"libresign/saas-onboarding"} /-->

<!-- wp:woocommerce/breadcrumbs /-->

<!-- wp:query-title {"type":"archive","showPrefix":false,"align":"wide"} /-->
Expand Down
1 change: 1 addition & 0 deletions templates/page-checkout.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<!-- wp:woocommerce/page-content-wrapper {"page":"checkout"} -->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:pattern {"slug":"libresign/saas-onboarding"} /-->
<!-- wp:woocommerce/store-notices /-->
<!-- wp:post-content {"align":"wide"} /-->
</main>
Expand Down
41 changes: 41 additions & 0 deletions tests/E2E/Patterns/SaasOnboarding.spec.ts
Original file line number Diff line number Diff line change
@@ -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 );
} );
} );
8 changes: 4 additions & 4 deletions tests/Integration/Inc/AccountTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,22 +165,22 @@ 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( '<p>Page</p>', libresign_theme_prepend_saas_onboarding_to_content( '<p>Page</p>' ) );
$this->assertSame( '<p>Page</p>', libresign_theme_render_account_content( '<p>Page</p>' ) );
}

public function test_keeps_the_content_outside_the_loop() {
add_filter( 'woocommerce_is_account_page', '__return_true' );
$this->go_to( home_url() );

$this->assertSame( '<p>Page</p>', libresign_theme_prepend_saas_onboarding_to_content( '<p>Page</p>' ) );
$this->assertSame( '<p>Page</p>', libresign_theme_render_account_content( '<p>Page</p>' ) );
}

public function test_keeps_the_content_in_the_admin() {
add_filter( 'woocommerce_is_account_page', '__return_true' );
$this->in_the_loop_of( home_url() );
set_current_screen( 'dashboard' );

$this->assertSame( '<p>Page</p>', libresign_theme_prepend_saas_onboarding_to_content( '<p>Page</p>' ) );
$this->assertSame( '<p>Page</p>', libresign_theme_render_account_content( '<p>Page</p>' ) );
}

/**
Expand All @@ -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 ) {
Expand Down
93 changes: 93 additions & 0 deletions tests/Integration/Inc/OnboardingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace LibreSign\WordPressTheme\Tests\Integration\Inc;

use LibreSign\WordPressTheme\Tests\Support\StoreFactory;
use WCS_Related_Order_Store;
use WP_UnitTestCase;

final class OnboardingTest extends WP_UnitTestCase {

private const HEADING = 'How your LibreSign workspace works';

private $store;

public function set_up() {
parent::set_up();

$this->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( '<!-- wp:group --><div class="wp-block-group"><p>Other group</p></div><!-- /wp:group -->' ) );
}

private function onboarding() {
return do_blocks( '<!-- wp:pattern {"slug":"libresign/saas-onboarding"} /-->' );
}

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() );
}
}
Loading
Loading