> ## Documentation Index
> Fetch the complete documentation index at: https://docs.exode.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Мобильное приложение

> Как из вашего приложения открыть конкретный урок (или другой экран) прямо в приложении ExodeBiz через deep link, с опциональной авторизацией по токену

<Info>
  Из вашего <b>нативного приложения</b> можно открыть приложение ExodeBiz сразу на нужном экране —
  выбрать школу, при необходимости авторизовать пользователя по токену и открыть урок. Достаточно открыть
  ссылку-схему штатным способом (`Intent.ACTION_VIEW` на Android / `UIApplication.open` на iOS).
</Info>

## Формат ссылки

Deep link использует URL-схему приложения `exodebizapp://`. Все параметры кладутся в один параметр
`data` — это <b>URL-кодированная query-строка</b>:

```
exodebizapp://?data=<url-encoded "action=open-page&domain=...&pageId=...&params=...&extra=...">
```

Параметры внутри `data`:

<ParamField query="action" type="string" required>
  Всегда `open-page`.
</ParamField>

<ParamField query="domain" type="string" required>
  Домен (FQDN) школы, например `my-school.com` или `my-school.exode.biz`.
</ParamField>

<ParamField query="pageId" type="string" required>
  Идентификатор экрана. Для урока — см. раздел «Открыть урок».
</ParamField>

<ParamField query="params" type="string (JSON)" required>
  JSON с параметрами экрана, например `{"page":"1","courseId":"42","lessonId":"123"}`.
</ParamField>

<ParamField query="extra" type="string (base64url)" required={false}>
  Список дополнительных действий (например авторизация). См. раздел «extra».
</ParamField>

<Warning>
  Значения параметров содержат спецсимволы и требуют <b>двойного кодирования</b>: сначала соберите
  query-строку `action=...&domain=...` (значения URL-кодированы), затем закодируйте её целиком и положите
  в `data`. Не собирайте вручную — используйте стандартные сборщики (`URLSearchParams`, `Uri.Builder`,
  `URLComponents`), см. примеры кода ниже.
</Warning>

## Открыть урок

| Поле     | Значение                                                           |
| -------- | ------------------------------------------------------------------ |
| `pageId` | `/courses/:page([0-9]+)/:courseId([0-9]+)/study/:lessonId([0-9]+)` |
| `params` | `{"page":"1","courseId":"<ID курса>","lessonId":"<ID урока>"}`     |

* `page` — страница в списке курсов; для перехода на урок укажите `"1"`.
* `courseId`, `lessonId` — идентификаторы курса и урока в вашей школе.

Готовая ссылка — подставьте `DOMAIN`, `COURSE_ID`, `LESSON_ID`:

```
exodebizapp://?data=action%3Dopen-page%26domain%3DDOMAIN%26pageId%3D%252Fcourses%252F%253Apage%2528%255B0-9%255D%252B%2529%252F%253AcourseId%2528%255B0-9%255D%252B%2529%252Fstudy%252F%253AlessonId%2528%255B0-9%255D%252B%2529%26params%3D%257B%2522page%2522%253A%25221%2522%252C%2522courseId%2522%253A%2522COURSE_ID%2522%252C%2522lessonId%2522%253A%2522LESSON_ID%2522%257D
```

### Другие экраны

Меняя `pageId` и `params`, можно открыть любой экран:

| Экран | `pageId`                                                           | `params`                                        |
| ----- | ------------------------------------------------------------------ | ----------------------------------------------- |
| Урок  | `/courses/:page([0-9]+)/:courseId([0-9]+)/study/:lessonId([0-9]+)` | `{"page":"1","courseId":"42","lessonId":"123"}` |
| Курс  | `/course/:courseId([0-9_A-Za-z]+)`                                 | `{"courseId":"42"}`                             |

<Tip>
  Нужен другой экран — напишите в поддержку, подскажем корректные `pageId` и параметры.
</Tip>

## extra — вложенные действия (авторизация)

Параметр `extra` позволяет прокинуть в приложение <b>список действий</b>, которые выполнятся вместе с
открытием — например авторизацию по токену. `extra` — это `base64url(JSON.stringify({ actions: [...] }))`.

Структура JSON:

```json theme={null}
{
  "actions": [
    { "type": "login-by-token", "token": "USER_TOKEN" }
  ]
}
```

Поддерживаемые действия:

| `type`           | Поля               | Описание                           |
| ---------------- | ------------------ | ---------------------------------- |
| `login-by-token` | `token`            | Авторизация пользователя по токену |
| `open-page`      | `pageId`, `params` | Навигация на экран                 |

<Info>
  <b>Порядок выполнения гарантирован:</b> сначала — действия из `extra` (по порядку в массиве), затем —
  навигация из `open-page`. То есть ссылка `open-page` + `extra:[{login-by-token}]` даёт ровно:
  <b>открыть приложение → авторизоваться → открыть страницу</b>.
</Info>

<Steps>
  <Step title="Получите токен сессии пользователя">
    Токен создаётся методом [Создание токена сессии](/ru/exode-api/school/user/session/auth-token)
    (`POST /saas/v2/user/session/auth-token`). Возьмите значение поля `token` из ответа.
  </Step>

  <Step title="Соберите extra">
    `extra = base64url(JSON.stringify({ actions: [{ type: 'login-by-token', token }] }))`.

    Пример (с placeholder-токеном `USER_TOKEN`):

    ```
    eyJhY3Rpb25zIjpbeyJ0eXBlIjoibG9naW4tYnktdG9rZW4iLCJ0b2tlbiI6IlVTRVJfVE9LRU4ifV19
    ```
  </Step>

  <Step title="Добавьте extra в data">
    Добавьте `extra` к query-строке внутри `data` — приложение переключит школу, авторизует пользователя
    и откроет урок.
  </Step>
</Steps>

<Warning>
  Ссылка с `login-by-token` даёт вход под конкретным пользователем — любой, у кого есть ссылка,
  залогинится под ним. Такие ссылки должны быть <b>короткоживущими / персональными</b> и не
  публиковаться.
</Warning>

<Note>
  Используйте именно <b>base64url</b> (`-`/`_` вместо `+`/`/`, без `=`-padding) — обычный base64 может
  некорректно пройти через query-строку.
</Note>

## Сборка и открытие ссылки в коде

<CodeGroup>
  ```js JavaScript theme={null}
  const SCHEME = 'exodebizapp';
  const LESSON = '/courses/:page([0-9]+)/:courseId([0-9]+)/study/:lessonId([0-9]+)';

  const b64url = (obj) =>
    btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(obj))))
      .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

  function buildLessonLink({ domain, courseId, lessonId, page = 1, token }) {
    const inner = new URLSearchParams({
      action: 'open-page',
      domain,
      pageId: LESSON,
      params: JSON.stringify({
        page: String(page), courseId: String(courseId), lessonId: String(lessonId),
      }),
    });

    // опционально: автологин
    if (token) {
      inner.set('extra', b64url({ actions: [{ type: 'login-by-token', token }] }));
    }

    return `${SCHEME}://?data=${encodeURIComponent(inner.toString())}`;
  }
  ```

  ```kotlin Android theme={null}
  fun buildLessonLink(domain: String, courseId: Long, lessonId: Long, page: Int = 1): String {
    val pageId = "/courses/:page([0-9]+)/:courseId([0-9]+)/study/:lessonId([0-9]+)"
    val paramsJson = """{"page":"$page","courseId":"$courseId","lessonId":"$lessonId"}"""
    val inner = Uri.Builder()
      .appendQueryParameter("action", "open-page")
      .appendQueryParameter("domain", domain)
      .appendQueryParameter("pageId", pageId)
      .appendQueryParameter("params", paramsJson)
      .build().encodedQuery ?: ""
    return "exodebizapp://?data=" + Uri.encode(inner)
  }

  // открыть с fallback
  try {
    startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
  } catch (e: ActivityNotFoundException) {
    // приложение не установлено → откройте магазин / сайт
  }
  ```

  ```swift iOS theme={null}
  func buildLessonLink(domain: String, courseId: Int, lessonId: Int, page: Int = 1) -> URL {
    let pageId = "/courses/:page([0-9]+)/:courseId([0-9]+)/study/:lessonId([0-9]+)"
    let paramsJson = "{\"page\":\"\(page)\",\"courseId\":\"\(courseId)\",\"lessonId\":\"\(lessonId)\"}"

    var inner = URLComponents()
    inner.queryItems = [
      .init(name: "action", value: "open-page"),
      .init(name: "domain", value: domain),
      .init(name: "pageId", value: pageId),
      .init(name: "params", value: paramsJson),
    ]

    let data = (inner.percentEncodedQuery ?? "")
      .addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? ""
    return URL(string: "exodebizapp://?data=\(data)")!
  }

  // открыть с fallback
  UIApplication.shared.open(link, options: [:]) { success in
    if !success { /* приложение не установлено → откройте магазин / сайт */ }
  }
  ```
</CodeGroup>

## Поведение

* Deep link <b>срабатывает только если приложение установлено</b>. Если нет — ОС ничего не откроет:
  обработайте это сами и покажите fallback (магазин приложений или сайт школы). На Android ловите
  `ActivityNotFoundException`, на iOS — `success == false` в completion-handler `open`.
* Если школа ещё не была добавлена в приложении — она <b>добавится автоматически</b> и станет активной.
* Навигация подчиняется обычной авторизации: без `extra`-логина неавторизованный пользователь увидит
  экран входа.

## Диагностика

| Симптом                                  | Причина / решение                                                                                                         |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Ничего не происходит при открытии ссылки | Приложение не установлено, либо контекст не распознаёт кастомные схемы. Реализуйте fallback.                              |
| Открылось не на том уроке                | Проверьте `pageId` (символ-в-символ) и значения в `params`; убедитесь, что `data` собран корректно (двойное кодирование). |
| «Школа не найдена»                       | Неверный `domain`. Укажите точный FQDN школы.                                                                             |
| Открывается экран входа                  | Пользователь не авторизован / нет доступа. Для автологина используйте `extra` с `login-by-token`.                         |
