Files
ignacionelson 4ce6793da9 Show a new installation's administrator around, once
Setup ended by handing somebody a login form and an empty dashboard.
Everything this application can do was one menu away, and which menu was
theirs to discover.

The first time the administrator signs in to a new installation they now
land on a short ordered list of what is worth doing first — add a client,
upload a file, group the people who get the same things, choose how the
file lists and the email look, point it at a mail server, add the team,
check the scheduler — each a link straight to the screen that does it.

The list is filtered twice, and both filters matter. By permission,
because a link that answers 403 is worse than no link. And by edition:
a managed installation is not sent off to configure a mail server
somebody else runs, to create staff accounts that are not its to create,
or to check a scheduler it does not host. Those three drop out on Cloud
and the other five remain.

Two steps tick themselves, because the database can answer them: a client
exists, a file exists. Nothing else is checkable without guessing — a
theme that was never changed looks exactly like one chosen deliberately —
and a tick meaning "we assume so" is worse than no tick.

The invitation to the Discord is at the very bottom, after the list.
Somebody who has just installed this came with a job in mind, and opening
with a social invitation is the fastest way to lose them.

The marker is raised where a first administrator comes into existence —
the setup screen and `projectsend:admin`, so a container provisioned from
environment variables is welcomed too — and it is false by default, so an
installation that updates into this feature is not congratulated on an
install it finished a year ago.

RedirectToWhatsNew becomes RedirectToGreeting and answers for both: they
are the same interruption, and a second middleware on the same route
would have to know about the first to avoid arguing with it. Installing
wins; release notes for a version you never ran are the wrong greeting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:44:22 -03:00

80 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Platform\Onboarding;
use App\Models\User;
use App\Modules\Identity\StaffAccounts;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
/**
* "This installation is new — has its administrator been shown around
* yet?" The install-time twin of UpdateWelcome, and deliberately shaped
* like it: one marker, one person, one time, and an address that keeps
* working afterwards.
*
* The marker is raised explicitly at the two places a first administrator
* comes into existence — the setup screen and `projectsend:admin` — rather
* than inferred from an empty database. Inferring it would mean deciding,
* on every request forever, whether an installation is "new"; a flag
* written once at the only moment the answer is unambiguous costs one
* boolean and cannot drift. A third provisioning path added later has to
* call raise() too, which is why it lives here rather than being copied
* into both callers.
*/
class InstallationWelcome
{
public function __construct(
private readonly Settings $settings,
private readonly StaffAccounts $staff,
) {}
/**
* Record that this installation was just installed.
*/
public function raise(): void
{
$this->settings->set(Setting::GettingStartedPending, true);
}
public function pending(): bool
{
return $this->settings->get(Setting::GettingStartedPending) === true;
}
/**
* Whether this user should be taken to the page right now.
*/
public function isWaitingFor(User $user): bool
{
return $this->pending()
&& $this->mayRead($user)
&& $this->staff->mainAdministrator()?->is($user) === true;
}
/**
* Whether this user may open the page at all.
*
* Any staff member, and no capability gate: the page is a list of
* links to screens they can already reach, each one filtered to what
* that person may actually do (see QuickStart). Both editions get it —
* a managed installation still has a first client to add and a first
* file to upload, which is the whole content.
*/
public function mayRead(User $user): bool
{
return $user->isStaff();
}
/**
* Stop redirecting. The page itself keeps working — somebody who
* closed it on their way past should be able to find it again.
*/
public function dismiss(): void
{
$this->settings->set(Setting::GettingStartedPending, false);
}
}