Merge pull request 'Show a sign-in link instead of the registration form after email confirmation' (#68) from feature/confirmed-signin-link into main
CI / Tests (PHP 8.1) (push) Successful in 37s
CI / Coding Standards (push) Successful in 2m46s
CI / Build Plugin Zip (push) Successful in 2m44s
CI / Tests (PHP 8.2) (push) Successful in 47s
CI / No Debug Code (push) Successful in 2s
CI / PHPStan (push) Successful in 2m53s
CI / Tests (PHP 8.3) (push) Successful in 2m36s
CI / Tests (PHP 8.1) (push) Successful in 37s
CI / Coding Standards (push) Successful in 2m46s
CI / Build Plugin Zip (push) Successful in 2m44s
CI / Tests (PHP 8.2) (push) Successful in 47s
CI / No Debug Code (push) Successful in 2s
CI / PHPStan (push) Successful in 2m53s
CI / Tests (PHP 8.3) (push) Successful in 2m36s
Reviewed-on: #68
This commit was merged in pull request #68.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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=`.
|
||||
|
||||
@@ -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' ],
|
||||
|
||||
@@ -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 else : ?>
|
||||
<?php if ($confirmResult === '1') : ?>
|
||||
<?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>
|
||||
<?php elseif ($confirmResult === 'expired') : ?>
|
||||
<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 === '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; ?>
|
||||
|
||||
|
||||
@@ -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]' ];
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user