From 6e3affb1cbeaef70e6f55ccea5ca2f1fe6cb2bb4 Mon Sep 17 00:00:00 2001
From: James Griffin
Date: Wed, 29 Jul 2026 22:31:28 -0300
Subject: [PATCH 1/2] Add an account block showing who is signed in
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[us_account], or the Account block: the signed-in visitor's name, their
email, a Sign out link, and — only when the account books for someone
besides itself — the students it books for. A parent's first question on
seeing "signed in as Grace" is whether this is the account their children's
lessons are on.
Two decisions worth naming.
Signed out with no login page chosen, the block renders nothing. Its whole
subject is the person signed in, which a stranger is not, and a bare "you
are not signed in" in a site header is noise with no way to act on it. With
a login page chosen it offers a Sign in link instead. The editor preview is
populated regardless, so the block is never an invisible box to the person
placing it.
Signing out returns to the chosen login page, or to the current page when
there is none. A block meant for a header should not also navigate someone
somewhere when they use it; the login page wins when configured, because the
page they were on may well be members-only.
The name comes from UserName::format(), so the block never exposes a
username the way display_name can.
Also brings docs/features/editor-blocks.md back in step: it still described
"four shortcodes" and had never listed the family block.
Closes #142
Co-Authored-By: Claude Opus 5
---
CHANGELOG.md | 3 +
assets/css/frontend.css | 24 ++++
assets/js/blocks.js | 22 ++++
docs/features/editor-blocks.md | 34 +++++-
src/Auth/AccountPage.php | 91 +++++++++++++++
src/BlockPreview.php | 26 +++++
src/BlockRegistrar.php | 20 ++++
src/Plugin.php | 6 +-
src/ShortcodeRegistrar.php | 3 +
templates/frontend/account-page.php | 38 +++++++
tests/Unit/Auth/AccountPageTest.php | 158 ++++++++++++++++++++++++++
tests/Unit/BlockRegistrarTest.php | 6 +
tests/Unit/ShortcodeRegistrarTest.php | 6 +-
13 files changed, 430 insertions(+), 7 deletions(-)
create mode 100644 src/Auth/AccountPage.php
create mode 100644 templates/frontend/account-page.php
create mode 100644 tests/Unit/Auth/AccountPageTest.php
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 105a748..12f66e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,9 @@ each change under the current top section as you work.
## [1.3.1]
+### Added
+- An **Account** block (`[us_account]`) showing who is signed in — their name, their email, the students they book for if that is anyone besides themselves — and a **Sign out** link. Signing out returns to the login page chosen in the block, or to the page the visitor was already on when none is set, so putting it in a site header does not also move people somewhere. To a signed-out visitor it shows a **Sign in** link when a login page is chosen, and nothing at all when one is not: a panel about who is signed in has nothing to tell a stranger, and a notice they cannot act on is just clutter in a header.
+
### Security
- Signup now checks the password properly. The form scores it as you type with the same zxcvbn meter wp-admin uses and will not submit a weak one, and the server refuses — regardless of what the browser allowed — anything shorter than 8 characters, one of the well-known leaked passwords, one built from barely any distinct characters, or one containing your own name or email address. Composition rules ("must contain a symbol") are deliberately not imposed: they mostly produce predictable substitutions. Email addresses are validated on the server on every signup path, with a clear message when one is already registered.
diff --git a/assets/css/frontend.css b/assets/css/frontend.css
index 55f7e5a..6a3c8c5 100644
--- a/assets/css/frontend.css
+++ b/assets/css/frontend.css
@@ -549,6 +549,30 @@
color: #1a7d2e;
}
+/*
+ * The account panel: who is signed in, and the way out. Sized to sit in a
+ * header or sidebar, so the rules stay minimal and inherit the theme's type —
+ * a block that lands in a site header should look like it belongs there.
+ */
+.us-account p {
+ margin: 0 0 4px;
+}
+
+.us-account-name {
+ font-weight: 600;
+}
+
+.us-account-email,
+.us-account-students {
+ display: block;
+ font-size: 0.9em;
+ opacity: 0.75;
+}
+
+.us-account-actions {
+ margin-top: 8px;
+}
+
/* Shown only in block-editor previews (see BlockPreview). */
.us-editor-note {
font-size: 0.85em;
diff --git a/assets/js/blocks.js b/assets/js/blocks.js
index d643f28..dcaf81b 100644
--- a/assets/js/blocks.js
+++ b/assets/js/blocks.js
@@ -307,6 +307,28 @@
})
),
},
+ {
+ name: 'us-scheduler/account',
+ title: __('Account', 'unsupervised-schedular'),
+ description: __('Shows who is signed in, who they book for, and a sign out link. Renders nothing for signed-out visitors unless a login page is chosen.', 'unsupervised-schedular'),
+ icon: 'admin-users',
+ keywords: ['account', 'sign out', 'log out', 'signed in', 'profile'],
+ shortcode: 'us_account',
+ attributes: {
+ loginPageId: { type: 'number', default: 0 },
+ },
+ inspector: (attributes, setAttributes) => el(
+ PanelBody,
+ { title: __('Signing in and out', 'unsupervised-schedular') },
+ el(PageSelect, {
+ label: __('Login page', 'unsupervised-schedular'),
+ help: __('Where signing out returns to, and where signed-out visitors are offered a link to sign in. Without one, signing out returns to the current page and signed-out visitors see nothing.', 'unsupervised-schedular'),
+ defaultLabel: __('Stay on the current page', 'unsupervised-schedular'),
+ value: attributes.loginPageId,
+ onChange: (loginPageId) => setAttributes({ loginPageId }),
+ })
+ ),
+ },
];
blocks.forEach((def) => {
diff --git a/docs/features/editor-blocks.md b/docs/features/editor-blocks.md
index 3383256..c30a16d 100644
--- a/docs/features/editor-blocks.md
+++ b/docs/features/editor-blocks.md
@@ -1,8 +1,8 @@
# Editor Blocks
-Gutenberg dynamic-block wrappers for the plugin's four front-end shortcodes,
-so the pages can be previewed and styled inside the block editor instead of
-appearing as grey shortcode text.
+Gutenberg dynamic-block wrappers for the plugin's front-end shortcodes, so the
+pages can be previewed and styled inside the block editor instead of appearing
+as grey shortcode text.
## Blocks
@@ -12,6 +12,8 @@ appearing as grey shortcode text.
| `us-scheduler/student-login` | `[us_student_login]` | `Auth\LoginPage::render()` |
| `us-scheduler/student-register` | `[us_student_register]` | `Auth\RegistrationPage::render()` |
| `us-scheduler/group-classes` | `[us_group_classes]` | `GroupClass\GroupClassPage::render()` |
+| `us-scheduler/family` | `[us_family]` | `Guardian\FamilyPage::render()` |
+| `us-scheduler/account` | `[us_account]` | `Auth\AccountPage::render()` |
The shortcodes remain registered for back-compat; blocks and shortcodes share
the same page objects (constructed once in `Plugin::boot()`), so front-end
@@ -21,7 +23,7 @@ transform.
## Block options
-Four blocks have sidebar (inspector) options:
+Most blocks have sidebar (inspector) options:
| Block | Attribute | Default | Effect |
|---|---|---|---|
@@ -34,6 +36,8 @@ Four blocks have sidebar (inspector) options:
| `us-scheduler/student-login` | `autoRedirect` (boolean) | `false` | Send logged-in visitors straight to the booking page instead of showing the link. Does nothing until a booking page is chosen. |
| `us-scheduler/student-register` | `loginPageId` (number) | `0` | Page students continue to once registration finishes — the "Sign in to your account" link after they confirm their email, and the "Continue to your account" link an invited student gets on the spot. `0` = the WordPress login screen for the confirmation link, and no link at all for the (already signed-in) invited student. Shortcode equivalent: `[us_student_register login_page_id="…"]`. |
| `us-scheduler/student-register` | `autoRedirect` (boolean) | `false` | Send students straight to that page instead of showing the link. Does nothing until a page is chosen — there is no login-screen fallback here. |
+| `us-scheduler/family` | `loginPageId` (number) | `0` | Where visitors who are not signed in are sent to log in. Shortcode equivalent: `[us_family login_page_id="…"]`. |
+| `us-scheduler/account` | `loginPageId` (number) | `0` | Where signing out returns to, and where a signed-out visitor is offered a **Sign in** link. `0` = signing out returns to the current page, and a signed-out visitor sees **nothing at all** — see below. Shortcode equivalent: `[us_account login_page_id="…"]`. |
| `us-scheduler/group-classes` | `offeringId` (number) | `0` | Restrict the page to a single group class, for embedding on a page dedicated to that class. The class description is then omitted — only the schedule, instructor, price and enrolment controls are shown, so the surrounding page's own copy is not repeated. `0` = browse all classes, descriptions included. Shortcode equivalent: `[us_group_classes offering="…"]`. |
The page selects list all published pages; if a chosen page is later deleted,
@@ -105,6 +109,10 @@ placeholder content:
- **Login** — the real `templates/frontend/login-page.php` template (it has
no request-state dependencies).
- **Registration** — a disabled sample of the `.us-register-form` fields.
+- **Account** — a populated sample panel. Deliberately populated whatever the
+ editor user's own state: on the published page a signed-out visitor may see
+ nothing at all, and an empty box tells the person placing the block nothing
+ about where it will sit.
Each preview starts with a `.us-editor-note` paragraph explaining what the
published page shows instead. The note class only appears in editor previews.
@@ -118,5 +126,23 @@ published page shows instead. The note class only appears in editor previews.
and fallbacks.
- `tests/Unit/Auth/LoginPageTest.php` — logged-in booking-link targets and
fallbacks.
+- `tests/Unit/Auth/AccountPageTest.php` — what each visitor sees, the
+ sign-out redirect target, and the signed-out empty render.
- `tests/Unit/BlockPreviewTest.php` — preview markup mirrors the live CSS
classes/ids and includes the editor note.
+
+## The account block's signed-out behaviour
+
+`us-scheduler/account` is the one block that can render **nothing**. It is meant
+for a header, sidebar or account page, and its whole subject is the person
+signed in — which a stranger is not. A bare "you are not signed in" in a site
+header is noise that cannot be acted on, so:
+
+- **No login page chosen** → empty string for signed-out visitors.
+- **Login page chosen** → a single **Sign in** link.
+
+Signed in, it shows the display name (`Auth\UserName::format()`, so a username
+is never exposed), the account email, a **Sign out** link, and — only on an
+account that books for someone other than itself — the students it books for.
+Signing out returns to the chosen login page, or to the current page when there
+is none, so a header sign-out does not also navigate the visitor somewhere.
diff --git a/src/Auth/AccountPage.php b/src/Auth/AccountPage.php
new file mode 100644
index 0000000..1ace546
--- /dev/null
+++ b/src/Auth/AccountPage.php
@@ -0,0 +1,91 @@
+ $atts Block attributes (`loginPageId`) or
+ * shortcode attributes (`login_page_id`).
+ */
+ public function render( array $atts ): string {
+ $loginPageId = Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 );
+ $loginUrl = $this->pageUrl( $loginPageId );
+
+ wp_enqueue_style( 'us-scheduler' );
+
+ if ( ! is_user_logged_in() ) {
+ if ( null === $loginUrl ) {
+ return '';
+ }
+
+ return sprintf(
+ '',
+ esc_url( $loginUrl ),
+ esc_html__( 'Sign in', 'unsupervised-schedular' )
+ );
+ }
+
+ // Always a WP_User here — is_user_logged_in() above rules out the
+ // id-0 placeholder wp_get_current_user() returns for a visitor.
+ $user = wp_get_current_user();
+
+ $name = UserName::format( $user, get_current_user_id() );
+ $email = $user->user_email;
+
+ // Whose lessons this account books, when that is more than just their own.
+ // A parent's first question on seeing "signed in as Grace" is whether this
+ // is the account their children's lessons are on.
+ $students = [];
+ foreach ( $this->guardians->bookableStudents( get_current_user_id() ) as $student ) {
+ if ( ! $student['is_self'] ) {
+ $students[] = $student['name'];
+ }
+ }
+
+ // Back to where they were, so signing out of a header link does not also
+ // navigate them somewhere. The login page is the better landing spot when
+ // one is configured, since the current page may be members-only.
+ $logoutUrl = wp_logout_url( $loginUrl ?? (string) get_permalink() );
+
+ ob_start();
+ include USC_PLUGIN_DIR . 'templates/frontend/account-page.php';
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * Permalink of a configured page, or null when none is chosen or the chosen
+ * page has since been deleted.
+ */
+ private function pageUrl( int $pageId ): ?string {
+ if ( $pageId <= 0 ) {
+ return null;
+ }
+
+ $url = get_permalink( $pageId );
+
+ return is_string( $url ) ? $url : null;
+ }
+}
diff --git a/src/BlockPreview.php b/src/BlockPreview.php
index 47bce0d..9f5fd1d 100644
--- a/src/BlockPreview.php
+++ b/src/BlockPreview.php
@@ -210,6 +210,32 @@ class BlockPreview {
);
}
+ /**
+ * Sample account panel. Shown populated whatever the editor's own login
+ * state, since on the published page a signed-out visitor may see nothing at
+ * all and an empty box tells the person placing the block nothing.
+ */
+ public static function account(): string {
+ return sprintf(
+ '%s'
+ . '
%s'
+ . '%s
'
+ . '
%s
'
+ . '
%s
',
+ self::note( __( 'Editor preview — each visitor sees their own account here.', 'unsupervised-schedular' ) ),
+ esc_html__( 'Grace Hopper', 'unsupervised-schedular' ),
+ esc_html__( 'grace@example.com', 'unsupervised-schedular' ),
+ esc_html(
+ sprintf(
+ /* translators: %s: comma-separated list of the students this account books for. */
+ __( 'Booking for %s', 'unsupervised-schedular' ),
+ __( 'Ada, Alan', 'unsupervised-schedular' )
+ )
+ ),
+ esc_html__( 'Sign out', 'unsupervised-schedular' )
+ );
+ }
+
private static function note( string $text ): string {
return '' . esc_html( $text ) . '
';
}
diff --git a/src/BlockRegistrar.php b/src/BlockRegistrar.php
index f97c3c6..c59e1ff 100644
--- a/src/BlockRegistrar.php
+++ b/src/BlockRegistrar.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Unsupervised\Schedular;
+use Unsupervised\Schedular\Auth\AccountPage;
use Unsupervised\Schedular\Auth\LoginPage;
use Unsupervised\Schedular\Auth\RegistrationPage;
use Unsupervised\Schedular\Booking\BookingPage;
@@ -30,6 +31,7 @@ class BlockRegistrar {
private RegistrationPage $registrationPage,
private GroupClassPage $groupClassPage,
private FamilyPage $familyPage,
+ private AccountPage $accountPage,
) {}
public function register(): void {
@@ -148,6 +150,15 @@ class BlockRegistrar {
],
],
],
+ 'us-scheduler/account' => [
+ 'render' => [ $this, 'renderAccount' ],
+ 'attributes' => [
+ 'loginPageId' => [
+ 'type' => 'number',
+ 'default' => 0,
+ ],
+ ],
+ ],
];
}
@@ -195,6 +206,15 @@ class BlockRegistrar {
return BlockPreview::groupClasses( Val::int( $attributes['offeringId'] ?? 0 ) > 0 );
}
+ /**
+ * Renders the account (who is signed in) block.
+ *
+ * @param array $attributes Block attributes.
+ */
+ public function renderAccount( array $attributes = [] ): string {
+ return $this->isEditorPreview() ? BlockPreview::account() : $this->accountPage->render( $attributes );
+ }
+
/**
* Renders the family (manage-children) block.
*
diff --git a/src/Plugin.php b/src/Plugin.php
index 889b966..fa65c95 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -5,6 +5,7 @@ namespace Unsupervised\Schedular;
use Unsupervised\Schedular\Auth\EmailConfirmationHandler;
use Unsupervised\Schedular\Auth\InviteRepository;
+use Unsupervised\Schedular\Auth\AccountPage;
use Unsupervised\Schedular\Auth\LoginPage;
use Unsupervised\Schedular\Auth\RegistrationLoginGate;
use Unsupervised\Schedular\Auth\RegistrationMailer;
@@ -99,6 +100,7 @@ class Plugin {
$registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances, $settings, $registrationMailer, $questions, $answers, $groupAccess, $guardians );
$groupClassPage = new GroupClassPage( $guardians );
$familyPage = new FamilyPage( $guardians, $questions, $answers );
+ $accountPage = new AccountPage( $guardians );
( new ScheduledBillingRunner( $paymentService, $bookings, $enrollments, $offerings, new PaymentDueMailer(), $guardians ) )->register();
@@ -110,7 +112,7 @@ class Plugin {
( new EmailConfirmationHandler( $settings, $registrationMailer ) )->register();
( new AdminMenu( $availability, $bookings, $offerings, $questions, $answers, $policies, $policyVersions, $policyService, $acceptances, $invites, $enrollments, $groupAccess, $settings, $paymentRepo, $paymentService, $resolver, $registrationMailer, $creditRepo, $guardians ) )->register();
( new RestRegistrar( $availability, $bookings, $offerings, $questions, $policies, $policyVersions, $policyService, $registrationGate, $enrollments, $groupAccess, $paymentService, $guardians ) )->register();
- ( new ShortcodeRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage, $familyPage ) )->register();
- ( new BlockRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage, $familyPage ) )->register();
+ ( new ShortcodeRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage, $familyPage, $accountPage ) )->register();
+ ( new BlockRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage, $familyPage, $accountPage ) )->register();
}
}
diff --git a/src/ShortcodeRegistrar.php b/src/ShortcodeRegistrar.php
index 42d713c..977af61 100644
--- a/src/ShortcodeRegistrar.php
+++ b/src/ShortcodeRegistrar.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Unsupervised\Schedular;
+use Unsupervised\Schedular\Auth\AccountPage;
use Unsupervised\Schedular\Auth\LoginPage;
use Unsupervised\Schedular\Auth\RegistrationPage;
use Unsupervised\Schedular\Booking\BookingPage;
@@ -18,6 +19,7 @@ class ShortcodeRegistrar {
private RegistrationPage $registrationPage,
private GroupClassPage $groupClassPage,
private FamilyPage $familyPage,
+ private AccountPage $accountPage,
) {}
public function register(): void {
@@ -26,6 +28,7 @@ class ShortcodeRegistrar {
add_shortcode( 'us_student_register', self::shortcode( [ $this->registrationPage, 'render' ] ) );
add_shortcode( 'us_group_classes', self::shortcode( [ $this->groupClassPage, 'render' ] ) );
add_shortcode( 'us_family', self::shortcode( [ $this->familyPage, 'render' ] ) );
+ add_shortcode( 'us_account', self::shortcode( [ $this->accountPage, 'render' ] ) );
// Process registration submissions before output so the invite branch's
// auth cookie is actually sent (render() runs too late, during the_content).
add_action( 'template_redirect', [ $this->registrationPage, 'maybeHandleSubmit' ] );
diff --git a/templates/frontend/account-page.php b/templates/frontend/account-page.php
new file mode 100644
index 0000000..3dcd446
--- /dev/null
+++ b/templates/frontend/account-page.php
@@ -0,0 +1,38 @@
+ $students Names this account books for, excluding themselves; empty for a plain student account.
+ * @var string $logoutUrl Nonced sign-out URL, already carrying its redirect.
+ */
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Unit/Auth/AccountPageTest.php b/tests/Unit/Auth/AccountPageTest.php
new file mode 100644
index 0000000..b1a672c
--- /dev/null
+++ b/tests/Unit/Auth/AccountPageTest.php
@@ -0,0 +1,158 @@
+guardians = Mockery::mock(GuardianService::class);
+ $this->page = new AccountPage($this->guardians);
+
+ Functions\when('is_user_logged_in')->justReturn(true);
+ Functions\when('get_current_user_id')->justReturn(5);
+ Functions\when('wp_enqueue_style')->justReturn(null);
+ Functions\when('get_permalink')->alias(
+ static fn (int $id = 0): string => $id > 0
+ ? 'https://studio.test/sign-in/'
+ : 'https://studio.test/current/'
+ );
+ Functions\when('wp_logout_url')->alias(
+ static fn (string $redirect): string => 'https://studio.test/wp-login.php?action=logout&redirect_to=' . rawurlencode($redirect)
+ );
+ Functions\when('wp_get_current_user')->justReturn($this->user('Grace', 'Hopper', 'grace@studio.test'));
+ }
+
+ private function user(string $first, string $last, string $email): \WP_User
+ {
+ $user = Mockery::mock(\WP_User::class);
+ $user->ID = 5;
+ $user->first_name = $first;
+ $user->last_name = $last;
+ $user->nickname = '';
+ $user->user_email = $email;
+
+ return $user;
+ }
+
+ /** @param list $students */
+ private function bookable(array $students): void
+ {
+ $this->guardians->shouldReceive('bookableStudents')->with(5)->andReturn($students);
+ }
+
+ public function testShowsTheSignedInNameAndEmail(): void
+ {
+ $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
+
+ $html = $this->page->render([]);
+
+ self::assertStringContainsString('Grace Hopper', $html);
+ self::assertStringContainsString('grace@studio.test', $html);
+ self::assertStringContainsString('Sign out', $html);
+ }
+
+ public function testNamesTheStudentsTheAccountBooksFor(): void
+ {
+ $this->bookable([
+ ['id' => 42, 'name' => 'Ada', 'is_self' => false],
+ ['id' => 43, 'name' => 'Alan', 'is_self' => false],
+ ['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true],
+ ]);
+
+ self::assertStringContainsString('Booking for Ada, Alan', $this->page->render([]));
+ }
+
+ /**
+ * An account that only books for itself has nothing to add — "Booking for
+ * Grace Hopper" under "Grace Hopper" is noise.
+ */
+ public function testSaysNothingAboutStudentsOnAPlainAccount(): void
+ {
+ $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
+
+ self::assertStringNotContainsString('Booking for', $this->page->render([]));
+ }
+
+ public function testSigningOutReturnsToTheConfiguredLoginPage(): void
+ {
+ $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
+
+ self::assertStringContainsString(
+ rawurlencode('https://studio.test/sign-in/'),
+ $this->page->render(['loginPageId' => 9])
+ );
+ }
+
+ /**
+ * With no page chosen, signing out from a header link should leave the
+ * visitor where they were rather than navigating them somewhere.
+ */
+ public function testSigningOutReturnsToTheCurrentPageWhenNoLoginPageIsSet(): void
+ {
+ $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
+
+ self::assertStringContainsString(
+ rawurlencode('https://studio.test/current/'),
+ $this->page->render([])
+ );
+ }
+
+ public function testTheShortcodeAttributeNameIsAccepted(): void
+ {
+ $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
+
+ self::assertStringContainsString(
+ rawurlencode('https://studio.test/sign-in/'),
+ $this->page->render(['login_page_id' => 9])
+ );
+ }
+
+ /**
+ * A block whose whole job is "you are signed in as X" has nothing to say to
+ * a stranger, and a bare notice in a site header cannot be acted on.
+ */
+ public function testRendersNothingForASignedOutVisitorWithNoLoginPage(): void
+ {
+ Functions\when('is_user_logged_in')->justReturn(false);
+ $this->guardians->shouldNotReceive('bookableStudents');
+
+ self::assertSame('', $this->page->render([]));
+ }
+
+ public function testOffersASignInLinkToASignedOutVisitorWhenAPageIsChosen(): void
+ {
+ Functions\when('is_user_logged_in')->justReturn(false);
+ $this->guardians->shouldNotReceive('bookableStudents');
+
+ $html = $this->page->render(['loginPageId' => 9]);
+
+ self::assertStringContainsString('https://studio.test/sign-in/', $html);
+ self::assertStringContainsString('Sign in', $html);
+ self::assertStringNotContainsString('Sign out', $html);
+ }
+
+ /**
+ * A page can be deleted after it has been chosen in the block, which
+ * get_permalink() reports as false.
+ */
+ public function testTreatsADeletedLoginPageAsNoneChosen(): void
+ {
+ Functions\when('is_user_logged_in')->justReturn(false);
+ Functions\when('get_permalink')->justReturn(false);
+
+ self::assertSame('', $this->page->render(['loginPageId' => 9]));
+ }
+}
diff --git a/tests/Unit/BlockRegistrarTest.php b/tests/Unit/BlockRegistrarTest.php
index 9401247..3084a6b 100644
--- a/tests/Unit/BlockRegistrarTest.php
+++ b/tests/Unit/BlockRegistrarTest.php
@@ -6,6 +6,7 @@ namespace Unsupervised\Schedular\Tests\Unit;
use Brain\Monkey\Actions;
use Brain\Monkey\Functions;
use Mockery;
+use Unsupervised\Schedular\Auth\AccountPage;
use Unsupervised\Schedular\Auth\LoginPage;
use Unsupervised\Schedular\Auth\RegistrationPage;
use Unsupervised\Schedular\BlockRegistrar;
@@ -43,6 +44,7 @@ class BlockRegistrarTest extends TestCase
private RegistrationPage&Mockery\MockInterface $registrationPage;
private GroupClassPage&Mockery\MockInterface $groupClassPage;
private FamilyPage&Mockery\MockInterface $familyPage;
+ private AccountPage&Mockery\MockInterface $accountPage;
private TestableBlockRegistrar $registrar;
protected function setUp(): void
@@ -54,6 +56,7 @@ class BlockRegistrarTest extends TestCase
$this->registrationPage = Mockery::mock(RegistrationPage::class);
$this->groupClassPage = Mockery::mock(GroupClassPage::class);
$this->familyPage = Mockery::mock(FamilyPage::class);
+ $this->accountPage = Mockery::mock(AccountPage::class);
// Most requests are not a just-finished registration; the tests that
// exercise that path override this.
@@ -67,6 +70,7 @@ class BlockRegistrarTest extends TestCase
$this->registrationPage,
$this->groupClassPage,
$this->familyPage,
+ $this->accountPage,
);
}
@@ -116,6 +120,7 @@ class BlockRegistrarTest extends TestCase
'us-scheduler/student-register',
'us-scheduler/group-classes',
'us-scheduler/family',
+ 'us-scheduler/account',
],
array_keys($registered)
);
@@ -213,6 +218,7 @@ class BlockRegistrarTest extends TestCase
$this->registrationPage,
$this->groupClassPage,
$this->familyPage,
+ $this->accountPage,
);
$this->bookingPage->shouldReceive('render')->once()->with([])->andReturn('live');
diff --git a/tests/Unit/ShortcodeRegistrarTest.php b/tests/Unit/ShortcodeRegistrarTest.php
index 790ff17..726a9cf 100644
--- a/tests/Unit/ShortcodeRegistrarTest.php
+++ b/tests/Unit/ShortcodeRegistrarTest.php
@@ -6,6 +6,7 @@ namespace Unsupervised\Schedular\Tests\Unit;
use Brain\Monkey\Actions;
use Brain\Monkey\Functions;
use Mockery;
+use Unsupervised\Schedular\Auth\AccountPage;
use Unsupervised\Schedular\Auth\LoginPage;
use Unsupervised\Schedular\Auth\RegistrationPage;
use Unsupervised\Schedular\Booking\BookingPage;
@@ -20,6 +21,7 @@ class ShortcodeRegistrarTest extends TestCase
private RegistrationPage&Mockery\MockInterface $registrationPage;
private GroupClassPage&Mockery\MockInterface $groupClassPage;
private FamilyPage&Mockery\MockInterface $familyPage;
+ private AccountPage&Mockery\MockInterface $accountPage;
private ShortcodeRegistrar $registrar;
/** @var array */
@@ -37,6 +39,7 @@ class ShortcodeRegistrarTest extends TestCase
$this->registrationPage = Mockery::mock(RegistrationPage::class);
$this->groupClassPage = Mockery::mock(GroupClassPage::class);
$this->familyPage = Mockery::mock(FamilyPage::class);
+ $this->accountPage = Mockery::mock(AccountPage::class);
$this->registrar = new ShortcodeRegistrar(
$this->bookingPage,
@@ -44,6 +47,7 @@ class ShortcodeRegistrarTest extends TestCase
$this->registrationPage,
$this->groupClassPage,
$this->familyPage,
+ $this->accountPage,
);
$shortcodes = &$this->shortcodes;
@@ -66,7 +70,7 @@ class ShortcodeRegistrarTest extends TestCase
$this->registrar->register();
self::assertSame(
- ['us_booking', 'us_student_login', 'us_student_register', 'us_group_classes', 'us_family'],
+ ['us_booking', 'us_student_login', 'us_student_register', 'us_group_classes', 'us_family', 'us_account'],
array_keys($this->shortcodes)
);
}
--
2.54.0
From ab5212282d90b64908e59f9bd9e5b4497f605929 Mon Sep 17 00:00:00 2001
From: James Griffin
Date: Wed, 29 Jul 2026 22:35:04 -0300
Subject: [PATCH 2/2] Show only the name and email, not who the account books
for
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The block reports who is signed in and nothing more. Dropping the "Booking
for …" line takes GuardianService with it — it was the only reason the page
had a dependency at all, so AccountPage now constructs with no arguments.
Co-Authored-By: Claude Opus 5
---
CHANGELOG.md | 2 +-
assets/css/frontend.css | 3 +-
assets/js/blocks.js | 2 +-
docs/features/editor-blocks.md | 5 ++--
src/Auth/AccountPage.php | 13 ---------
src/BlockPreview.php | 8 ------
src/Plugin.php | 2 +-
templates/frontend/account-page.php | 13 ---------
tests/Unit/Auth/AccountPageTest.php | 43 +----------------------------
9 files changed, 7 insertions(+), 84 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12f66e3..cd7eab8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,7 @@ each change under the current top section as you work.
## [1.3.1]
### Added
-- An **Account** block (`[us_account]`) showing who is signed in — their name, their email, the students they book for if that is anyone besides themselves — and a **Sign out** link. Signing out returns to the login page chosen in the block, or to the page the visitor was already on when none is set, so putting it in a site header does not also move people somewhere. To a signed-out visitor it shows a **Sign in** link when a login page is chosen, and nothing at all when one is not: a panel about who is signed in has nothing to tell a stranger, and a notice they cannot act on is just clutter in a header.
+- An **Account** block (`[us_account]`) showing who is signed in — their name and their email — and a **Sign out** link. Signing out returns to the login page chosen in the block, or to the page the visitor was already on when none is set, so putting it in a site header does not also move people somewhere. To a signed-out visitor it shows a **Sign in** link when a login page is chosen, and nothing at all when one is not: a panel about who is signed in has nothing to tell a stranger, and a notice they cannot act on is just clutter in a header.
### Security
- Signup now checks the password properly. The form scores it as you type with the same zxcvbn meter wp-admin uses and will not submit a weak one, and the server refuses — regardless of what the browser allowed — anything shorter than 8 characters, one of the well-known leaked passwords, one built from barely any distinct characters, or one containing your own name or email address. Composition rules ("must contain a symbol") are deliberately not imposed: they mostly produce predictable substitutions. Email addresses are validated on the server on every signup path, with a clear message when one is already registered.
diff --git a/assets/css/frontend.css b/assets/css/frontend.css
index 6a3c8c5..98a9d9d 100644
--- a/assets/css/frontend.css
+++ b/assets/css/frontend.css
@@ -562,8 +562,7 @@
font-weight: 600;
}
-.us-account-email,
-.us-account-students {
+.us-account-email {
display: block;
font-size: 0.9em;
opacity: 0.75;
diff --git a/assets/js/blocks.js b/assets/js/blocks.js
index dcaf81b..8971ce0 100644
--- a/assets/js/blocks.js
+++ b/assets/js/blocks.js
@@ -310,7 +310,7 @@
{
name: 'us-scheduler/account',
title: __('Account', 'unsupervised-schedular'),
- description: __('Shows who is signed in, who they book for, and a sign out link. Renders nothing for signed-out visitors unless a login page is chosen.', 'unsupervised-schedular'),
+ description: __('Shows the name and email of whoever is signed in, with a sign out link. Renders nothing for signed-out visitors unless a login page is chosen.', 'unsupervised-schedular'),
icon: 'admin-users',
keywords: ['account', 'sign out', 'log out', 'signed in', 'profile'],
shortcode: 'us_account',
diff --git a/docs/features/editor-blocks.md b/docs/features/editor-blocks.md
index c30a16d..51d826f 100644
--- a/docs/features/editor-blocks.md
+++ b/docs/features/editor-blocks.md
@@ -142,7 +142,6 @@ header is noise that cannot be acted on, so:
- **Login page chosen** → a single **Sign in** link.
Signed in, it shows the display name (`Auth\UserName::format()`, so a username
-is never exposed), the account email, a **Sign out** link, and — only on an
-account that books for someone other than itself — the students it books for.
-Signing out returns to the chosen login page, or to the current page when there
+is never exposed), the account email, and a **Sign out** link — deliberately
+nothing else. Signing out returns to the chosen login page, or to the current page when there
is none, so a header sign-out does not also navigate the visitor somewhere.
diff --git a/src/Auth/AccountPage.php b/src/Auth/AccountPage.php
index 1ace546..3e89637 100644
--- a/src/Auth/AccountPage.php
+++ b/src/Auth/AccountPage.php
@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Unsupervised\Schedular\Auth;
-use Unsupervised\Schedular\Guardian\GuardianService;
use Unsupervised\Schedular\Val;
/**
@@ -15,8 +14,6 @@ use Unsupervised\Schedular\Val;
*/
class AccountPage {
- public function __construct( private GuardianService $guardians ) {}
-
/**
* Renders the account shortcode/block output.
*
@@ -55,16 +52,6 @@ class AccountPage {
$name = UserName::format( $user, get_current_user_id() );
$email = $user->user_email;
- // Whose lessons this account books, when that is more than just their own.
- // A parent's first question on seeing "signed in as Grace" is whether this
- // is the account their children's lessons are on.
- $students = [];
- foreach ( $this->guardians->bookableStudents( get_current_user_id() ) as $student ) {
- if ( ! $student['is_self'] ) {
- $students[] = $student['name'];
- }
- }
-
// Back to where they were, so signing out of a header link does not also
// navigate them somewhere. The login page is the better landing spot when
// one is configured, since the current page may be members-only.
diff --git a/src/BlockPreview.php b/src/BlockPreview.php
index 9f5fd1d..c1c3f65 100644
--- a/src/BlockPreview.php
+++ b/src/BlockPreview.php
@@ -220,18 +220,10 @@ class BlockPreview {
'%s'
. '
%s'
. '%s
'
- . '
%s
'
. '
%s
',
self::note( __( 'Editor preview — each visitor sees their own account here.', 'unsupervised-schedular' ) ),
esc_html__( 'Grace Hopper', 'unsupervised-schedular' ),
esc_html__( 'grace@example.com', 'unsupervised-schedular' ),
- esc_html(
- sprintf(
- /* translators: %s: comma-separated list of the students this account books for. */
- __( 'Booking for %s', 'unsupervised-schedular' ),
- __( 'Ada, Alan', 'unsupervised-schedular' )
- )
- ),
esc_html__( 'Sign out', 'unsupervised-schedular' )
);
}
diff --git a/src/Plugin.php b/src/Plugin.php
index fa65c95..394f92e 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -100,7 +100,7 @@ class Plugin {
$registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances, $settings, $registrationMailer, $questions, $answers, $groupAccess, $guardians );
$groupClassPage = new GroupClassPage( $guardians );
$familyPage = new FamilyPage( $guardians, $questions, $answers );
- $accountPage = new AccountPage( $guardians );
+ $accountPage = new AccountPage();
( new ScheduledBillingRunner( $paymentService, $bookings, $enrollments, $offerings, new PaymentDueMailer(), $guardians ) )->register();
diff --git a/templates/frontend/account-page.php b/templates/frontend/account-page.php
index 3dcd446..b94a444 100644
--- a/templates/frontend/account-page.php
+++ b/templates/frontend/account-page.php
@@ -8,7 +8,6 @@ if (! defined('ABSPATH')) {
/**
* @var string $name Display name of the signed-in visitor.
* @var string $email Their account email.
- * @var list $students Names this account books for, excluding themselves; empty for a plain student account.
* @var string $logoutUrl Nonced sign-out URL, already carrying its redirect.
*/
?>
@@ -20,18 +19,6 @@ if (! defined('ABSPATH')) {
-
-
-
-
-
-
diff --git a/tests/Unit/Auth/AccountPageTest.php b/tests/Unit/Auth/AccountPageTest.php
index b1a672c..a6b8708 100644
--- a/tests/Unit/Auth/AccountPageTest.php
+++ b/tests/Unit/Auth/AccountPageTest.php
@@ -6,20 +6,17 @@ namespace Unsupervised\Schedular\Tests\Unit\Auth;
use Brain\Monkey\Functions;
use Mockery;
use Unsupervised\Schedular\Auth\AccountPage;
-use Unsupervised\Schedular\Guardian\GuardianService;
use Unsupervised\Schedular\Tests\Unit\TestCase;
class AccountPageTest extends TestCase
{
- private GuardianService&Mockery\MockInterface $guardians;
private AccountPage $page;
protected function setUp(): void
{
parent::setUp();
- $this->guardians = Mockery::mock(GuardianService::class);
- $this->page = new AccountPage($this->guardians);
+ $this->page = new AccountPage();
Functions\when('is_user_logged_in')->justReturn(true);
Functions\when('get_current_user_id')->justReturn(5);
@@ -47,16 +44,8 @@ class AccountPageTest extends TestCase
return $user;
}
- /** @param list $students */
- private function bookable(array $students): void
- {
- $this->guardians->shouldReceive('bookableStudents')->with(5)->andReturn($students);
- }
-
public function testShowsTheSignedInNameAndEmail(): void
{
- $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
-
$html = $this->page->render([]);
self::assertStringContainsString('Grace Hopper', $html);
@@ -64,32 +53,8 @@ class AccountPageTest extends TestCase
self::assertStringContainsString('Sign out', $html);
}
- public function testNamesTheStudentsTheAccountBooksFor(): void
- {
- $this->bookable([
- ['id' => 42, 'name' => 'Ada', 'is_self' => false],
- ['id' => 43, 'name' => 'Alan', 'is_self' => false],
- ['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true],
- ]);
-
- self::assertStringContainsString('Booking for Ada, Alan', $this->page->render([]));
- }
-
- /**
- * An account that only books for itself has nothing to add — "Booking for
- * Grace Hopper" under "Grace Hopper" is noise.
- */
- public function testSaysNothingAboutStudentsOnAPlainAccount(): void
- {
- $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
-
- self::assertStringNotContainsString('Booking for', $this->page->render([]));
- }
-
public function testSigningOutReturnsToTheConfiguredLoginPage(): void
{
- $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
-
self::assertStringContainsString(
rawurlencode('https://studio.test/sign-in/'),
$this->page->render(['loginPageId' => 9])
@@ -102,8 +67,6 @@ class AccountPageTest extends TestCase
*/
public function testSigningOutReturnsToTheCurrentPageWhenNoLoginPageIsSet(): void
{
- $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
-
self::assertStringContainsString(
rawurlencode('https://studio.test/current/'),
$this->page->render([])
@@ -112,8 +75,6 @@ class AccountPageTest extends TestCase
public function testTheShortcodeAttributeNameIsAccepted(): void
{
- $this->bookable([['id' => 5, 'name' => 'Grace Hopper', 'is_self' => true]]);
-
self::assertStringContainsString(
rawurlencode('https://studio.test/sign-in/'),
$this->page->render(['login_page_id' => 9])
@@ -127,7 +88,6 @@ class AccountPageTest extends TestCase
public function testRendersNothingForASignedOutVisitorWithNoLoginPage(): void
{
Functions\when('is_user_logged_in')->justReturn(false);
- $this->guardians->shouldNotReceive('bookableStudents');
self::assertSame('', $this->page->render([]));
}
@@ -135,7 +95,6 @@ class AccountPageTest extends TestCase
public function testOffersASignInLinkToASignedOutVisitorWhenAPageIsChosen(): void
{
Functions\when('is_user_logged_in')->justReturn(false);
- $this->guardians->shouldNotReceive('bookableStudents');
$html = $this->page->render(['loginPageId' => 9]);
--
2.54.0