Feat/optimisation user account (#773)

* removed place registration on front page

* phone field is mandatory

* added address verification on a product demand

* fix test
This commit is contained in:
Maxiloud 2025-10-22 16:14:23 +02:00 committed by Paul Andrieux
parent 69014eb69e
commit e6ace1d40f
18 changed files with 87 additions and 112 deletions

View file

@ -88,7 +88,6 @@ App\Entity\User:
name: 'APES compte lieu'
address: '@address_region_hauts_de_france'
schedule: '9h30 - 17h30'
phoneNumber: null
createdAt: <date_create_immutable('+1 month')>
# —— Users —————————————————————————————————————————————————————————————————
@ -170,6 +169,7 @@ App\Entity\User:
firstname: <firstname()>
lastname: <lastname()>
address: null
phoneNumber: '+33600000000'
avatar: 'a9a9bf49-24e4-4b3e-bdbd-86808c32939e.jpg'
# user with an address and a preferred category set

View file

@ -342,7 +342,7 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
->setFormType(PhoneNumberType::class)
->setFormTypeOptions([
'format' => PhoneNumberFormat::INTERNATIONAL,
'required' => false,
'required' => true,
])
->setHelp($i18prefix.'.field.phone.help')
;

View file

@ -8,6 +8,7 @@ use App\Controller\FlashTrait;
use App\Controller\i18nTrait;
use App\Controller\User\MyAccountAction;
use App\Entity\User;
use App\Enum\User\UserType;
use App\Exception\UserConfirmationTokenExpiredException;
use App\Exception\UserNotFoundException;
use App\Form\Type\Security\AccountCreateStep1FormType;
@ -105,6 +106,7 @@ final class AccountCreateController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
/** @var User $user */
$user = $form->getData();
$user->setType(UserType::USER);
$this->commandBus->dispatch(new AccountCreateStep2Command($user));
$this->security->login($user); // auto-log the user

View file

@ -5,11 +5,10 @@ declare(strict_types=1);
namespace App\Form\Type\Security;
use App\Entity\User;
use App\Enum\User\UserType;
use libphonenumber\PhoneNumberFormat;
use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -27,19 +26,6 @@ final class AccountCreateStep2FormType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('type', ChoiceType::class, [
'label' => 'account_create_action.account_type',
'label_attr' => ['class' => 'text-black fw-light'],
'choices' => UserType::getForFront(),
'choice_attr' => function () {
return [
'data-controller' => 'account',
'data-action' => 'click->account#choosenType',
];
},
'expanded' => true,
])
->add('firstname', TextType::class, [
'label' => 'account_create_action.firsname',
'label_attr' => ['class' => 'text-black fw-light required'],
@ -60,14 +46,14 @@ final class AccountCreateStep2FormType extends AbstractType
'required' => false,
])
->add('name', TextType::class, [
'label' => 'account_create_action.name',
'label_attr' => ['class' => 'text-black fw-light required'],
'attr' => [
'class' => 'form-control-sm input-name',
'placeholder' => 'account_create_action.name.placeholder',
],
'required' => false,
->add('phone', PhoneNumberType::class, [
'label' => 'account_create_action.phone',
'label_attr' => ['class' => 'text-black fs-6 fw-normal'],
'widget' => PhoneNumberType::WIDGET_COUNTRY_CHOICE,
'format' => PhoneNumberFormat::INTERNATIONAL,
'country_display_emoji_flag' => true,
'preferred_country_choices' => ['FR'],
'required' => true,
])
->add('plainPassword', RepeatedType::class, [
@ -118,15 +104,6 @@ final class AccountCreateStep2FormType extends AbstractType
'attr' => ['class' => 'btn btn-primary btn-sm'],
])
;
$builder->get('type')->addModelTransformer(new CallbackTransformer(
function (?UserType $enumToString) {
return $enumToString === null ? '' : $enumToString->value;
},
function (string $stringToEnum) {
return UserType::from($stringToEnum);
}
));
}
public function configureOptions(OptionsResolver $resolver): void

View file

@ -50,8 +50,9 @@ final class EditProfileFormType extends AbstractType
'label_attr' => ['class' => 'text-black fs-6 fw-normal'],
'format' => PhoneNumberFormat::INTERNATIONAL,
'widget' => PhoneNumberType::WIDGET_COUNTRY_CHOICE,
'country_display_emoji_flag' => true,
'preferred_country_choices' => ['FR'],
'required' => false,
'required' => true,
])
->add('smsNotifications', CheckboxType::class, [

View file

@ -7,6 +7,7 @@ namespace App\Message\Command\Security;
use App\Entity\User;
use App\Enum\User\UserType;
use App\MessageHandler\Command\Security\AccountCreateStep2CommandHandler;
use libphonenumber\PhoneNumber;
use Symfony\Component\Uid\Uuid;
use Webmozart\Assert\Assert;
@ -22,6 +23,7 @@ final class AccountCreateStep2Command
public ?string $firstname = null;
public ?string $name = null;
public string $plainPassword;
public PhoneNumber $phone;
public function __construct(User $user)
{
@ -33,5 +35,7 @@ final class AccountCreateStep2Command
$this->name = $user->getName();
Assert::stringNotEmpty($user->getPlainPassword());
$this->plainPassword = $user->getPlainPassword();
Assert::notNull($user->phone);
$this->phone = $user->phone;
}
}

View file

@ -9,6 +9,8 @@ use App\Entity\User;
use App\Enum\User\UserType;
use App\Message\Command\Security\AccountCreateStep2Command;
use App\Repository\UserRepository;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Webmozart\Assert\Assert;
@ -47,6 +49,11 @@ final class AccountCreateStep2CommandHandler
throw new \UnexpectedValueException('This hanlder can only create users or places.');
}
$phoneObjectToString = PhoneNumberUtil::getInstance()->format(
$message->phone,
PhoneNumberFormat::E164
);
$user->setPhoneNumber($phoneObjectToString);
$this->userManager->updatePassword($user->setPlainPassword($message->plainPassword));
$this->userManager->finalizeAccountCreateStep2($user);
$this->userManager->save($user, true);

View file

@ -1,6 +1,7 @@
{% set is_product_owner = product.owner == app.user %}
<div class="bg-light rounded-2 p-3" data-controller="calendar" data-calendar-unavailabilities-value="{{ product.getUnavailabilities()|join(',') }}">
<div class="bg-light rounded-2 p-3" data-controller="calendar"
data-calendar-unavailabilities-value="{{ product.getUnavailabilities()|join(',') }}">
<div class="row">
<div class="col">
<h5>{{ title }}</h5>
@ -71,22 +72,36 @@
</div>
{% endif %}
<div class="d-grid col mt-3">
<button class="btn border border-0 text-primary text-decoration-underline" data-action="click->calendar#resetDates" type="button">
<button class="btn border border-0 text-primary text-decoration-underline"
data-action="click->calendar#resetDates" type="button">
{{ 'templates.components.product.calendar.reset'|trans }}
</button>
</div>
{% if actionNeeded %}
{% if actionNeeded %}
<div class="d-grid col-12 mt-3">
{% if app.user is null or is_granted('borrow', product) %}
<button
id="service-request"
class="btn btn-sm btn-primary"
data-path="{{ path('app_user_service_request_new', {id: product.id}) }}"
data-action="click->calendar#serviceRequest"
disabled
>
{{ 'templates.components.product.calendar.service_request'|trans }}
</button>
{% if app.user and app.user.address is null %}
{% include 'components/product/_modal.html.twig' with {
menu_action: false,
page_type: 'article',
button: 'templates.components.product.calendar.service_request'|trans,
title: 'templates.pages.account.index.no-address-title'|trans,
message: 'templates.pages.account.index.no-address-message'|trans({
'%product%': product.type.isObject ? 'objet' : 'service'
}),
action: 'templates.pages.account.product.list.no-address-add'|trans
} %}
{% else %}
<button
id="service-request"
class="btn btn-sm btn-primary"
data-path="{{ path('app_user_service_request_new', {id: product.id}) }}"
data-action="click->calendar#serviceRequest"
disabled
>
{{ 'templates.components.product.calendar.service_request'|trans }}
</button>
{% endif %}
{% else %}
{{ form_widget(form.submit) }}
{% endif %}

View file

@ -1,7 +1,14 @@
{% if menu_action is defined and menu_action == true %}
<span class="text-decoration-none text-primary ms-2 position-relative pe-1 cursor-pointer"
data-bs-toggle="modal"
data-bs-target="#modalAddAddress">{{ button }}</span>
data-bs-toggle="modal"
data-bs-target="#modalAddAddress">{{ button }}</span>
{% elseif menu_action is defined and menu_action == false and page_type == 'article' %}
<button type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#modalAddAddress">
{{ button }}
</button>
{% else %}
<div class="d-grid col-12 col-lg-4 mx-auto my-3 order-last">
<button type="button"
@ -32,7 +39,7 @@
<button type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">
{{ (i18n_prefix ~ '.no-address-cancel')|trans }}
{{ 'templates.pages.account.product.list.no-address-cancel'|trans }}
</button>
<a href="{{ path('user_address_step1') }}"
class="btn btn-secondary">

View file

@ -31,7 +31,6 @@
{{ form_start(form, {attr: {novalidate: true}}) }}
{{ form_row(form.type) }}
<div class="user-input mt-2">
{{ form_label(form.firstname) }}
{{ form_widget(form.firstname) }}
@ -40,10 +39,10 @@
{{ form_widget(form.lastname) }}
{{ form_errors(form.lastname) }}
</div>
<div class="place-input mt-2">
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
<div class="phone-input mt-2">
{{ form_label(form.phone) }}
{{ form_widget(form.phone) }}
{{ form_errors(form.phone) }}
</div>
{% include 'components/form/_password_visibility.html.twig' with {

View file

@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests\Functional\Controller\Security;
use App\Test\ContainerRepositoryTrait;
use App\Test\KernelTrait;
use App\Tests\TestReference;
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\String\ByteString;
/**
* @see AccountCreateController
*/
final class AccountCreateActionStep2PlaceTest extends WebTestCase
{
use ContainerRepositoryTrait;
use RefreshDatabaseTrait;
use KernelTrait;
private const ROUTE = '/fr/compte/creer-mon-compte-etape-2/';
public function testUserConfirmationTokenExpiredException(): void
{
$client = self::createClient();
$client->request('GET', self::ROUTE.TestReference::USER_13_CONFIRMATION_TOKEN);
self::assertResponseRedirects();
$client->followRedirect();
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('body', 'app.controller.security.account_create_controller.step2.user_confirmation_token_expired.warning');
}
public function testFormSubmitPlaceSuccess(): void
{
$client = self::createClient();
$crawler = $client->request('GET', self::ROUTE.TestReference::USER_12_CONFIRMATION_TOKEN);
$form = $crawler->selectButton('account_create_step2_form_submit')->form();
$password = ByteString::fromRandom(13);
$client->submit($form, [
$form->getName().'[type]' => 'place',
$form->getName().'[name]' => 'My Association',
$form->getName().'[plainPassword][first]' => $password,
$form->getName().'[plainPassword][second]' => $password,
$form->getName().'[gdpr]' => 1,
]);
self::assertResponseRedirects();
$client->followRedirect();
self::assertResponseIsSuccessful();
}
}

View file

@ -33,11 +33,12 @@ final class AccountCreateActionStep2UserInvitationTest extends WebTestCase
$password = ByteString::fromRandom(13);
$client->submit($form, [
$form->getName().'[type]' => 'user',
$form->getName().'[firstname]' => 'Foo',
$form->getName().'[lastname]' => 'Bar',
$form->getName().'[plainPassword][first]' => $password,
$form->getName().'[plainPassword][second]' => $password,
$form->getName().'[phone][country]' => 'FR',
$form->getName().'[phone][number]' => '602030405',
$form->getName().'[gdpr]' => 1,
]);
self::assertResponseRedirects();

View file

@ -40,11 +40,12 @@ final class AccountCreateActionStep2UserTest extends WebTestCase
$password = ByteString::fromRandom(13);
$client->submit($form, [
$form->getName().'[type]' => 'user',
$form->getName().'[firstname]' => 'Foo',
$form->getName().'[lastname]' => 'Bar',
$form->getName().'[plainPassword][first]' => $password,
$form->getName().'[plainPassword][second]' => $password,
$form->getName().'[phone][country]' => 'FR',
$form->getName().'[phone][number]' => '602030405',
$form->getName().'[gdpr]' => 1,
]);
self::assertResponseRedirects();

View file

@ -26,6 +26,7 @@ final class UserManagerTest extends KernelTestCase
$user = new User();
$user->setEmail(ByteString::fromRandom(6)->toString().'@example.com');
$user->setPhoneNumber('+33600000000');
$user->setPassword('foo');
$userManager->save($user, true);

View file

@ -10,6 +10,7 @@ use App\Message\Command\Security\AccountCreateStep2Command;
use App\MessageHandler\Command\Security\AccountCreateStep2CommandHandler;
use App\Tests\TestReference;
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
use libphonenumber\PhoneNumberUtil;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Uid\Uuid;
@ -25,11 +26,17 @@ final class AccountCreateStep2CommandHandlerTest extends KernelTestCase
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage('This hanlder can only create users or places');
$user = (new User())
->setId(Uuid::fromString(TestReference::USER_17))
->setType(UserType::ADMIN)
->setPlainPassword('foo')
;
$phoneUtil = PhoneNumberUtil::getInstance();
$phone = $phoneUtil->parse('+33602030405', 'FR');
$user->phone = $phone;
$message = new AccountCreateStep2Command($user);
$handler($message);
}

View file

@ -30,6 +30,7 @@ final class UserRepositoryTest extends KernelTestCase
$user = new User();
$user->setEmail(ByteString::fromRandom(6)->toString().'@example.com');
$user->setPhoneNumber('+33600000000');
$user->setPassword('foo');
$repo->save($user, true);
$count = $repo->count([]);

View file

@ -75,6 +75,11 @@
<target>8 caractères minimun</target>
</trans-unit>
<trans-unit id="3Wd6Kp0" resname="account_create_action.phone">
<source>account_create_action.phone</source>
<target>Numéro de téléphone</target>
</trans-unit>
<trans-unit id="m3UMrJf" resname="account_create_action.gdpr">
<source>account_create_action.gdpr</source>
<target>Jai lu et jaccepte les <![CDATA[<a href="%link%" class="text-primary">Conditions Générales d'Utilisation</a>]]></target>

View file

@ -74,7 +74,7 @@
<trans-unit id="ftBMsiP" resname="templates.pages.account.index.no-address-message">
<source>templates.pages.account.index.no-address-message</source>
<target>Pour pouvoir créer un %product%, commencez par remplir votre adresse.</target>
<target>Pour pouvoir créer ou emprunter un %product%, commencez par remplir votre adresse.</target>
</trans-unit>
<trans-unit id="e8xvcp6" resname="templates.pages.account.index.no-address-cancel">