Направление работ: Mobile Код раздела: 1.4 Авторизация > Восстановление пароля Статус: Готов Ответственный: Панкова Л.А. Версия: 0.1 Обновлено: 14.07.26
1. Назначение
Раздел описывает восстановление пароля пользователем мобильного приложения, если пользователь не может войти по текущим учетным данным.
Интерфейс должен провести пользователя от ввода телефона или почты до подтверждения владения аккаунтом кодом и установки нового пароля.
Документ предназначен для проектирования frontend-сценария. Он не задает backend-логику поиска аккаунта, отправки и проверки кода или сохранения нового пароля.
Раздел зависит от Back: 1.1 Организация аккаунтов и Back: 1.1.3 Восстановление.
2. Ключевые понятия
Восстановление доступа — процесс подтверждения владения аккаунтом через данные для сброса пароля и последующей смены пароля.
Восстановление пароля — пользовательский сценарий в мобильном приложении, в котором пользователь подтверждает доступ к аккаунту и задает новый пароль.
Способ восстановления — выбор идентификатора: телефон или почта.
Идентификатор — телефон или подтвержденный email, по которому пользователь инициирует восстановление пароля.
Код сброса пароля — цифровой код из 6 цифр, который пользователь вводит для подтверждения права задать новый пароль.
Маскирование контакта — скрытие части номера телефона или адреса электронной почты при отображении канала, на который отправлен код.
3. Scope
Входит
- S1: Инициация восстановления пароля: открытие экрана восстановления, выбор способа по телефону или почте, ввод идентификатора, отправка запроса и отображение ошибки, если пользователь не найден.
- S2: Подтверждение восстановления кодом: ввод кода из письма или СМС, отображение замаскированного контакта, таймера повторной отправки и действия для запроса нового кода.
- S3: Установка нового пароля: ввод нового пароля и повтора пароля, управление видимостью значений, сохранение пароля и переход к следующему продуктовому сценарию после успешного восстановления.
- S4: Состояния восстановления: неактивные действия до заполнения обязательных полей, ошибки ввода идентификатора/кода/пароля, ограничения повторной отправки и возврат назад в рамках flow.
Не входит
- Mobile: 1.2 — Регистрация
- Mobile: 1.3 — Вход
- Mobile: 1.3.1 — Вход по код-паролю
- Back: 1.1.3 — backend-логика поиска аккаунта, отправки/проверки кода, лимитов и сохранения нового пароля
- WebLk: 1.3 / Widget: 3.3 / Catalog: 3.3 — восстановление пароля в других интерфейсах
- Mobile: 2.1 — Поиск > Список сфер
4. BR
Указаны в пункте 4 раздела Back: 1.1.3 Восстановление.
5. Функциональные требования (UC + FR)
FR — functional requirement: что интерфейс должен уметь делать в рамках конкретного пользовательского сценария.
| ID | Сценарий | Функциональное требование | BR | Вес | Статус |
|---|---|---|---|---|---|
| S1: #FR1.4-1 | Пользователь открывает восстановление пароля | Интерфейс показывает экран восстановления с выбором способа восстановления по телефону или по почте. | 1 | Реализовано | |
| S1: #FR1.4-2 | Пользователь выбирает способ восстановления | Интерфейс меняет поле идентификатора и доступный тип ввода в зависимости от выбранного способа: телефон или почта. | 1 | Реализовано | |
| S1: #FR1.4-3 | Пользователь вводит телефон | Интерфейс принимает телефон, применяет маску ввода и не позволяет продолжить, пока номер не заполнен до допустимого формата. | 1 | Реализовано | |
| S1: #FR1.4-4 | Пользователь вводит почту | Интерфейс принимает email и не позволяет продолжить, пока значение не заполнено в допустимом формате. | 1 | Реализовано | |
| S1: #FR1.4-5 | Пользователь отправляет запрос восстановления | Интерфейс отправляет выбранный идентификатор и источник Mobile на восстановление пароля, после успешного ответа переводит пользователя на шаг подтверждения кодом. | 1 | Реализовано | |
| S1: #FR1.4-6 | По введенному телефону пользователь не найден | Интерфейс показывает ошибку под полем телефона и оставляет пользователя на экране восстановления с возможностью исправить значение. | 2 | Частично \ #OQ1.4-2 | |
| S1: #FR1.4-7 | По введенной почте пользователь не найден | Интерфейс показывает ошибку под полем почты и оставляет пользователя на экране восстановления с возможностью исправить значение. | 2 | Частично \ #OQ1.4-2 | |
| S2: #FR1.4-8 | Пользователь попадает на подтверждение кодом из письма | Интерфейс показывает модальное окно подтверждения, замаскированный email, поле ввода кода и таймер до возможности запросить новый код. | 1 | Реализовано | |
| S2: #FR1.4-9 | Пользователь попадает на подтверждение кодом из СМС | Интерфейс показывает модальное окно подтверждения, замаскированный номер телефона, поле ввода кода и таймер до возможности запросить новый код. | 1 | Реализовано | |
| S2: #FR1.4-10 | Пользователь вводит код сброса пароля | Интерфейс принимает код в поле подтверждения и отправляет его на проверку после заполнения требуемого количества символов. | 1 | Частично \ #OQ1.4-1 | |
| S2: #FR1.4-11 | Код сброса пароля подтвержден | Интерфейс закрывает модальное окно подтверждения и переводит пользователя на экран установки нового пароля. | 1 | Реализовано | |
| S2: #FR1.4-12 | Пользователь ввел неверный код | Интерфейс показывает ошибку проверки кода и позволяет повторить ввод. | 2 | Частично \ #OQ1.4-5 | |
| S2: #FR1.4-13 | Срок действия кода истек | Интерфейс сообщает, что код устарел, и предлагает запросить новый код после доступности повторной отправки. | 2 | Не реализовано \ #OQ1.4-6 | |
| S2: #FR1.4-14 | Пользователь ожидает повторную отправку кода | Интерфейс показывает таймер до повторной отправки и не позволяет запросить новый код до завершения таймера. | 2 | Реализовано | |
| S2: #FR1.4-15 | Пользователь запрашивает новый код | Интерфейс позволяет запросить новый код после завершения таймера и обновляет состояние подтверждения после ответа. | 2 | Реализовано | |
| S3: #FR1.4-16 | Пользователь попадает на установку нового пароля | Интерфейс показывает экран установки пароля с полями нового пароля и повтора пароля. | 1 | Реализовано | |
| S3: #FR1.4-17 | Пользователь вводит новый пароль | Интерфейс скрывает значение пароля по умолчанию и позволяет пользователю показать или снова скрыть введенное значение. | 2 | Реализовано | |
| S3: #FR1.4-18 | Пользователь подтверждает новый пароль | Интерфейс сравнивает пароль и повтор пароля и не позволяет сохранить пароль, если значения не совпадают. | 1 | Реализовано | |
| S3: #FR1.4-19 | Пользователь сохраняет новый пароль | Интерфейс отправляет новый пароль на сохранение только после успешной проверки кода и заполнения обязательных полей. | 1 | Реализовано \ #OQ1.4-4 | |
| S3: #FR1.4-20 | Новый пароль успешно сохранен | Интерфейс завершает восстановление пароля и переводит пользователя к следующему продуктовому сценарию: Mobile 2.1 Поиск > Список сфер. | 1 | Реализовано | |
| S4: #FR1.4-21 | Пользователь не заполнил обязательные поля | Интерфейс оставляет основное действие неактивным до заполнения обязательных полей на текущем шаге. | 1 | Реализовано | |
| S4: #FR1.4-22 | Пользователь возвращается на предыдущий шаг | Интерфейс позволяет вернуться назад из экранов и модального окна восстановления без перехода в соседние сценарии. | 2 | Реализовано | |
| S4: #FR1.4-23 | Превышено ограничение на повторные запросы кода | Интерфейс показывает состояние временного ограничения и не позволяет повторно инициировать восстановление до истечения ограничения, если такая блокировка была применена. | 2 | Частично \ #OQ1.4-3 | |
| S4: #FR1.4-24 | Во время восстановления нет интернет-соединения или получена техническая ошибка | Интерфейс показывает общее сообщение о проблеме и позволяет повторить действие без потери актуального контекста шага. | 2 | Частично \ #OQ1.4-7 | |
| S4: #FR1.4-25 | Пользователь повторно нажимает действие во время запроса | Интерфейс предотвращает дублирование запроса: повторное нажатие недоступно или игнорируется до получения ответа. | 2 | Реализовано |
6. Открытые вопросы
| ID | Источник | Вопрос | Кому | Статус | Рекомендации | Итоговое решение |
|---|---|---|---|---|---|---|
| #OQ1.4-1 | Код FR1.4-10 | Требование говорит: код сброса — цифровой код из 6 цифр (см. п.2), отправка на проверку после ввода требуемого числа символов. В коде длина зашита _kCodeLength = 4; Pinput на 4 ячейки с авто-отправкой при 4 символах — расходится с «6 цифр» из глоссария. \ forgot_password_screen.dart:23,780,538-548 | Бизнес/Разработка | Open | Критично: если бэкенд шлёт 6-значные коды, поле на 4 ячейки авто-отправит первые 4 символа и валидация не пройдёт никогда. Фактическую длину — согласовать с Back: 1.1.3. (А, рекоменд.) привести _kCodeLength к реальной длине, идеально — не хардкодить, а брать из ответа SendRestorePasswordCodeMobile; (Б) если 4 верно — поправить глоссарий BRD (правит владелец BRD). | привести _kCodeLength к реальной длине и захардкодить |
| #OQ1.4-2 | Код FR1.4-6, FR1.4-7 | Требование говорит: при «пользователь не найден» — ошибка под полем телефона/почты, пользователь остаётся на экране. В коде ветка accountNotFound пишет текст в единую центрированную область под обоими полями (не под конкретным полем); при пустом shortMessage показывает generic «Не удалось отправить код…», а не «пользователь не найден». \ forgot_password_screen.dart:147-154,207-241 | Бизнес/Разработка | Open | Копирайт и размещение ошибки — под вопросом. (А) показывать явный «Пользователь не найден» под активным полем; (Б) оставить как есть, если продукт согласен, что случай покрывается серверным shortMessage без привязки к полю. Текст согласовать с Back: 1.1.3. | От бизнеса: доработка, вес 2. |
| #OQ1.4-3 | Код FR1.4-23 | Требование говорит: при превышении лимита повторных запросов — состояние временного ограничения, нельзя повторно инициировать до истечения. В коде на экране инициации tooManyRequests только показывает текст; кнопка «Продолжить» остаётся активной, кулдаун-блокировки нет (таймер есть лишь в модалке повторной отправки). \ forgot_password_screen.dart:147-154 (в модалке — :492-497) | Бизнес/Разработка | Open | Решение: требуется ли лок на экране инициации. (А, рекоменд.) ввести кулдаун-блокировку кнопки по timeout из ответа, как в модалке; (Б) оставить как есть, если продукт согласен на текст без блокировки. Значение лимита/окна — с Back: 1.1.3. | оставить как есть, кулдаун на сервере |
| #OQ1.4-4 | Код вне BRD | Что нашли: клиент сам enforce-ит минимум 6 символов пароля (_minPasswordLength = 6, текст «Минимум 6 символов»), иных правил сложности нет. BRD 1.4 отдаёт правила пароля бэкенду (п.1 «Не входит»). \ set_password_screen.dart:43,230-231 | Бизнес/Разработка | Open | Нужно подтверждение, что клиентский порог совпадает с политикой Back; согласовать с Back: 1.1.3. (А) синхронизировать минимум/правила с Back и вынести в конфиг; (Б) убрать клиентский порог, полагаясь на ответ SetNewPassword. FR1.4-19 функционально работает — вопрос про полноту правил. | согласовать с Back: 1.1.3. (А) синхронизировать минимум/правила с Back и вынести в конфиг |
| #OQ1.4-5 | Код FR1.4-12 | Требование говорит: при неверном коде — ошибка и повтор, если лимит попыток не был исчерпан. В коде invalidCode → тряска + снек, поле очищается, повтор разрешён бесконечно; состояния «лимит попыток исчерпан» на валидации нет (enum: ok/invalidCode/invalidSource/expiredCode/accountNotFound). Условие «если лимит не исчерпан» не реализовано. \ restore_password_result.dart:55-61; forgot_password_screen.dart:558-565 | Бизнес/Разработка | Open | (А, рекоменд.) согласовать с Back (1.1.3) поведение при исчерпании попыток (доп. состояние/ошибка) и обработать отдельно — блокировать ввод или предлагать новый код; (Б) если лимит целиком на бэкенде — убрать оговорку «если лимит не исчерпан» из BRD (правит владелец BRD). | От бизнеса: уточнили БРД. |
| #OQ1.4-6 | Код FR1.4-13 | Требование говорит: при истёкшем коде — «код устарел» и предложить запросить новый после доступности повторной отправки. В коде expiredCode обрабатывается идентично invalidCode (тряска + серверный shortMessage); отдельного «код устарел» и проактивного предложения нет — повторная отправка по-прежнему заблокирована таймером. Доп.: invalidSource не разбирается → generic «Ошибка проверки кода». \ forgot_password_screen.dart:558-580 | Бизнес/Разработка | Open | (А, рекоменд.) развести ветки — для expiredCode показывать «Код устарел, запросите новый» и разблокировать/подсветить повторную отправку; для invalidSource — осмысленный текст; (Б) подтвердить, что серверный shortMessage покрывает оба случая. Связать с OQ1.4-5. | Доработка: привести код к требованию, вес 1. |
| #OQ1.4-7 | Код FR1.4-24 | Требование говорит: при отсутствии сети/техошибке — общее сообщение, повтор без потери контекста шага. В коде контекст шага сохраняется (остаёмся на экране/в модалке), но пользователю показывается сырой текст исключения (shortMessage/description с бэкенда), без общего «проблема с соединением, повторите». \ forgot_password_screen.dart:55-60,150-154,513-528 | Разработка | Open | Контекст шага не теряется, вопрос только в тексте — продуктового выбора нет. Закрываю сам: маппить транспортные/технические ошибки в единое дружелюбное сообщение, детали — в лог/Sentry → тикет в Bitrix. | Доработка. |
| #OQ1.4-8 | Код вне BRD | Что нашли: в репозитории два флоу восстановления — активный auth/forgot_password и мёртвый restore_password (RestorePasswordScreen/RestorePasswordRoute зарегистрирован в роутере, но нигде не пушится). Дубль-реализация, риск расхождения при доработках. \ restore_password/widget/resotre_password_screen.dart:11; core/utils/app_router.dart:305 | Разработка | Open | Мёртвый код, продуктового вопроса нет. Закрываю сам: удалить restore_password и снять RestorePasswordRoute из роутера → тикет в Bitrix. Если ветка планируется — альтернатива: задокументировать назначение. | Доработка. |
| #OQ1.4-9 | Код вне BRD | Что нашли: print-логи в потоке восстановления печатают логин (PII) и полный JSON ответов, включая JWT-токены (ValidateRestorePasswordCodeMobile/SetNewPassword) — в проде утечка чувствительных данных в логи. \ auth_repository.dart:235,264,306,338 | Разработка | Open | Явный риск безопасности, продуктового вопроса нет. Закрываю сам (приоритет): удалить debug-print (или загейтить за kDebugMode), из логов исключить токены и логин → тикет в Bitrix. | Доработка. |
Вопросы по сверке с кодом
BRD 1.4 v0.1 · booking.mobile.flutter · commit dad0c65/dev · 2026-07-10 · якорь
lib/src/feature/auth/forgot_password/(вход:login_screen.dart:441→ForgotPasswordRoute→SetPasswordRoute;restore_password/*— мёртвая ветка) Сводка: вопросов 9 (бизнесовых 4 / программных 5) · подтверждено без вопросов 18 (FR1.4-1, FR1.4-2, FR1.4-3, FR1.4-4, FR1.4-5, FR1.4-8, FR1.4-9, FR1.4-11, FR1.4-14, FR1.4-15, FR1.4-16, FR1.4-17, FR1.4-18, FR1.4-19, FR1.4-20, FR1.4-21, FR1.4-22, FR1.4-25) · отрезано находок обратного прохода 4 (аналитика AppMetrica, хаптик-фидбек, shake-анимация при неверном коде, нормализация вставленного 11-значного номера) Под особым вниманием: длина кода 4 vs «6 цифр» в BRD (Б1); отсутствие обработки лимита попыток на валидации кода (П1);tooManyRequestsбез блокировки на экране инициации (Б3)
Бизнесовые
| № | Связано с | Что в BRD | Что в коде | Где в коде |
|---|---|---|---|---|
| Б1 | FR1.4-10 (+ п.2 «Код сброса пароля») | Код сброса пароля — цифровой код из 6 цифр; отправка на проверку после ввода требуемого числа символов | Длина зашита _kCodeLength = 4; Pinput на 4 ячейки с авто-отправкой при 4 символах — расходится с «6 цифр» из глоссария. Нужно решение: 4 или 6 (клиент обязан совпасть с длиной кода бэкенда, иначе валидация не отправится/сорвётся) | forgot_password_screen.dart:23, :780, :538-548 |
| Б2 | FR1.4-6, FR1.4-7 | При «пользователь не найден» — ошибка под полем телефона/почты, пользователь остаётся на экране | Ветка accountNotFound пишет текст в единую центрированную область под обоими полями (не под конкретным полем); при пустом shortMessage показывает generic «Не удалось отправить код…», а не «пользователь не найден» — нужно подтверждение копирайта и размещения | forgot_password_screen.dart:147-154, :207-241 |
| Б3 | FR1.4-23 | При превышении лимита повторных запросов — состояние временного ограничения, нельзя повторно инициировать до истечения | На экране инициации tooManyRequests только показывает текст; кнопка «Продолжить» остаётся активной, кулдаун-блокировки нет (таймер есть лишь в модалке повторной отправки) — нужно решение: требуется ли лок на экране инициации | forgot_password_screen.dart:147-154 (в модалке — :492-497) |
| Б4 | вне BRD (п.1: сохранение пароля — Back) | — | Клиент сам enforce-ит минимум 6 символов пароля (_minPasswordLength = 6, текст «Минимум 6 символов»); иных правил сложности нет. BRD 1.4 отдаёт правила пароля бэкенду — нужно подтверждение, что клиентский порог совпадает с политикой Back | set_password_screen.dart:43, :230-231 |
Рекомендация / варианты решения
- Длина кода 4 vs 6 (Б1) — критично: если бэкенд шлёт 6-значные коды, Pinput на 4 ячейки авто-отправит первые 4 символа и валидация не пройдёт никогда. Вариант А (реком.): уточнить фактическую длину у Back (1.1.3) и привести
_kCodeLengthк ней, идеально — не хардкодить, а брать из ответаSendRestorePasswordCodeMobile. Вариант Б: если 4 верно — поправить глоссарий BRD (правит владелец BRD, не мы). - Копирайт и размещение ошибок (Б2, Б3) — Вариант А: для
accountNotFoundпоказывать явный «Пользователь не найден» под активным полем; дляtooManyRequestsна экране инициации ввести кулдаун-блокировку кнопки поtimeoutиз ответа, как в модалке. Вариант Б: оставить как есть, если продукт согласен, что «не найден» и «лимит» покрываются сервернымshortMessageбез блокировки. - Клиентская политика пароля (Б4) — Вариант А: синхронизировать минимум/правила с Back и вынести в конфиг. Вариант Б: убрать клиентский порог, полагаясь на ответ
SetNewPassword.
Программные
| № | Связано с | Что в BRD | Что в коде | Где в коде |
|---|---|---|---|---|
| П1 | FR1.4-12 | При неверном коде — ошибка и повтор, если лимит попыток не был исчерпан | invalidCode → тряска + снек, поле очищается, повтор разрешён бесконечно; состояния «лимит попыток исчерпан» на валидации нет (enum содержит только ok/invalidCode/invalidSource/expiredCode/accountNotFound). Условие «если лимит не исчерпан» не реализовано | restore_password_result.dart:55-61; forgot_password_screen.dart:558-565 |
| П2 | FR1.4-13 | При истёкшем коде — сообщить «код устарел» и предложить запросить новый после доступности повторной отправки | expiredCode обрабатывается идентично invalidCode (тряска + серверный shortMessage); отдельного «код устарел» и проактивного предложения запросить новый нет — повторная отправка по-прежнему заблокирована таймером. Доп.: invalidSource не разбирается → generic «Ошибка проверки кода» | forgot_password_screen.dart:558-580 |
| П3 | FR1.4-24 | При отсутствии сети/техошибке — общее сообщение, повтор без потери контекста шага | Контекст шага сохраняется (остаёмся на экране/в модалке), но пользователю показывается «очеловеченный», но сырой текст исключения (shortMessage/description с бэкенда), без общего «проблема с соединением, повторите» | forgot_password_screen.dart:55-60, :150-154, :513-528 |
| П4 | вне BRD | — | В репозитории два флоу восстановления: активный auth/forgot_password и мёртвый restore_password — RestorePasswordScreen/RestorePasswordRoute зарегистрирован в роутере, но нигде не пушится. Дубль-реализация, риск расхождения при доработках | restore_password/widget/resotre_password_screen.dart:11; core/utils/app_router.dart:305 |
| П5 | вне BRD | — | print-логи в потоке восстановления печатают логин (PII) и полный JSON ответов, включающий JWT-токены (ValidateRestorePasswordCodeMobile/SetNewPassword) — в проде это утечка чувствительных данных в логи | auth_repository.dart:235, :264, :306, :338 |
Рекомендация / варианты решения
- Лимит попыток на валидации (П1) — Вариант А (реком.): согласовать с Back поведение при исчерпании попыток (доп. состояние/ошибка) и обработать его отдельно — блокировать ввод или предлагать новый код. Вариант Б: если лимит целиком на бэкенде — убрать оговорку «если лимит не исчерпан» из BRD (правит владелец BRD).
- Истёкший код и invalidSource (П2) — Вариант А: развести ветки — для
expiredCodeпоказывать «Код устарел, запросите новый» и разблокировать/подсвечивать повторную отправку; дляinvalidSource— осмысленный текст. Вариант Б: подтвердить, что серверныйshortMessageпокрывает оба случая. - Текст техошибки (П3) — Вариант А: маппить транспортные/технические ошибки в единое дружелюбное сообщение, детали — в лог/Sentry. Вариант Б: оставить серверный
shortMessageкак есть. - Дубль-флоу (П4) — Вариант А (реком.): удалить мёртвый
restore_passwordи снятьRestorePasswordRouteиз роутера. Вариант Б: оставить, если ветка планируется к использованию (тогда задокументировать назначение). - Логирование токенов/PII (П5) — Вариант А (реком.): удалить debug-
printили загейтить заkDebugMode, из логов исключить токены и логин. Вариант Б: заменить на Sentry-breadcrumb без чувствительных полей.