diff --git a/assets/js/blocks.js b/assets/js/blocks.js index 7093251..1c70401 100644 --- a/assets/js/blocks.js +++ b/assets/js/blocks.js @@ -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', diff --git a/docs/features/account-registration.md b/docs/features/account-registration.md index 4a85306..b5c0714 100644 --- a/docs/features/account-registration.md +++ b/docs/features/account-registration.md @@ -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=` 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=` 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. diff --git a/docs/features/editor-blocks.md b/docs/features/editor-blocks.md index 0f39d51..3d5af19 100644 --- a/docs/features/editor-blocks.md +++ b/docs/features/editor-blocks.md @@ -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, diff --git a/src/Auth/RegistrationPage.php b/src/Auth/RegistrationPage.php index 2196e9a..48e7cb5 100644 --- a/src/Auth/RegistrationPage.php +++ b/src/Auth/RegistrationPage.php @@ -31,9 +31,10 @@ class RegistrationPage { /** * Renders the student registration shortcode output. * - * @param array $atts Shortcode attributes (unused — reserved for future options). + * @param array $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 '

' . esc_html__( 'You already have an account and are logged in.', 'unsupervised-schedular' ) . '

'; } @@ -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=`. diff --git a/src/BlockRegistrar.php b/src/BlockRegistrar.php index fad99d7..52ccb91 100644 --- a/src/BlockRegistrar.php +++ b/src/BlockRegistrar.php @@ -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' ], diff --git a/templates/frontend/register-page.php b/templates/frontend/register-page.php index 3b16af6..2ce01ef 100644 --- a/templates/frontend/register-page.php +++ b/templates/frontend/register-page.php @@ -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 $policyForms */ @@ -21,10 +22,11 @@ if (! defined('ABSPATH')) {

+ +

+

- -

- + diff --git a/tests/Unit/Auth/RegistrationPageTest.php b/tests/Unit/Auth/RegistrationPageTest.php index bc40c38..bb96428 100644 --- a/tests/Unit/Auth/RegistrationPageTest.php +++ b/tests/Unit/Auth/RegistrationPageTest.php @@ -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(' '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(' 'expired']; + $this->stubRenderContext(); + + $html = $this->ctx['page']->render([]); + + self::assertStringContainsString('us-error', $html); + self::assertStringContainsString(' 'password123', 'display_name' => 'Ada', 'email' => 'new@b.test' ]; diff --git a/tests/Unit/BlockRegistrarTest.php b/tests/Unit/BlockRegistrarTest.php index 5a6f1af..d1aec4c 100644 --- a/tests/Unit/BlockRegistrarTest.php +++ b/tests/Unit/BlockRegistrarTest.php @@ -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'])