# Идентификация пользователя во Flutter

SDK поддерживает анонимный профиль, склейку по CUID и ручную идентификацию. Не смешивайте эти идентификаторы:

| Идентификатор | Назначение |
|---|---|
| Серверный `uid` | Внутренний профиль Gravity Field; SDK сохраняет его на устройстве |
| `ses` | Сессия запросов |
| `cuid` и `cuidType` | Единый идентификатор клиента между Web, приложением и офлайн-данными |
| `userId` в `setUser()` | Внешний ID приложения; передаётся как `user.custom` |

Общая модель: [Идентификация и омниканальность](/Integration/identification.md).

## Анонимный профиль

Не вызывайте `setUser()`, если хотите стандартное управление идентификацией. При первом успешном запросе сервер возвращает идентификаторы, а SDK сохраняет и использует их в последующих запросах. Не создавайте новую сессию вручную на каждый экран.

## Авторизация и CUID

После успешной авторизации отправьте `LoginEvent`. При регистрации можно использовать `SignUpEvent`. Рекомендуемый CUID — SHA-256 нормализованного телефона с `cuidType: 'phone_hash'`.

Номер нормализуют в международный формат без `+` и разделителей. Для РФ/КЗ — `7XXXXXXXXXX`. Хеш вычисляют по UTF-8 строке и передают как lowercase hex. Если CDP/DWH уже хранит такой идентификатор, используйте тот же хеш; не пересчитывайте его по другому правилу.

Добавьте `crypto: ^3.0.6` в зависимости. Пример для номеров РФ/КЗ:

```dart
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:gravity_sdk/gravity_sdk.dart';

String phoneHashRuKz(String rawPhone) {
  var digits = rawPhone.replaceAll(RegExp(r'[^0-9]'), '');
  if (digits.length == 11 && digits.startsWith('8')) {
    digits = '7' + digits.substring(1);
  }
  if (digits.length != 11 || !digits.startsWith('7')) {
    throw ArgumentError('Нужен полный номер РФ/КЗ в международном формате');
  }
  return sha256.convert(utf8.encode(digits)).toString();
}

Future<void> identifyAfterLogin(String phone) async {
  await GravitySDK.instance.triggerEventNoShow(
    events: [
      LoginEvent(cuid: phoneHashRuKz(phone), cuidType: 'phone_hash'),
    ],
    pageContext: const PageContext(
      type: ContextType.other,
      data: [],
      location: 'app://login',
    ),
  );
}
```

Для других стран нормализацию выполняйте по правилам их телефонных номеров. Склейка происходит на сервере после доставки события; при офлайн-отправке не считайте её завершённой сразу. Успешный возврат `triggerEventNoShow()` не является подтверждением авторизации или доставки.

SDK допускает nullable-поля у Login/SignUp, но серверу нужен идентификатор: `cuid` вместе с `cuidType` либо `hashedEmail`. Не отправляйте пустое событие.

## Ручной пользователь

```dart
GravitySDK.instance.setUser('customer-42', 'app-session-2026-10-01');
```

Приложение само управляет внешним ID и сроком жизни этой сессии. Не меняйте session ID на каждый запрос и не используйте константу для всех клиентов. `setUser()` не выполняет запрос к серверу и не отправляет `LoginEvent`.

В 0.24.0 этот пользователь применяется и к просмотрам/событиям, и ко всем запросам контента, inline-блокам и автопоказу in-app. Сам внешний ID не заменяет CUID для омниканальной склейки. `getUserId()` не возвращает значение из `setUser()`.

## Выход и смена аккаунта

```dart
await GravitySDK.instance.resetUser();
```

Метод убирает ручного пользователя и сбрасывает серверный UID/сессию. Следующий запрос начинает новую анонимную сессию. Дождитесь завершения сброса перед запросами нового аккаунта.

`resetUser()` **не очищает офлайн-очередь**: события прежнего пользователя должны доставляться с его исходной идентификацией. Если ваша политика для общего устройства требует удалить ожидающие события:

```dart
await GravitySDK.instance.clearQueue();
await GravitySDK.instance.resetUser();
```

На время смены аккаунта приложение должно остановить отправку новых событий и пересоздать персонализированные виджеты/состояния. Уже загруженный UI не обновляется автоматически только от вызова `setUser()` или `resetUser()`. `clearQueue()` удаляет данные без возможности восстановления и не отменяет запрос уже в сети.

## Получение серверного UID

```dart
final String? uid = await GravitySDK.instance.getUserId();

GravitySDK.instance.setUserIdListener((String? uid) {
  // В этом примере только наблюдаем изменение.
  print('Server UID: ' + (uid ?? 'не задан'));
});

// Когда подписка больше не нужна:
GravitySDK.instance.setUserIdListener(null);
```

До первого успешного запроса `getUserId()` может вернуть `null`. Если инициализация сессии уже выполняется, метод дожидается её; ошибку ожидания обрабатывайте в вызывающем коде.

Listener сообщает о появлении/изменении известного процессу UID, включая сброс на `null` и восстановление. Повторный серверный ответ с тем же UID его не вызывает. Подписка одна; новый listener заменяет старый. Чтобы получить текущее сохранённое значение, отдельно вызовите `getUserId()` — подписка не заменяет чтение.

## Восстановление после переустановки

Если приложение сохранило серверный UID вне удаляемого локального хранилища, его можно передать перед первым запросом:

```dart
Future<void> restoreProfile(String savedServerUid) async {
  await GravitySDK.instance.restoreUserId(savedServerUid);
}
```

`restoreUserId()` принимает **серверный UID**, полученный через `getUserId()`. Он сбрасывает текущую сессию и пользователя из `setUser()`; следующий запрос передаёт восстановленный UID серверу. Неизвестный серверу UID не восстанавливает профиль — сервер присваивает новый.

Пустая строка вызывает `ArgumentError`. Методы чтения UID, подписки и восстановления доступны до `initialize()`. Восстановление само по себе не проверяет UID на сервере. Хранение внешней копии UID и её удаление при выходе организует приложение.
