Обложка: Как мы создали приложение «Тайный Санта»

Как мы создали приложение «Тайный Санта»

В компании есть традиция — каждый год перед новогодними праздниками мы играем в «Тайного Санту». Сначала это делали вручную: создавали таблицу с сотрудниками, нумеровали каждого участника, а потом при помощи рандомайзера распределяли подопечных. То в 2015 году группа инициаторов решила перенести это в мобильное приложение.

Идея хорошая, если бы не одно «но» — до Нового года оставался всего месяц. Но, как часто это бывает, когда «горит», то и сжатые сроки не помеха. Мы сделали приложение под три платформы — iOS, Android и Windows Phone. И даже успели пройти модерацию в магазинах.

Прошло время, и за несколько лет использования приложения мы заметили, что можно улучшить. К тому же пора было обновлять дизайн, поэтому запустили процесс глобальных доработок.

Задачи по улучшению

Начали мы с анализа отзывов в магазинах приложений, затем изучили поведенческие сценарии пользователей. Дальше разработали опрос и раздали его фокус-группам. После всех этапов отобрали пункты, которые нужно доработать в первую очередь. Фокус внимания был направлен на то, чтобы пользователи легко ориентировались в нашем «Тайном Санте».

Итак, перед нами возникло несколько задач:

  • сделать путь создания игры максимально коротким;
  • упростить ввод информации для организатора игры;
  • обеспечить безопасность данных пользователей;
  • объяснить правила игры и добавить подсказки.

Регистрация в приложении

Мы хотели упростить регистрацию пользователей в игре: скачал приложение, ввёл номер — всё, началось создание игры. Но по факту получилось чуть иначе.

Данные обо всех играх находятся на облачном сервере. Если бы пользователь при авторизации ошибся цифрой в номере, то получил бы доступ к информации об играх другого человека. Такой вариант нас совершенно не устраивал, и мы добавили верификацию пользователя с помощью кода из SMS-сообщения.

Несколько позже у Apple вышли новые требования, и мы добавили авторизацию через Apple ID. И потом сразу — авторизацию через Gmail для пользователей Android. Такой шаг в итоге сильно ускорил процесс регистрации. Кстати, позже аналитика показала, что этот способ регистрации наиболее популярен у пользователей «Тайного Санты».

Хотелось бы на старте рассказать про технический стек проекта — mobile side в виде кроссплатформенного Flutter, backend — Laravel & MySQL, frontend — классический MPA с использованием Laravel Blades.

Авторизация/регистрация в приложении построена на базе сервиса Firebase Authentication. Учитывая это, смысла городить свои кастомные токены нет, так как токены Firebase можно с лёгкостью валидировать на стороне API, используя неофициальный Firebase Admin SDK. С токенами определились, дальше задействуем стандартный механизм валидации поступающих запросов на API. Изменим конфигурацию Authentication Guards, находящуюся в /config/auth.php, а именно заменим драйвер для всех api endpoints на custom_token.


'api' => [
      'driver' => 'custom_token',
],

Дальше воспользуемся AuthServiceProvider.php и опишем, что именно нам нужно делать при любом API-запросе и как нужно валидировать Auth Bearer token.

Внутри public function boot() {} опишем следующее:

/**
    * Register any authentication / authorization services.
    *
    * @return void
    */
   public function boot()
    {
       $this->registerPolicies();
 
       Auth::viaRequest('custom_token', function ($request) {
           // получаем Bearer token из входящего request
           $idTokenString = $request->bearerToken();
           // инициализируем Firebase instance
           $factory = new Factory();
           $auth = $factory->createAuth();
 
           try {
               // непосредственно валидируем токен
               $verified_id_token = $auth->verifyIdToken($idTokenString);
           } catch (\Exception $e) {
               Sentry\captureException($e);
               return null;
           }
           $firebase = $verified_id_token->getClaim('firebase');
           $uid = $verified_id_token->getClaim('sub');
           if ($firebase->sign_in_provider === 'phone') {
               $phone_number = $verified_id_token->getClaim('phone_number');
               return User::firstOrCreate(['uid' => $uid], ['phone' => $phone_number]);
           } elseif ($firebase->sign_in_provider === 'google.com' || $firebase->sign_in_provider === 'apple.com') {
               $user =  $auth->getUser($uid);
               $email = $verified_id_token->getClaim('email');
               return User::firstOrCreate(['uid' => $uid], ['email' => $email, 'name' => $user->displayName]);
           };
       });
   }

Такой способ позволяет при первом запросе к API создать пользователя и в любом месте внутри проекта обратиться к Auth::user(). Так мы получаем текущего авторизованного пользователя, причём довольно удобно.

Соответственно, регистрация пользователя происходит при первом запросе к API, а затем уже отдельным запросом происходит обновление остальных пользовательских данных в базе. Первично же пользователь должен выбрать метод регистрации, который ему удобен, подтвердить номер телефона с помощью SMS-кода или же воспользоваться другим Sign-in provider в виде Apple ID или Google. Всё это происходит на клиентской стороне.

Создание «Тайного Санты»

Другое преобразование, которое мы задумывали сделать, — уменьшение полей в форме при создании игры. Это должно было выглядеть так: пользователь после регистрации начинает игру и делится ссылкой с участниками. Мы хотели избавить организатора от необходимости заполнять большое-большое количество информации. Но опрос показал, что эти поля нужны, и убирать их не надо.

Поэтому во время создании игры необходимо заполнить следующую информацию.

  1. Название. Бывает, что у организатора не одна игра (семья, друзья, сотрудники). И чтобы как-то ориентироваться, пользователи хотели сами задавать название играм.
  2. Место. Всегда найдутся те, кто забудут, где договаривались обмениваться подарками.
  3. Время и дата. Чтобы все участники были в курсе, какого числа и во сколько запланирован радостный момент.
  4. Комментарий. Можно договориться о каких-то условиях игры. Например, задать лимит стоимости подарка.

Эта информация рассылается по email всем участникам. Если вдруг кто-то забудет имя своего подопечного или место обмена подарками, может легко найти это в письме.

В игре есть отдельный чек-бокс для организатора, который дает выбор — участвовать или нет. Кому-то хочется просто организовать «Тайного Санту», а кто-то собирается вместе с остальными обмениваться подарками. Поэтому каждый раз при создании игры — а их у одного человека может быть несколько — организатор отмечает, участвует он или нет.

А теперь немного технических подробностей

Здесь нам уже необходимо описать модель сущности Game и Request для валидация входящих данных с использованием базовых инструментов Laravel.

В итоге получим следующее:

public function createGame(CreateGameRequest $request)
   {
       try {
           $game = new Game();
           $game->fill($request->validated());
           $game->locale = app()->getLocale();
           $game->game_share_key = Str::random(128);
           // связываем игру и текущего авторизованного пользователя
           $game->owner()->associate(Auth::user());
           $game->save();
           // генерируем URL по которому можно будет присоединиться к игре
           $url = route('lang.web.game.share-link', [$game->locale, $game->game_share_key]);
       } catch (\Throwable $e) {
           captureException($e);
           return response()->json([
               'message' => __('error_messages.server')
           ], Response::HTTP_INTERNAL_SERVER_ERROR);
       }
 
       return response()->json([
           'data'       => $game,
           'share_link' => $url,
       ], Response::HTTP_OK);
   }

Wow-фича

По просьбе участников количество полей нам сократить не удалось. Но оптимизировать процесс добавления игроков было в наших силах. Раньше организаторы вводили email каждого участника, согласитесь, мало приятного. И в результате некоторые бросали игру, так и не добавив все адреса.

Чтобы решить эту проблему, мы сделали wow-фичу — кнопку шаринга анкеты для игроков. Организатор регистрируется, создаёт игру и рассылает ссылку на анкету любым удобным способом — с помощью SMS, мессенджера или социальной сети. И участники уже самостоятельно вводят свой email. Это, в свою очередь, минимизирует количество ошибок при вводе электронной почты.

Регистрация участников на игру осуществляется через веб-анкету, так что никому, кроме организатора, скачивать приложение не нужно. Если же игрок захочет скачать приложение, всё равно не увидит игр, в которых участвует. Эта информация хранится только у организатора.

Если по какой-то причине участник недоступен и не может заполнить анкету, организатор может сделать это за него. Но в любом случае игрок должен подтвердить свой email.

Секретная фича

Да простит нас Санта, мы внедрили секретную фичу, которая позволяет подсмотреть, кто чей подопечный. Все-таки ситуации бывают разные, и иногда эта функция необходима. В частности, она нужна была тем, кто играл вместе с детьми. Сразу скажем, фича спрятана, найдут её только самые внимательные.

С точки зрения технической реализации, узнать, кому пользователь дарит и от кого получает подарок, не особо сложно. Данная фича может пригодиться в запутанных случаях для выяснения обстоятельств.

Для этого нам потребуется лишь ID самой игры и участника, которого нужно подсмотреть.

public function peekOnParticipant($game_id, $participant_id)
   {
       try {
           // достаем нужную игру из базы
           $game = Game::where('owner_id', Auth::user()->id)->findOrFail($game_id);
           // находим самого участника
           $participant = Participant::where('game_id', $game_id)->findOrFail($participant_id);
           // получаем игрока, которому наш участник будет дарить подарок
           $participant_gives_to = Participant::where('game_id', $game_id)
               ->select(['id', 'name'])
               ->findOrFail($participant->gives_to);
           // получаем игрока, который будет дарить подарок нашему участнику
           $participant_receives_from = Participant::where('game_id', $game_id)
               ->where('gives_to', '=', $participant_id)
               ->select(['id', 'name'])
               ->first();
       } catch (\Throwable $exception) {
           captureException($exception);
 
           return response()->json([
               'message' => __('error_messages.server')
           ], Response::HTTP_INTERNAL_SERVER_ERROR);
       }
 
       return response()->json([
           'gives_to' => $participant_gives_to,
           'receives_from' => $participant_receives_from,
       ], Response::HTTP_OK);
   }

Статус подтверждения почты

Каждый игрок должен подтвердить свой email. Если он этого не сделает, то под его именем появится надпись о том, что почта не подтверждена. Организатор увидит эту информацию и сможет поторопить участника.

Чтобы снизить вероятность ошибки при вводе почты в анкету, мы добавили валидацию email-адреса.

Теги с подарками

При заполнении анкеты каждый участник с помощью тегов может отметить категорию подарка. Эта информация придёт Санте в письме. Таким образом мы подсказываем, в каком направлении искать подарок для подопечного.

Результаты редизайна

Перечисленные выше улучшения дали свои результаты.

  1. Число проведённых игр выросло в десять раз по сравнению с первой версией приложения.
  2. В два раза стало больше тех, кто скачал приложение и создал игру.
  3. Количество уникальных игроков выросло в десять раз.

Планы по развитию приложения

Конечно, мы не хотим останавливаться на достигнутом и планируем совершенствовать наше приложение. Расскажем, какие улучшения будем делать в первую очередь.

Редизайн 2021

Собираемся провести UI/UX-аудит текущего приложения и на его основе сделать редизайн. И так как удалёнка стала неотъемлемой частью нашей жизни, мы хотим помочь организовывать игру для распределённых команд. Раньше был расчёт только на офлайн-игру. То есть было определённое место, где участники в назначенное время обменивались подарками. Мы хотим, чтобы в «Тайного Санту» можно было играть на расстоянии. Поэтому добавим информацию об адресе участника, и Санта будет знать, куда отправлять подарок.

Ещё одна существенная проблема Сант — выбор подарка. Ведь часто в качестве подопечных попадаются люди, которых даритель не знает или знает недостаточно хорошо. Соответственно, что человек любит, какие у него увлечения и предпочтения, Санте неизвестно.

Мысль о том, как помочь Сантам угадывать с подарками, не давала нам покоя. В прошлом году это были теги с подарками, о которых мы писали выше. В этом году мы решили пойти еще дальше — помогать Сантам купить подарок. Поэтому планируем внедрить вишлист с товарами и возможностью добавить их в избранное. Это поможет назначенному Санте понять, что подопечный хотел бы получить в подарок.

Монетизация

Помимо снятия головной боли с Сант — «что подарить», мы увидели, что вишлист — потенциальный канал для монетизации. Поэтому возникла идея реализовать в приложении реферальную систему с помощью Яндекс и Amazon. Санта видит список рекомендаций, выбирает товар, переходит в один из магазинов по реферальной ссылке и оформляет заказ. В случае успешной оплаты мы получаем комиссию.