Show a sign-in link instead of the registration form after email confirmation #68

Merged
thatguygriff merged 1 commits from feature/confirmed-signin-link into main 2026-07-18 21:00:23 +00:00
8 changed files with 111 additions and 9 deletions
+14
View File
@@ -149,6 +149,20 @@
icon: 'welcome-add-page',
keywords: ['register', 'student', 'invite'],
shortcode: 'us_student_register',
attributes: {
loginPageId: { type: 'number', default: 0 },
},
inspector: (attributes, setAttributes) => el(
PanelBody,
{ title: __('After email confirmation', 'unsupervised-schedular') },
el(PageSelect, {
label: __('Sign-in page', 'unsupervised-schedular'),
help: __('Where the sign-in link shown after a student confirms their email address sends them.', 'unsupervised-schedular'),
defaultLabel: __('WordPress login screen', 'unsupervised-schedular'),
value: attributes.loginPageId,
onChange: (loginPageId) => setAttributes({ loginPageId }),
})
),
},
{
name: 'us-scheduler/group-classes',
+1 -1
View File
@@ -50,7 +50,7 @@ confirmation token's SHA-256 hash is stored; the token expires after 48h
| Invite/admin-created student | none of these metas | allowed | full student |
- **Login gate** (`Auth\RegistrationLoginGate`): the `wp_authenticate_user` filter blocks login while the email is unconfirmed; the `user_has_cap` filter withholds `book_lesson` while `us_awaiting_approval` is set, so a confirmed-but-unapproved student only reaches the "awaiting approval" screen on the booking page.
- **Email confirmation** (`Auth\EmailConfirmationHandler` on `template_redirect`): opening the emailed `?us_confirm=<token>` link confirms the email, notifies the studio admins, and redirects back to the registration page with `?us_confirmed=1` (or `expired`).
- **Email confirmation** (`Auth\EmailConfirmationHandler` on `template_redirect`): opening the emailed `?us_confirm=<token>` link confirms the email, notifies the studio admins, and redirects back to the registration page with `?us_confirmed=1` (or `expired`). On `?us_confirmed=1` the registration page replaces the form with the confirmation message plus a "Sign in to your account" link — the configured sign-in page (block `loginPageId` / shortcode `login_page_id` attribute), falling back to the WordPress login screen. The `expired` notice keeps the form.
- **Approval** (`Auth\RegistrationApprovalController`, **Students → Pending Students**, `manage_students`): approve clears the pending flags and emails the student; reject emails them and hard-deletes the account so the email is freed to re-apply.
- **Emails**: `Auth\RegistrationMailer` sends the confirmation link, the admin heads-up, and the approval/rejection notices.
+2 -1
View File
@@ -21,7 +21,7 @@ transform.
## Block options
Three blocks have sidebar (inspector) options:
Four blocks have sidebar (inspector) options:
| Block | Attribute | Default | Effect |
|---|---|---|---|
@@ -29,6 +29,7 @@ Three blocks have sidebar (inspector) options:
| `us-scheduler/booking` | `autoRedirect` (boolean) | `false` | Send logged-out visitors straight to the login page instead of showing the link. |
| `us-scheduler/student-login` | `bookingPageId` (number) | `0` | Page the "View available lessons" link points to for logged-in visitors, and the post-login redirect target. `0` = the current page. |
| `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 the "Sign in to your account" link points to after a student confirms their email. `0` = the WordPress login screen. Shortcode equivalent: `[us_student_register 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. `0` = browse all classes. Shortcode equivalent: `[us_group_classes offering="…"]`. |
The page selects list all published pages; if a chosen page is later deleted,
+23 -2
View File
@@ -31,9 +31,10 @@ class RegistrationPage {
/**
* Renders the student registration shortcode output.
*
* @param array<int|string, mixed> $atts Shortcode attributes (unused — reserved for future options).
* @param array<int|string, mixed> $atts Block attributes (`loginPageId`) or
* shortcode attributes (`login_page_id`).
*/
public function render( array $atts ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
public function render( array $atts ): string {
if ( is_user_logged_in() ) {
return '<p>' . esc_html__( 'You already have an account and are logged in.', 'unsupervised-schedular' ) . '</p>';
}
@@ -60,6 +61,9 @@ class RegistrationPage {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display flag, not a state change.
$confirmResult = sanitize_key( Val::string( wp_unslash( $_GET['us_confirmed'] ?? '' ) ) );
// Where the post-confirmation prompt sends students to sign in.
$loginUrl = $this->loginUrl( Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 ) );
$policyForms = $this->signupPolicies();
$canRegister = $open || ( null !== $invite && $invite->isAcceptable( current_time( 'mysql' ) ) );
@@ -179,6 +183,23 @@ class RegistrationPage {
return self::RESULT_CONFIRM;
}
/**
* URL the post-confirmation sign-in link points to: the chosen login page
* when one is configured (and still exists), otherwise the WordPress login
* screen.
*/
private function loginUrl( int $loginPageId ): string {
if ( $loginPageId > 0 ) {
$url = get_permalink( $loginPageId );
if ( is_string( $url ) ) {
return $url;
}
}
return wp_login_url();
}
/**
* Build the email-confirmation URL for a raw token: the configured
* registration page (falling back to the home page) with `?us_confirm=`.
+6 -1
View File
@@ -104,7 +104,12 @@ class BlockRegistrar {
],
'us-scheduler/student-register' => [
'render' => [ $this, 'renderRegistration' ],
'attributes' => [],
'attributes' => [
'loginPageId' => [
'type' => 'number',
'default' => 0,
],
],
],
'us-scheduler/group-classes' => [
'render' => [ $this, 'renderGroupClasses' ],
+5 -3
View File
@@ -12,6 +12,7 @@ if (! defined('ABSPATH')) {
* @var bool $open Whether open (self-approval) registration is enabled.
* @var string $successType '' | 'invite' (created + logged in) | 'confirm' (check email).
* @var string $confirmResult '' | '1' (email confirmed) | 'expired'.
* @var string $loginUrl Where the post-confirmation sign-in link points.
* @var string $error
* @var list<array{policy: \Unsupervised\Schedular\Policy\Policy, version: \Unsupervised\Schedular\Policy\PolicyVersion}> $policyForms
*/
@@ -21,10 +22,11 @@ if (! defined('ABSPATH')) {
<p class="us-success"><?php esc_html_e('Your account has been created and you are now logged in.', 'unsupervised-schedular'); ?></p>
<?php elseif ($successType === 'confirm') : ?>
<p class="us-success"><?php esc_html_e('Your account has been created. Check your email for a link to confirm your address — once you do, a studio admin will review and approve your account.', 'unsupervised-schedular'); ?></p>
<?php elseif ($confirmResult === '1') : ?>
<p class="us-success"><?php esc_html_e('Thanks — your email is confirmed. Your account is now awaiting studio approval; we will email you when it is ready.', 'unsupervised-schedular'); ?></p>
<p><a href="<?php echo esc_url($loginUrl); ?>"><?php esc_html_e('Sign in to your account', 'unsupervised-schedular'); ?></a></p>
<?php else : ?>
<?php if ($confirmResult === '1') : ?>
<p class="us-success"><?php esc_html_e('Thanks — your email is confirmed. Your account is now awaiting studio approval; we will email you when it is ready.', 'unsupervised-schedular'); ?></p>
<?php elseif ($confirmResult === 'expired') : ?>
<?php if ($confirmResult === 'expired') : ?>
<p class="us-error" role="alert"><?php esc_html_e('That confirmation link is invalid or has expired. Please contact the studio.', 'unsupervised-schedular'); ?></p>
<?php endif; ?>
+56
View File
@@ -57,9 +57,20 @@ class RegistrationPageTest extends TestCase
protected function tearDown(): void
{
$_POST = [];
$_GET = [];
parent::tearDown();
}
/** Stub everything render() needs on a logged-out GET request. */
private function stubRenderContext(): void
{
Functions\when('is_user_logged_in')->justReturn(false);
Functions\when('sanitize_key')->alias(static fn ($v) => strtolower((string) $v));
Functions\when('wp_login_url')->justReturn('http://home.test/wp-login.php');
Functions\when('wp_nonce_field')->justReturn('');
$this->ctx['settings']->shouldReceive('openRegistrationEnabled')->andReturn(true);
}
private function submit(?Invite $invite, bool $open): string
{
$method = new \ReflectionMethod(RegistrationPage::class, 'handleSubmit');
@@ -120,6 +131,51 @@ class RegistrationPageTest extends TestCase
self::assertNotSame('', $result);
}
public function testConfirmedEmailShowsSignInLinkInsteadOfForm(): void
{
$_GET = ['us_confirmed' => '1'];
$this->stubRenderContext();
$html = $this->ctx['page']->render([]);
self::assertStringContainsString('us-success', $html);
self::assertStringContainsString('http://home.test/wp-login.php', $html);
self::assertStringNotContainsString('<form', $html);
}
public function testConfirmedSignInLinkUsesConfiguredLoginPage(): void
{
$_GET = ['us_confirmed' => '1'];
$this->stubRenderContext();
Functions\expect('get_permalink')->once()->with(7)->andReturn('http://home.test/sign-in/');
$html = $this->ctx['page']->render(['login_page_id' => 7]);
self::assertStringContainsString('http://home.test/sign-in/', $html);
self::assertStringNotContainsString('wp-login.php', $html);
}
public function testRegistrationFormRendersWithoutConfirmationFlag(): void
{
$this->stubRenderContext();
$html = $this->ctx['page']->render([]);
self::assertStringContainsString('<form', $html);
self::assertStringNotContainsString('us-success', $html);
}
public function testExpiredConfirmationStillShowsForm(): void
{
$_GET = ['us_confirmed' => 'expired'];
$this->stubRenderContext();
$html = $this->ctx['page']->render([]);
self::assertStringContainsString('us-error', $html);
self::assertStringContainsString('<form', $html);
}
public function testRejectsWhenARequiredPolicyIsUnaccepted(): void
{
$_POST = [ 'password' => 'password123', 'display_name' => 'Ada', 'email' => '[email protected]' ];
+4 -1
View File
@@ -125,7 +125,10 @@ class BlockRegistrarTest extends TestCase
['bookingPageId', 'autoRedirect'],
array_keys($registered['us-scheduler/student-login']['attributes'])
);
self::assertSame([], $registered['us-scheduler/student-register']['attributes']);
self::assertSame(
['loginPageId'],
array_keys($registered['us-scheduler/student-register']['attributes'])
);
self::assertSame(
['offeringId'],
array_keys($registered['us-scheduler/group-classes']['attributes'])