Added newsletter consent and term/privacy checkbox to registration

This commit is contained in:
Constantin Graf
2024-05-14 17:02:28 +02:00
committed by Constantin Graf
parent 11af9fab7e
commit 0448ebc180
9 changed files with 129 additions and 48 deletions

View File

@@ -6,6 +6,7 @@ namespace App\Actions\Fortify;
use App\Enums\Role; use App\Enums\Role;
use App\Enums\Weekday; use App\Enums\Weekday;
use App\Events\NewsletterRegistered;
use App\Models\Organization; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use App\Service\TimezoneService; use App\Service\TimezoneService;
@@ -49,6 +50,9 @@ class CreateNewUser implements CreatesNewUsers
], ],
'password' => $this->passwordRules(), 'password' => $this->passwordRules(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
'newsletter_consent' => [
'boolean',
],
])->validate(); ])->validate();
$timezone = 'UTC'; $timezone = 'UTC';
@@ -56,7 +60,7 @@ class CreateNewUser implements CreatesNewUsers
$timezone = $input['timezone']; $timezone = $input['timezone'];
} }
return DB::transaction(function () use ($input, $timezone) { $user = DB::transaction(function () use ($input, $timezone) {
return tap(User::create([ return tap(User::create([
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => $input['email'],
@@ -67,6 +71,13 @@ class CreateNewUser implements CreatesNewUsers
$this->createTeam($user); $this->createTeam($user);
}); });
}); });
$newsletterConsent = isset($input['newsletter_consent']) && (bool) $input['newsletter_consent'];
if ($newsletterConsent) {
NewsletterRegistered::dispatch($input['name'], $input['email'], $user->getKey());
}
return $user;
} }
/** /**

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
class NewsletterRegistered
{
use Dispatchable;
public string $name;
public string $email;
public string $id;
/**
* Create a new event instance.
*/
public function __construct(string $name, string $email, string $id)
{
$this->name = $name;
$this->email = $email;
$this->id = $id;
}
}

View File

@@ -22,6 +22,8 @@ use Brick\Money\Currency;
use Brick\Money\ISOCurrencyProvider; use Brick\Money\ISOCurrencyProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Inertia\Inertia;
use Laravel\Fortify\Fortify;
use Laravel\Jetstream\Actions\UpdateTeamMemberRole; use Laravel\Jetstream\Actions\UpdateTeamMemberRole;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
@@ -52,6 +54,13 @@ class JetstreamServiceProvider extends ServiceProvider
Jetstream::useTeamModel(Organization::class); Jetstream::useTeamModel(Organization::class);
Jetstream::useTeamInvitationModel(OrganizationInvitation::class); Jetstream::useTeamInvitationModel(OrganizationInvitation::class);
app()->singleton(UpdateTeamMemberRole::class, UpdateMemberRole::class); app()->singleton(UpdateTeamMemberRole::class, UpdateMemberRole::class);
Fortify::registerView(function () {
return Inertia::render('Auth/Register', [
'terms_url' => config('auth.terms_url'),
'privacy_policy_url' => config('auth.privacy_policy_url'),
'newsletter_consent' => config('auth.newsletter_consent'),
]);
});
} }
/** /**

View File

@@ -117,4 +117,10 @@ return [
'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')), 'super_admins' => ! is_string(env('SUPER_ADMINS', null)) ? [] : explode(',', env('SUPER_ADMINS')),
'terms_url' => env('TERMS_URL'),
'privacy_policy_url' => env('PRIVACY_POLICY_URL'),
'newsletter_consent' => env('NEWSLETTER_CONSENT', false),
]; ];

View File

@@ -60,9 +60,8 @@ return [
*/ */
'features' => [ 'features' => [
// Features::termsAndPrivacyPolicy(), Features::termsAndPrivacyPolicy(),
Features::profilePhotos(), Features::profilePhotos(),
// Features::api(),
Features::teams(['invitations' => true]), Features::teams(['invitations' => true]),
Features::accountDeletion(), Features::accountDeletion(),
], ],

View File

@@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'scheme' => 'https',
],
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
];

View File

@@ -15,15 +15,21 @@ const form = useForm({
password_confirmation: '', password_confirmation: '',
terms: false, terms: false,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? null, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? null,
newsletter_consent: false,
}); });
const submit = () => { const submit = () => {
form.post(route('register'), { form.post(route('register'), {
onFinish: () => form.reset('password', 'password_confirmation'), onSuccess: () => {
form.reset('password', 'password_confirmation');
},
}); });
}; };
const page = usePage<{ const page = usePage<{
terms_url: string | null;
privacy_policy_url: string | null;
newsletter_consent: boolean;
jetstream: { jetstream: {
hasTermsAndPrivacyPolicyFeature: boolean; hasTermsAndPrivacyPolicyFeature: boolean;
}; };
@@ -111,29 +117,32 @@ const page = usePage<{
</div> </div>
<div <div
v-if="page.props.jetstream.hasTermsAndPrivacyPolicyFeature" v-if="
page.props.jetstream.hasTermsAndPrivacyPolicyFeature &&
page.props.terms_url !== null &&
page.props.privacy_policy_url !== null
"
class="mt-4"> class="mt-4">
<InputLabel for="terms"> <InputLabel for="terms">
<div class="flex items-center"> <div class="flex items-center">
<Checkbox <Checkbox
id="terms" id="terms"
v-model:checked="form.terms" v-model:checked="form.terms"
name="terms" name="terms" />
required />
<div class="ms-2"> <div class="ms-2">
I agree to the I agree to the
<a <a
target="_blank" target="_blank"
:href="route('terms.show')" :href="page.props.terms_url"
class="underline text-sm text-muted hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" class="underline text-sm text-muted hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Terms of Service</a >Terms of Service</a
> >
and and
<a <a
target="_blank" target="_blank"
:href="route('policy.show')" :href="page.props.privacy_policy_url"
class="underline text-sm text-muted hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" class="underline text-sm text-muted hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Privacy Policy</a >Privacy Policy</a
> >
</div> </div>
@@ -142,6 +151,25 @@ const page = usePage<{
</InputLabel> </InputLabel>
</div> </div>
<div class="mt-4" v-if="page.props.newsletter_consent">
<InputLabel for="newsletter_consent">
<div class="flex items-center">
<Checkbox
id="newsletter_consent"
v-model:checked="form.newsletter_consent"
name="newsletter_consent" />
<div class="ms-2">
I agree to receive emails about product related
updates
</div>
</div>
<InputError
class="mt-2"
:message="form.errors.newsletter_consent" />
</InputLabel>
</div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
<Link <Link
:href="route('login')" :href="route('login')"

View File

@@ -51,7 +51,7 @@ const verificationLinkSent = computed(
<div> <div>
<Link <Link
:href="route('profile.show')" :href="route('profile.show')"
class="underline text-sm text-muted hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> class="underline text-sm text-muted hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Edit Profile</Link Edit Profile</Link
> >

View File

@@ -5,10 +5,12 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Enums\Role; use App\Enums\Role;
use App\Events\NewsletterRegistered;
use App\Models\Membership; use App\Models\Membership;
use App\Models\User; use App\Models\User;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
use Tests\TestCase; use Tests\TestCase;
@@ -41,6 +43,11 @@ class RegistrationTest extends TestCase
public function test_new_users_can_register(): void public function test_new_users_can_register(): void
{ {
// Arrange
Event::fake([
NewsletterRegistered::class,
]);
// Act // Act
$response = $this->post('/register', [ $response = $this->post('/register', [
'name' => 'Test User', 'name' => 'Test User',
@@ -51,6 +58,7 @@ class RegistrationTest extends TestCase
]); ]);
// Assert // Assert
$response->assertValid();
$this->assertAuthenticated(); $this->assertAuthenticated();
$response->assertRedirect(RouteServiceProvider::HOME); $response->assertRedirect(RouteServiceProvider::HOME);
$user = User::where('email', 'test@example.com')->firstOrFail(); $user = User::where('email', 'test@example.com')->firstOrFail();
@@ -60,6 +68,34 @@ class RegistrationTest extends TestCase
$this->assertSame(true, $organization->personal_team); $this->assertSame(true, $organization->personal_team);
$member = Membership::query()->whereBelongsTo($user, 'user')->whereBelongsTo($organization, 'organization')->firstOrFail(); $member = Membership::query()->whereBelongsTo($user, 'user')->whereBelongsTo($organization, 'organization')->firstOrFail();
$this->assertSame(Role::Owner->value, $member->role); $this->assertSame(Role::Owner->value, $member->role);
Event::assertNotDispatched(NewsletterRegistered::class);
}
public function test_new_users_can_consent_to_newsletter_during_registration(): void
{
// Arrange
Event::fake([
NewsletterRegistered::class,
]);
// Act
$response = $this->post('/register', [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password',
'password_confirmation' => 'password',
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
'newsletter_consent' => true,
]);
// Assert
$response->assertValid();
$this->assertAuthenticated();
$response->assertRedirect(RouteServiceProvider::HOME);
$user = User::where('email', 'test@example.com')->firstOrFail();
$this->assertSame('Test User', $user->name);
$this->assertSame('UTC', $user->timezone);
Event::assertDispatched(NewsletterRegistered::class);
} }
public function test_new_users_can_register_and_frontend_can_send_timezone_for_user(): void public function test_new_users_can_register_and_frontend_can_send_timezone_for_user(): void