Add an account block showing who is signed in

[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 <[email protected]>
This commit is contained in:
2026-07-29 22:42:25 -03:00
co-authored by Claude Opus 5
parent 7875cb1bf7
commit 6e3affb1cb
13 changed files with 430 additions and 7 deletions
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Auth;
use Unsupervised\Schedular\Guardian\GuardianService;
use Unsupervised\Schedular\Val;
/**
* Who is signed in, and the way out.
*
* Meant for a header, sidebar or account page — somewhere it sits alongside
* other content rather than being the whole of it. That shapes the two
* decisions below.
*/
class AccountPage {
public function __construct( private GuardianService $guardians ) {}
/**
* Renders the account shortcode/block output.
*
* Signed out, this renders a sign-in link when a login page is configured and
* **nothing at all** when one is not. A block whose whole job is "you are
* signed in as X" has nothing to say to a stranger, and a bare "you are not
* signed in" in a site header is noise with no way to act on it. The editor
* preview shows the populated state regardless, so the block is never
* invisible to the person placing it.
*
* @param array<int|string, mixed> $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(
'<div class="us-account us-account-out"><a class="us-account-signin" href="%s">%s</a></div>',
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;
}
}
+26
View File
@@ -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(
'<div class="us-account">%s'
. '<p class="us-account-who"><span class="us-account-name">%s</span>'
. '<span class="us-account-email">%s</span></p>'
. '<p class="us-account-students">%s</p>'
. '<p class="us-account-actions"><a class="us-account-signout" href="#">%s</a></p></div>',
self::note( __( 'Editor preview — each visitor sees their own account here.', 'unsupervised-schedular' ) ),
esc_html__( 'Grace Hopper', 'unsupervised-schedular' ),
esc_html__( '[email protected]', '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 '<p class="us-editor-note">' . esc_html( $text ) . '</p>';
}
+20
View File
@@ -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<string, mixed> $attributes Block attributes.
*/
public function renderAccount( array $attributes = [] ): string {
return $this->isEditorPreview() ? BlockPreview::account() : $this->accountPage->render( $attributes );
}
/**
* Renders the family (manage-children) block.
*
+4 -2
View File
@@ -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();
}
}
+3
View File
@@ -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' ] );