Универсальные ссылки: дворец из подводных камней
При том, как много мобильные приложения дали человечеству, они в то же время «сломали» интернет. Вместо понятных ссылок на сайты, которые можно скопировать и поделиться, стало нужно объяснять «поставь такое-то приложение и зайди там туда-то».
К счастью, разработчики мобильных платформ осознали эту проблему и предложили концепцию «универсальных ссылок», которые одним кликом открывают нужное на любой платформе. Но то, что для пользователя «одним кликом», для программиста — «потом и кровью». На пути к успеху стоит целый ворох неожиданных нюансов, и Константин Якушев познакомился с ними на личном опыте при внедрении универсальных ссылок в Badoo. А затем на нашей конференции Mobius рассказал, как сделать всё правильно и обойти проблемы. Зрителям доклад понравился, и мы решили, что негоже полезному материалу оставаться только видеозаписью, поэтому под катом — его текстовая версия.
В докладе ситуация рассмотрена со стороны iOS, но универсальные ссылки на то и универсальные, чтобы объединять разные платформы, так что извлечь пользу могут не только iOS-разработчики.
Вступление
Меня зовут Костя Якушев и я провёл несколько месяцев, отлаживая универсальные ссылки в Badoo для всего сразу: iOS, Android, веба. Я координировал действия всех этих прекрасных ребят, потому что я мамин архитектор.
И сегодня я расскажу вам следующее:
- Что такое универсальные ссылки
- Зачем они вам нужны
- Как их сделать
- Самое главное: какие будут с этим проблемы
- И как эти проблемы решать
- Каков общий фантастический алгоритм невероятного успеха, который позволит вам сделать это не за несколько месяцев, как мы, а за один вечер (ну, может быть, два, потом баги неделю)…
Что вообще такое универсальные ссылки? История достаточно простая. Вы пользуетесь каким-нибудь приложением, например, Badoo. Если вы открыли там чей-то пользовательский профиль и вам захотелось отправить другу ссылку на этот профиль, вы нажимаете эту кнопку со стрелочкой:
Присылаете другу ссылку, она в переписке красиво отображается, друг на неё нажимает, и у него открывается страница той же самой девушки:
Когда ссылка открывает какой-то контент внутри приложения, это традиционно называется диплинкингом. Но это идеальный сценарий, когда у друга установлено приложение, и оно под iOS.
А на практике у друга может не быть приложения, и тогда мы хотим его отправить в App Store.
Либо, если мы не очень хотим установок, зато очень хотим сразу показать страницу, отправим его в мобильную версию.
Либо друг вообще открыл ссылку на десктопе, и тогда ему нужно отправить десктопный сайт.
Но ужаснее всего, что у друга может оказаться Android! И это тоже должно сработать.
Когда ссылка обладает всеми этими свойствами или хотя бы некоторыми из них, то она называется универсальной ссылкой. И про них я и расскажу, как это делать.
Вот как это выглядит с более алгоритмической точки зрения. Если ссылка открыта с десктопа, открываем десктопный веб, если со смартфона — смотрим, установлено ли приложение, и в зависимости от ответа либо открываем в приложении, либо ведём в магазин приложений. В случае с магазином в идеале мы хотим, чтобы после установки при первом запуске приложения пользователь тоже сразу попал туда, куда изначально хотел.
Зачем всё это нужно? Применений миллион.
Например, вы хотите отправить письмо пользователю «смотри, у тебя новый лайк». Было бы классно, если бы при нажатии на «Take a look» открылся именно новый лайк.
Например, группа Serebro открыла в вашем приложении профиль. Вы хотите, чтобы они могли опубликовать под своими клипами ссылку, которая тоже открывала бы этот профиль.
Например, вы размещаете рекламу в Facebook. Facebook очень щепетильно относится к тому, что если на рекламе изображено что-то конкретное, то ссылка тоже должна вести на это конкретное. То есть, если вы видите в рекламе профиль девушки, то они настаивают, чтобы эту же девушку можно было увидеть в приложении.
Не обязательно речь должна идти о дейтинге, в других случаях применений тоже много.
А кнопка «share», про которую я уже говорил — это самое важное, самое классное, самое прикольное, но и самое сложное.
Идея здесь в том, что пользователи хотят делиться чем-то, что есть в вашем приложении. Это даже важнее для других приложений, не-дейтинговых. Допустим, если у вас трэвел-приложение, пользователи точно захотят делиться билетами или гостиницами. Они бесплатно отправляют ссылки своим друзьям, тем самым делая рекламу вашему приложению. В идеале это приводит к новым установкам, к органическому росту, и при этом почти бесплатно (по цене, собственно, имплементации этой технологии).
И вот мы поняли, что это круто и мы этого хотим, собрались и начали думать, как же это сделать. Есть несколько сервисов: все знают branch.io, есть AppsFlyer, ещё есть какие-то странные сервисы, а можно делать свой велосипед. Всё закончилось тем, что мы сделали свой велосипед, но расскажем, как к этому пришли.
Сначала мы пошли смотреть на branch.io. Всё, что они делают, связано исключительно с deep links: на сайте по центру написано deep linking, но два пункта по бокам, в общем-то, тоже означают диплинкинг.
У них куча крутых клиентов, наш любимый конкурент Tinder ими пользуется. И в некоторых случаях они бесплатные. Но в нашем случае они оказались совсем не бесплатными.
Они, безусловно, эксперты в теме, пишут миллион блог-постов, в которых рассказывают о тех же самых проблемах, о которых я вам сейчас буду рассказывать. Однако, когда в посте они переходят от проблемы к решению, они немножко заминают его и говорят «ну, используйте наш сервис, мы знаем, как это делается». К счастью, у вас есть я.
AppsFlyer — это маркетинговый сервис, который в первую очередь предназначен для трекинга установок, чтобы определять, откуда пользователи пришли, измерять эффективность рекламных кампаний и так далее. Когда мы занялись универсальными ссылками, в Badoo уже пользовались этим сервисом для других задач, и наши маркетологи его просто обожают. Если прислонить нашего маркетолога к стене и спросить, крутой ли AppsFlyer, он скажет: «Очень крутой, все остальные сервисы гораздо хуже. И не прислоняй меня больше к стене, пожалуйста».
У AppsFlyer тоже много крутых клиентов, им тоже пользуется Tinder — для всего, кроме диплинков, но тогда это нас не смутило. Мы заходим на сайт, видим надпись «The most world’s powerful deeplinking platform» — ну, надо брать. Кроме того, на тот момент это было для нас, в общем, бесплатно, потому что входило в продукт, за который мы и так платим.
Мы зашли к ним на сайт. Тогда там была вот такая схема, а сейчас её на сайте уже нет. В принципе, это то же, что я показывал раньше. Вы переходите по ссылке, если приложение установлено — запускается, если нет — идёте в App Store. Но!
Как выяснилось, та часть, где приложение не установлено, мы идём в AppStore и после установки открываем контент, работает. И это логично: эти ребята занимаются трекингом инсталлов, они на этом собаку съели.
Но вот часть «приложение уже установлено», когда мы её реализовали, почему-то вместо того, чтобы открывать контент, тоже показывала вопрос «Open this page in App Store?».
Тогда мы перестали слепо следовать SDK и сели разбираться, как это в принципе работает. И чтобы объяснить, почему у них возникает эта ошибка и что мы с этим сделали, нужно начать с самого начала.
С самого начала
Вот архетипичный диплинк:
Здесь есть кастомная схема badoo://, какая-то штука, которую мы хотим открыть (допустим, юзера), и есть ID этого юзера.
Понятно, что в сыром виде эту ссылку использовать нельзя по очень простой причине. Если приложение установлено, она работает как ссылка, но если не установлено, приводит к ошибке «Safari cannot open the page». Это логично, Safari понятия не имеет, как с этим работать.
До недавнего времени это обходилось достаточно легко. Мы делаем обычную HTML-штуку, в которой у нас есть несколько вещей:
Есть iframe, который получает параметры из URL и имеет тот самый deep link. И есть JavaScript, который редиректит в App Store. Задумка в том, что если у вас установлено приложение — сработает iframe, если не установлено — сработает редирект, и все счастливы, ошибка не вылезет.
Кроме того, к HTML-странице можно довесить Open Graph-теги, которые будут позволять ссылке выглядеть вот так красиво, как я показал: у неё есть картинка, превью и все такое.
Так вот, начиная примерно с iOS 9, эта прекрасная схема перестала работать.
Что значит «перестала работать»? Это значит, что она стала работать так, как будто вы просто шарите ссылку badoo://. Если приложение установлено — работает, если не установлено — показывает ошибку (в общем-то, поверх редиректа). То есть, если пользователь переходит по ссылке и у него не установлено приложение, возникает ошибка. И в Apple сломали это совершенно осознанно.
Зачем? Потому что они придумали нечто «принципиально новое и фантастическое»: универсальные ссылки.
В их задумке вы просто регистрируете всю ссылку, весь домен как universal link. И когда я говорю «принципиально новый и фантастический» способ, я имею в виду «почти принципиально новый», в Android он уже много лет был.
Зачем они это делали?
- Во-первых протокол badoo:// может зарегистрировать кто угодно, нет никакой верификации. А домен подтверждается DNS.
- Во-вторых, приватность: приложение больше не получает доступ к cookie. Это то, что Apple хочет с каждым годом всё больше — отделить знания о пользователе в Safari от знания о пользователе в приложении.
В-третьих, помогает избавиться от хаков с айфреймом.
Наконец, (в идеале) одинаково работает на всех платформах.
К сожалению, когда мы избавились от хаков с айфреймами, мы получили много других хаков, но об этом попозже. Сейчас про AppsFlyer. Помните, всё началось с того, что они не поддерживают универсальные ссылки.
На самом деле, как стало ясно из документации, они их поддерживают, но они требуют использования вот такой вот схемы: они ожидают, что мы заведём у них домен третьего уровня и домен второго уровня будет принадлежать им.
В нашем случае это был совершенно неоправданный риск и очень неприятная история. Потому что они могли увеличить свой ценник, могли просто закрыться, а наши пользователи сами шарят эти ссылки, они должны в идеале работать вечно. Нам это не нравилось.
Но мы же умеем решать эти проблемы, да? Обычно мы делаем редирект, на вебе же работают редиректы, правда ведь?
И мы стали тестировать. Окей, onelink.me работает, мы все правильно интегрировали, SDK запускается, открывает Badoo, открывает контент. Наша ссылка, которая является тупым редиректом… открывает Safari. Ничего себе, подумали мы.
И тогда мы стали вести список, который становился всё длиннее с каждым днём нашей реализации. Он называется «Когда универсальные ссылки не открывают приложение, хотя ожидаешь, что откроют».
И вот первый пункт этого списка:
Редиректы на универсальные ссылки не работают. Это сильно сбило нас с толку, тогда мы решили бросить AppsFlyer (и по этой причине, и по некоторым другим), сделав всё самостоятельно. Тем более, что у нас уже была готовая схема андроида:
Она состояла из двух частей. У нас был page id (идентификатор той страницы, которая должна открыться) и id контента (например, user id). И сделать это в принципе не сложно, у Apple на сайте есть инструкция, она занимает буквально три страницы и представляет собой три пункта.
Во-первых нужно добавить apple-app-site-association — json-файл, в котором у вас будет указан ваш appID и пути, которые должны открываться приложением в универсальные ссылки.
Второе. Вам нужно добавить в entitlements домены:
И третье: вам нужно написать немного кода, который, в общем-то проверит, что ActivityType это BrowsingWeb, а URL есть.
Мы это сделали, потестили пару-тройку кейсов, всё заработало, и пошли спорить на тему того, как же должны выглядеть ссылки.
Вот в чём здесь история. У нас есть наша первая схема, о которой я сказал, в которой указан page id. Есть вторая схема, ей пользуются как раз branch.io и AppsFlyer: обычно делают так, что у вас нет идентификатора, а есть просто какой-то ref ссылки, который приложение при запуске отправляет на сервер, получает в ответ, куда ему залендить, и лендит туда.
Проблема второго варианта в том, что пока приложение получает контент, мы хотели бы показывать какой-то каркас содержимого, а вторая схема нам этого не позволяет (в частности, с AppsFlyer этого не сделать никак).
В конечном счёте мы решили, что просто будем использовать всё сразу:
В принципе, это нормальное решение: мы можем использовать ref и для статистики, и чтобы варьировать поведение сервера в зависимости от того, что это была за ссылка (например, у нас есть ссылки, которые дают кредиты). Page id и user id можем использовать для того, чтобы быстро запуститься и сразу же сделать get user.
Но возникла другая проблема. К нам пришли менеджеры и сказали: «Ребята, у вас очень длинная ссылка, как люди будут её шарить».
Ну опять же, мы знаем решение… делать редирект… гм.
В принципе, в короткой ссылке мы сохраним все свойства, там будет и идентификатор страницы, и идентификатор контента, это можно сделать. Но как мы сделаем редирект?
Оказывается, на самом деле редирект сделать можно, но для этого нужно сделать маленький хак. Он достаточно простой. Ваш минификатор (это должен быть ваш личный минификатор) тоже объявляется диплинком в приложении, для него вы тоже делает apple-app-site-association, тоже пишете код, и, соответственно, оба домена являются диплинком.
Более того, это можно достаточно легко реализовать. Просто-напросто в коде, который открывает ваше приложение, он делает http-запрос на минификатор, смотрит куда идёт редирект, и использует длинную ссылку уже по стандартной логике.
Если помните, у AppsFlyer часть с перенаправлением в магазины приложений работает хорошо, и мы применили его для App Store. Это единственная часть, которую мы не стали делать сами, но, в принципе, вы можете сделать это сами, мы просто не захотели.
В чём идея. Пользователь открывает http-ссылку AppsFlyer, они делают fingerprinting пользователя (то есть запоминает IP-адрес, модель iPhone, отсечение времени и так далее, какие-то свойства этого телефона), и редиректят в App Store. Пользователь устанавливает, и после этого в самом приложении AppsFlyer SDK сопоставляет устройства, недавно ходившие по ссылкам, с текущим устройством и делает вывод о том, какую ссылку надо открыть.
Соответственно, общая схема получилась такая. Минификатор редиректит в нашу ссылку, но если ни та, ни другая ссылка не подцепились приложением, значит, приложения нет, мы перенаправляем в AppsFlyer, он редиректит в App Store с трекингом и уже делает то, что нужно.
Пока мы занимались всей этой фигнёй, к нам пришел QA и сказал: «Ребята, я отправляю ссылку в Telegram, в Skype, в HipChat, ничего не работает». Мы: «Как, подождите, у нас все работает».
Оказалось, дело в SafariViewController.
История c SafariViewController совершенно трагическая. Дело вот в чём. По задумке Apple, если пользователь открывает Safari, вводит в адрес универсальную ссылку и нажимает Enter, то она не открывает приложение. Это логично: если ты пользователь, то не ожидаешь, что при нажатии Enter в браузере попадёшь в приложение.
Нелогична вторая часть: когда приложение открывает SafariViewController, происходит ровно то же самое, как если бы пользователь ввёл ссылку в адресную строку и нажал Enter. Нет никакого способа вместо открытия SafariViewController открыть универсальную ссылку.
Так мы увеличиваем этот список:
Если пользователь ввел ссылку в Safari сам или открыл SafariViewController — ничего не работает. Мы некоторое время думали, а потом подсмотрели придумали решение.
И оно достаточно логичное. Мы будем открывать с html-превью, а там по нажатию на кнопку переходить на ту же самую ссылку:
А поскольку в SafariViewController универсальные ссылки работают, не работает только открытие вместо SafariViewController универсальной ссылки, то это должно сработать, правда ведь?
Не совсем.
Это решение тоже не работает, и вот почему:
Если пользователь нажал на ссылку в том же домене, на котором он сейчас находится, она не открывает приложение.
Что же делать?
Ну как что — ещё один хак, конечно.
Всё очень просто: мы делаем два домена, и оба регистрируем как универсальную ссылку. Вот как это выглядит.
Пользователь открывает m.badoo.com, а на кнопке у него будет mlink.badoo.com. Можно даже скопировать эту ссылку и прислать, она работает в обе стороны, у нас эти два домена работают как эквивалентные. Соответственно, если пользователь откроет mlink.badoo.com, у него будет на кнопке m.badoo.com. Победа.
Общая схема стала еще веселее.
Теперь у нас есть минификатор, который здесь не показан. Домен m.badoo.com по кнопке директит в mlink, mlink редиректит в AppsFlyer, и там уже происходит редирект в App Store с трекингом. Стало работать несколько лучше. По крайней мере, в Safari, в Telegram люди более-менее смогли как-то открыть.
Потом к нам приходит опять наш любимый QA, тоже уже грустный-грустный, и говорит: «Знаете, у нас стали происходить очень странные дела».
На некоторых устройствах, только на некоторых, почему-то универсальные ссылки не работали. Вообще.
Мы выяснили. При переходе по универсальной ссылке и запуске приложения в правом верхнем углу появлялась вот эта кнопочка:
И она делает две вещи. Во-первых, она открывает Safari. Во-вторых, она навсегда ломает диплинки для вашего приложения!
И потом единственный способ разломать их обратно — это сделать long tap на ссылке, нажать «open in Badoo», и тогда всё заработает обратно. Но ни один пользователь на моей памяти сам до этого ещё не додумался. Общая идея: не трогайте эту кнопку.
К счастью, в своей бесконечной мудрости, компания Apple этой осенью сделала нам фантастический подарок и удалила эту кнопку из iOS 11.
Но если вы поддерживаете iOS 9 и 10, помните об этой кнопке. А если вы вдруг поддерживаете ещё и iOS 9, помните, что apple-app-site-association не должен быть закрыт robots.txt, иначе он не будет работать, и это тоже проблема, с которой мы столкнулись.
Версионирование
В общем, мы набили бесконечное количество шишек. И тут случилось нечто, что набило нам вторую бесконечность шишек.
Компания Badoo решила сделать новую крутую функцию «Двойники» («Lookalikes»).
Идея очень простая. Можно сфотографировать кого-то или взять готовую фотографию, и мы найдём в нашем сервисе людей, больше всего похожих на этого человека. Можно искать знаменитостей, можно искать друзей, можно искать себя, посмотреть на своих доппельгангеров. Классная тема.
Настолько классная, что вот про эту схему наши менеджеры сказали, что нужно её поменять.
Почему? Мы очень хотим, чтобы когда наши пользователи делятся друг с другом двойниками, получатели должны увидеть это во что бы то ни стало, ничто не должно стоять на их пути. Поэтому, если приложение не установлено, мы больше не уводим в App Store. Мы открываем в мобильном вебе.
«Ооокей», сказали мы. В принципе, всё не так сложно, да? Мы говорим: «Хорошо, если в адресе будет другой page id, по нему мы будем редиректить в мобильный веб»
Это было несложно, мы сделали. Но потом до нас дошло, что это новая функциональность, которой раньше не было в наших приложениях.
А это значит, что даже если приложение установлено, всё равно не факт, что мы должны его открывать. На самом деле схема должна выглядеть вот так:
Если приложение установлено, нужно проверить, поддерживает ли оно нашу новую функциональность. И только если поддерживает, открывать в приложении, а иначе, опять же, вести в мобильный веб.
Потому что на некоторых платформах, и на iOS в частности, тогда ещё не было даже возможности это открыть. И так мы подходим к теме версионирования.
Опять же, в идеале всё очень просто. У нас есть старая версия и новая версия. Задумка в том, что старая версия открывает только пути /u, а новая версия открывает /u и, допустим, /l/a от «look alikes».
Мы закатали рукава, думаем, сейчас всё сделаем. Но не тут-то было.
Помните, я в начале рассказывал как настраивать ссылки? Так вот, пути, которые поддерживает приложение, указываются в специальном файле на сайте. И только на сайте. Соответственно, как только мы добавляем туда новый путь, эта штуковина начинает открываться и в старом приложении, и в новом:
Но в старом приложении оно крашилось, потому что мы забыли об этом подумать. И мы ничего уже не могли с этим сделать. Единственное, что мы сделали — подождали, пока новое приложение раскатилось на достаточное количество пользователей, и только тогда включили.
Но вам я могу сказать — ребята, подумайте об этом заранее. Вот здесь проверьте, что можете ли открывать URL, и в нужных случаях возвращайте false:
Тогда, если у пользователей старая версия приложения попытается открыть что-то, что оно не поддерживает, у вас вместо него будет открываться Safari. А как только новое поддержит, canOpenUrl начнёт возвращать true, и открываться будет уже оно.
Фантастический алгоритм невероятного успеха
Вот таких мы набили шишек, и настало время фантастического алгоритма, который я вам обещал. Важный момент: это фантастический алгоритм невероятного успеха для тех, кто не хочет использовать branch.io. К этому моменту мы узнали, что их бизнес стоит не то чтобы «ни на чём», кое-что они делают. Все эти хаки, которые мы сделали, в принципе, так или иначе поддерживаются в их SDK и инфраструктуре (но посоветовать я их не могу, потому что никогда не пробовал). Однако, если вы хотите всё сделать сами, если вам нравится всё кастомизировать или вам жалко денег, сейчас я расскажу, что делать.
Во-первых, придумайте схему и не забудьте, чтобы в схеме у вас был и ref, и страница. Тогда вы сможете красиво отображать каркасы и при этом вести любую статистику.
Второе. Очень простое, сделайте то, что Apple вам говорит: apple-app-site-association, домены и код.
И не забудьте в коде про canOpenUrl, проверьте, что вы реально можете это открыть, а иначе отправляйте в Safari.
Третье. Не забудьте про HTML-превью и Open Graph-теги, иначе всё будет выглядеть некрасиво. Кстати, хотя в этом докладе я тему Android не раскрываю, для фейсбука на Android нужно поддержать отдельные og-теги с андроидовской ссылкой. Кроме того для некоторых webview андроида можно оставить iframe с задержкой на перенаправление, вроде того о котором я рассказывал в начале.
В HTML-превью, опять же, не забудьте, что вам нужны два домена: один в кнопке, другой в самом превью.
Не забудьте про deferred deep linking (отложенный диплинк, то есть диплинк в ещё не установленное приложение). Здесь у нас AppsFlyer, а у вас может быть и ваше личное решение, которое позволит вам трекать установку и открывать контент после установки пользователем.
В общем-то, это всё, что вам нужно для строительства вашего дворца из подводных камней:
Но у меня есть ещё маленький бонус-трек, который я добавил в последний момент. Помните, как в ранней схеме, начиная с iOS 9, у нас выдавалась ошибка Safari в том случае, если у пользователя не установлено приложение? Это некрасиво, мы не хотим, чтобы пользователи видели ошибку. А помните этот слайд?
Мы рассылаем письма этим пользователям, и эта кнопка одновременно логинит пользователя (там есть токен для логина) и открывает контент.
Так вот: мы рассылаем эти письма конкретным пользователям, и мы знаем, установлено ли у них приложение и на каком устройстве. А это значит, что мы можем использовать старую схему для тех случаев, когда обращаемся к конкретным пользователям, выходящих с этих устройств. В этом случае у нас на сервере логика о том, установлено приложение или нет.
Соответственно, если приложение установлено, мы можем использовать старую схему. К ней не нужно столько хаков, и она с большей вероятностью сработает в Gmail-клиенте (который использует SafariViewController) и других. А всех остальных отправим на мобильную версию. Соответственно, новая версия фантастического алгоритма невероятного успеха имеет ещё один пункт:
Минутка рекламы. Если доклад вам понравился и хочется ещё подобного — 20–21 апреля пройдёт Mobius 2018 Piter, и в его программе тоже много интересного!