{"id":10153,"date":"2021-04-14T16:09:25","date_gmt":"2021-04-14T14:09:25","guid":{"rendered":"https:\/\/vived.io\/capacitor-push-notifications-tutorial\/"},"modified":"2021-04-14T16:09:25","modified_gmt":"2021-04-14T14:09:25","slug":"capacitor-push-notifications-tutorial","status":"publish","type":"post","link":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/","title":{"rendered":"Extended Push Notifications with Capacitor &#038; Firebase"},"content":{"rendered":"<p>Cze\u015b\u0107 <\/p>\n<p>Dzi\u015b widzimy si\u0119 w formacie jakiego na naszym blogu jeszcze nie by\u0142o. Chc\u0119 podzieli\u0107 si\u0119 z Wami naszymi prze\u017cyciami z pola bitwy, jakim by\u0142a implementacja rozbudowanych notyfikacji przy wykorzystaniu Capacitora. <\/p>\n<p><em>UWAGA: Materia\u0142 b\u0119dzie mia\u0142 charakter mocno instrukta\u017cowy, wi\u0119c je\u015bli nie planujecie podobnej funkcjonalno\u015bci w Waszych aplikacjach, to prawdopodobnie nie znajdziecie tu dzi\u015b wiele ciekawego. Pozosta\u0142ych zapraszam do lektury <\/em><\/p>\n<figure class=\"kg-card kg-image-card kg-card-hascaption\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>I niech moc b\u0119dzie z Wami!<\/figcaption><\/figure>\n<h1 id=\"wst-p\">Wst\u0119p<\/h1>\n<p>W grudniu 2020 roku zako\u0144czyli\u015bmy <a href=\"https:\/\/blog.vived.io\/keep-up-czyli-o-tym-jak-byc-na-biezaco-bez-uczucia-przytloczenia-contentem\/\">prac\u0119 nad pi\u0119kniejszym i bardziej funkcjonalnym Keep Upem<\/a>. Nowe funkcjonalno\u015bci pachnia\u0142y jeszcze jak \u015bwie\u017ce bu\u0142eczki, ale zdawali\u015bmy sobie ju\u017c spraw\u0119, \u017ce przed nami jeszcze jedna g\u00f3ra do pokonania. Jak wynika z naszych statystyk, <strong>ruch w Keep Upie pochodzi g\u0142\u00f3wnie z push notyfikacji<\/strong>.<\/p>\n<p>W czym wi\u0119c problem? Z wywiad\u00f3w, kt\u00f3re przeprowadzili\u015bmy z naszymi najwierniejszymi u\u017cytkownikami wynika\u0142o, \u017ce ma\u0142a, generyczna notyfikacja cz\u0119sto gubi\u0142a si\u0119 w t\u0142umie i nie zach\u0119ca\u0142a w szczeg\u00f3lny spos\u00f3b do interakcji. Ci\u0119\u017cko oszacowa\u0107, ilu dok\u0142adnie u\u017cytkownik\u00f3w polubi\u0142o Keep Up i ze wzgl\u0119du na s\u0142abe notyfikacje porzuci\u0142o nawyk czytania artyku\u0142\u00f3w, ale te same statystyki pozwala\u0142y s\u0105dzi\u0107, \u017ce jest ich ca\u0142kiem sporo. Nie mogli\u015bmy tego tak zostawi\u0107 i przeszli\u015bmy do dzia\u0142ania. W efekcie powsta\u0142 nowy design notyfikacji i pomys\u0142 na cotygodniowy raport, kt\u00f3ry czytaj\u0105cych zmotywuje do utrzymania nawyku, a zapominalskim przypomni o istnieniu aplikacji.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3964bde.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Ju\u017c na etapie wst\u0119pnej analizy i dzielenia zadania na mniejsze kawa\u0142ki zorientowali\u015bmy si\u0119, \u017ce sprawa nie b\u0119dzie prosta, a do pokonania mamy zar\u00f3wno problemy po stronie Frontendu jak i Backendu. W komunikacji z Firebase korzystali\u015bmy z starego REST API (kt\u00f3re sami nazwali\u015bmy v0 i tak b\u0119d\u0119 go tytu\u0142owa\u0142 dalej), kt\u00f3re nie umo\u017cliwia\u0142o personalizacji notyfikacji w zale\u017cno\u015bci od urz\u0105dzenia, na kt\u00f3re ma ona trafi\u0107. Sprawa by\u0142a o tyle istotna, \u017ce nazwy niekt\u00f3rych p\u00f3l kolidowa\u0142y ze sob\u0105, a pr\u00f3ba wys\u0142ania notyfikacji na urz\u0105dzenie z Androidem, przy dodaniu p\u00f3l potrzebnych na iOS, zazwyczaj ko\u0144czy\u0142a si\u0119 b\u0142\u0119dem. Migracja do REST API v1 wydawa\u0142a si\u0119 krokiem w odpowiedni\u0105 stron\u0119. Pozwala\u0142a nam w ko\u0144cu wycofa\u0107 si\u0119 z legacy API i dawa\u0142a du\u017co wi\u0119ksz\u0105 elastyczno\u015b\u0107. Jak si\u0119 p\u00f3\u017aniej okaza\u0142o, nowe API mia\u0142o te\u017c swoje wady: mechanizm device group nie by\u0142 jeszcze wspierany i stan ten utrzymywa\u0142 si\u0119 co najmniej od roku. Opr\u00f3cz migracji do nowego API, musieli\u015bmy wi\u0119c zmigrowa\u0107 jeszcze model device group na ci\u0119\u017cszy w utrzymaniu model z tokenami\u2026 No c\u00f3\u017c, mo\u017ce kiedy\u015b Google postanowi doda\u0107 wsparcie dla Device Group do API v1. My tymczasem wr\u00f3\u0107my do clue, czyli frontendowej strony implementacji.<\/p>\n<figure class=\"kg-card kg-image-card kg-card-hascaption\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3bc1b35.gif\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Moja reakcja kiedy pierwszy raz zobaczy\u0142em dokumentacj\u0119 FCM v0<\/figcaption><\/figure>\n<p>Tutaj r\u00f3wnie\u017c czeka\u0142o na nas kilka pu\u0142apek. Capacitor posiada absolutnie \u015bwietne API do obs\u0142ugi notyfikacji (swego czasu zmuszony by\u0142em do testowania r\u00f3\u017cnych wtyczek do Cordovy, kt\u00f3re pr\u00f3bowa\u0142y osi\u0105gn\u0105\u0107 ten sam cel wi\u0119c wiem, o czym m\u00f3wi\u0119), jest to jednak API do\u015b\u0107 prymitywne i nie spe\u0142niaj\u0105ce naszych potrzeb. Poszukiwania odpowiedniego Pluginu r\u00f3wnie\u017c zako\u0144czy\u0142y si\u0119 fiaskiem, a to oznacza\u0142o, \u017ce przed nami implementacja kawa\u0142ka natywnego kodu &#8211; sprawa prawdopodobnie b\u0142aha, je\u015bli dysponujecie zespo\u0142em Android i iOS deweloper\u00f3w, ale jest to spore wyzwanie dla Fullstack Developer\u00f3w z podstawow\u0105 wiedz\u0105 o Mobile Developmencie.<\/p>\n<p>Ko\u0144cz\u0105c ten przyd\u0142ugi wst\u0119p przejd\u017amy do meritum, czyli tutoriala dla wszystkich tych, kt\u00f3rzy podobne notyfikacje chcieliby zaimplementowa\u0107 w swoich aplikacjach. Odpalajcie terminal i zaczynamy \u200d<\/p>\n<h1 id=\"tutorial\">Tutorial<\/h1>\n<h2 id=\"wymagania-poczatkowe\" data-num=1>Wymagania pocz\u0105tkowe<\/h2>\n<p>W poni\u017cszym tutorialu skupimy si\u0119 na rozbudowie notyfikacji. Oznacza to, \u017ce pominiemy w nim konfiguracj\u0119 podstawowych notyfikacji i jest to zabieg celowy. \u015awietny tutorial dotycz\u0105cy natywnych aplikacji znajdziecie w <a href=\"https:\/\/capacitorjs.com\/docs\/guides\/push-notifications-firebase\">dokumentacji Capacitora<\/a>. Je\u015bli za\u015b chodzi o cz\u0119\u015b\u0107 webow\u0105, to polecam Wam te artyku\u0142y: <a href=\"https:\/\/medium.com\/mighty-ghost-hack\/angular-8-firebase-cloud-messaging-push-notifications-cc80d9b36f82\">Angular 8 + Firebase Cloud Messaging Push Notifications<\/a> i <a href=\"https:\/\/blog.logrocket.com\/push-notifications-with-react-and-firebase\/\">Push notifications with React and Firebase<\/a><\/p>\n<p>Je\u015bli w Waszej aplikacji macie ju\u017c skonfigurowane podstawowe notyfikacje, to mo\u017cemy rusza\u0107 dalej, a je\u015bli nie, to nie zwlekajcie d\u0142u\u017cej &#8211; My tu na Was cierpliwie poczekamy .<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3c42df6.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<h2 id=\"plugin-interface\" data-num=2>Plugin interface<\/h2>\n<p>Zar\u00f3wno implementacj\u0119 Androida jak i iOSa* oprzemy o w\u0142asny plugin. Oba pluginy b\u0119d\u0105 wsp\u00f3\u0142dzieli\u0107 jeden interfejs, wi\u0119c mo\u017cemy zacz\u0105\u0107 od przygotowania jego szkieletu.<\/p>\n<p>*o ten sam interfejs mo\u017cecie oprze\u0107 te\u017c webow\u0105 implementacj\u0119, ale wymaga\u0107 to b\u0119dzie napisania odpowiedniego adaptera przekszta\u0142caj\u0105cego interfejs Firebase na ten zasugerowany poni\u017cej. \u00a0<\/p>\n<pre><code class=\"language-typescript\">import { Plugins } from &#039;@capacitor\/core&#039;;\nimport { PluginListenerHandle } from &#039;@capacitor\/core&#039;;\nexport type NotificationData = {\n  data: {\n    url: string;\n    vivedId?: string;\n  };\n  action: &#039;TAP&#039; | &#039;KEEP_UP_READ&#039;;\n};\nexport type ExtendedPushNotificationsPlugin = {\n  addListener(\n    eventName: &#039;notificationClick&#039;,\n    listenerFunc: (info: NotificationData) =&gt; void\n  ): PluginListenerHandle;\n};\nexport const ExtendedPushNotificationsPlugin = Plugins.VivedExtendedPushNotificationsPlugin as ExtendedPushNotificationsPlugin;<\/code><\/pre>\n<h2 id=\"android\" data-num=3>Android<\/h2>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3cbdf27.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Zanim przejdziemy do implementacji, zatrzymajmy si\u0119 na chwil\u0119 przy stronie teoretycznej. Spos\u00f3b, w jaki Android obs\u0142uguje notyfikacje zale\u017cy od tego, czy aplikacja jest aktualnie uruchomiona oraz od formatu danych, jaki zosta\u0142 przekazany do notyfikacji po stronie serwera.<\/p>\n<p>Notyfikacje podzieli\u0107 mo\u017cemy na dwa typy: standardowe push notyfikacje i data notyfikacje. Te pierwsze to notyfikacje, kt\u00f3re chcemy wy\u015bwietli\u0107 u\u017cytkownikowi, natomiast te drugie s\u0142u\u017c\u0105 do przekazania aplikacji danych, niekoniecznie wy\u015bwietlaj\u0105c sam\u0105 notyfikacj\u0105. Jak Android obs\u0142uguje poszczeg\u00f3lne sytuacje, najlepiej opisuje zaczerpni\u0119ta z dokumentacji tabela.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3d34006.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>W standardowym przypadku, je\u015bli aplikacja jest zamkni\u0119ta, to tracimy mo\u017cliwo\u015b\u0107 manipulowania ni\u0105. Niestety oznacza to, \u017ce je\u015bli chcemy pokaza\u0107 u\u017cytkownikom \u0142adne notyfikacje, nawet kiedy aplikacja jest wy\u0142\u0105czona, to zmuszeni b\u0119dziemy wykorzysta\u0107 mechanizm data notifications i troch\u0119 namiesza\u0107.<\/p>\n<p>Android posiada rozbudowany wachlarz dost\u0119pnych typ\u00f3w notyfikacji, wi\u0119c zanim przejdziecie do tworzenia w\u0142asnej spersonalizowanej implementacji sprawd\u017acie, czy jedna z dost\u0119pnych nie spe\u0142nia Waszych wymaga\u0144. Warto zauwa\u017cy\u0107, \u017ce nie mo\u017cecie wymiesza\u0107 ze sob\u0105 r\u00f3\u017cnych typ\u00f3w notyfikacji. Oznacza to, \u017ce je\u015bli zdecydujecie si\u0119 na du\u017cy obrazek, to braknie Wam ju\u017c miejsca na du\u017cy tekst. Na t\u0119 zasad\u0119 nie pomo\u017ce pr\u00f3ba stworzenia swojego interfejsu notyfikacji, bo Android wprowadza odg\u00f3rny limit wysoko\u015bci notyfikacji.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3e66855.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p><\/p>\n<p>Przejd\u017amy teraz do cz\u0119\u015bci praktycznej. Zgodnie ze sztuk\u0105, aby obs\u0142u\u017cy\u0107 push notyfikacje, musimy stworzy\u0107 implementacj\u0119 interfejsu FirebaseMessagingService i zarejestrowa\u0107 go w AndroidManifest.xml (b\u0119d\u0105c dok\u0142adnym nale\u017ca\u0142oby powiedzie\u0107 przechwycimy intent, ale przejdziemy do tego jeszcze w dalszej cz\u0119\u015bci tutoriala). Uwaga: stworzenie w\u0142asnej implementacji serwisu spowoduje, \u017ce metody do obs\u0142ugi notyfikacji z Capacitora przestan\u0105 dzia\u0142a\u0107, wi\u0119c sami b\u0119dziecie musieli zaimplementowa\u0107 brakuj\u0105ce funkcjonalno\u015bci. Nie przejmujcie si\u0119 natomiast b\u0142\u0119dem dotycz\u0105cym braku obs\u0142ugi rejestracji tokenu. Ta cz\u0119\u015b\u0107 jest \u015bwietnie obs\u0142ugiwana przez Capacitora i mo\u017cecie pozostawi\u0107 j\u0105 nietkni\u0119t\u0105.<\/p>\n<figure class=\"kg-card kg-image-card kg-card-hascaption\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b410fe3e.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania Androidowej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure>\n<pre><code class=\"language-kotlin\">\/\/ Note: onNewToken is handled by Capacitor&#039;s Push Notifications plugin\nclass PushNotificationsService : FirebaseMessagingService() {\n  override fun onMessageReceived(message: RemoteMessage) {\n    val rawData = RawNotificationData.parse(message)\n    val notificationData = rawData.enrich()\n    val notification = buildNotification(notificationData)\n    sendNotification(rowData.id, notification)\n  }\n}<\/code><\/pre>\n<pre><code class=\"language-XML\">&lt;application&gt;\n    &lt;service\n      android:name=&quot;com.virtuslab.vived.PushNotifications&quot;\n      android:stopWithTask=&quot;false&quot;&gt;\n      &lt;intent-filter&gt;\n        &lt;action android:name=&quot;com.google.firebase.MESSAGING_EVENT&quot; \/&gt;\n      &lt;\/intent-filter&gt;\n    &lt;\/service&gt;\n&lt;\/application&gt;<\/code><\/pre>\n<p>Metoda `onMessageReceived` wype\u0142niona jest jeszcze niezaimplementowanymi metodami, kt\u00f3re pokazuj\u0105 schemat jej dzia\u0142ania. Przyjrzymy si\u0119 teraz dok\u0142adnie ka\u017cdej z nich, zaczynaj\u0105c od (1). Jako \u017ce nasze dane przekazujemy za pomoc\u0105 JSONa, musimy je odpowiednio sparsowa\u0107 do klasy, kt\u00f3ra b\u0119dzie zrozumiana dla naszej aplikacji. Schemat JSONa z g\u00f3ry narzucony jest przez firebase. Wyj\u0105tkiem w tej kwestii jest pole `data`, kt\u00f3re mo\u017cemy dowolnie personalizowa\u0107, wi\u0119c mo\u017cecie pu\u015bci\u0107 wodze fantazji. My starali\u015bmy si\u0119 przygotowa\u0107 API jak najbardziej elastyczne i kompatybilne z przysz\u0142ymi zmianami.<\/p>\n<figure class=\"kg-card kg-code-card\">\n<pre><code class=\"language-JSON\">method: POST\npath: &quot;https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send&quot;\nbody: {\n  &quot;message&quot;: {\n    &quot;name&quot;: &quot;69ab68da-1dae-4c25-9386-cca6346123a8&quot;,\n    &quot;token&quot;: &quot;firebase-token&quot;,\n    &quot;notification&quot;: null,\n    &quot;data&quot;: {\n      &quot;url&quot;: &quot;\/hub\/keep-up&quot;,\n      &quot;vivedId&quot;: &quot;vived-id&quot;\n    },\n    &quot;android&quot;: {\n      &quot;data&quot;: {\n        &quot;title&quot;: &quot;Keep Up with IT World &quot;,\n        &quot;body&quot;: &quot;Read all articles selected for you today:\\n\u2022 \\&quot;Easily create web extensions for Safari\\&quot;\\n\u2022...&quot;,\n        &quot;imageUrl&quot;: &quot;https:\/\/uploads.vived.io\/images\/static\/notifications\/image\/rocket.png&quot;,\n        &quot;iconUrl&quot;: &quot;https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png&quot;,\n        &quot;type&quot;: &quot;BigText&quot;,\n        &quot;actions&quot;: [\n          {\n            &quot;action&quot;: &quot;KEEP_UP_READ&quot;,\n            &quot;title&quot;: &quot;Read now&quot;\n          }\n        ]\n      },\n    },\n    &quot;apns&quot;: { ... },\n    &quot;webpush&quot;: { ... },\n}<\/code><\/pre><figcaption>Zapytanie wysy\u0142ane po strone backendu do Firebase (okrojone tylko do niezb\u0119dnej cz\u0119\u015bci)<\/figcaption><\/figure>\n<pre><code class=\"language-Kotlin\">enum class NotificationType {\n  Image, BigText, Default\n}\ndata class NotificationAction(\n  val id: String,\n  val title: String\n) {\n  companion object {\n    fun parseJson(json: JSONObject): NotificationAction =\nGson().fromJson(str , Array&lt;NotificationAction&gt;::class.java)\n  }\n}\ndata class RawNotificationData(\n  val id: Int = Random.nextInt(1, 1000000),\n  val title: String?,\n  val body: String?,\n  val actions: List&lt;NotificationAction&gt;,\n  val imageUrl: String?,\n  val iconUrl: String?,\n  val type: NotificationType?,\n  val vivedId: String?,\n  val url: String?\n)\n  companion object {\n    fun parse(message: RemoteMessage): RawNotificationData =\n      RawNotificationData(\n        title = message.data[&quot;title&quot;] ?: message.notification?.title,\n        body = message.data[&quot;body&quot;] ?: message.notification?.body,\n        actions = message.data[&quot;actions&quot;]?.let { NotificationAction.parseJsonArray(JSONArray(it)) } ?: emptyList(),\n        imageUrl = message.data[&quot;imageUrl&quot;],\n        iconUrl = message.data[&quot;iconUrl&quot;],\n        type = NotificationType.values().find { it.name == message.data[&quot;type&quot;] } ?: NotificationType.Default,\n        vivedId = message.data[&quot;vivedId&quot;],\n        url = message.data[&quot;url&quot;]\n      )\n  }\n}<\/code><\/pre>\n<p>W kroku (2) odczytane dane wzbogacamy o dodatkowe informacje. W naszym przypadku jest to np. obrazek pobrany z sieci. Je\u015bli wystarcz\u0105 Wam suche dane, to spokojnie mo\u017cecie pomin\u0105\u0107 ten krok.<\/p>\n<pre><code class=\"language-Kotlin\">data class NotificationData(\n  val id: Int,\n  val title: String,\n  val body: String,\n  val actions: List&lt;NotificationAction&gt;,\n  val image: Bitmap?,\n  val icon: Bitmap?,\n  val type: NotificationType?,\n  val vivedId: String?,\n  val url: String\n)\ndata class RawNotificationData(\n  ...\n) {\n  private fun downloadBitmap(url: String): Bitmap? {\n    return try {\n      val input = URL(url).openStream()\n      BitmapFactory.decodeStream(input)\n    } catch (e: IOException) {\n      null\n    }\n  }\n  fun enrich(): NotificationData =\n    NotificationData(\n      id = id,\n      title = title ?: &quot;&quot;,\n      body = body ?: &quot;&quot;,\n      actions = actions,\n      image = imageUrl?.let { downloadBitmap(it) },\n      icon = iconUrl?.let { downloadBitmap(it) },\n      type = type,\n      vivedId = vivedId,\n      url = url ?: &quot;\/&quot;\n    )\n}<\/code><\/pre>\n<p>Na tym etapie mamy ju\u017c wszystkie potrzebne dane i mo\u017cemy przej\u015b\u0107 do zbudowania notyfikacji (3). Ponownie nasza implementacja stara si\u0119 by\u0107 maksymalnie elastyczna, ale je\u015bli chcecie wprowadzi\u0107 swoje modyfikacje, to najlepiej b\u0119dzie je\u015bli zag\u0142\u0119bicie si\u0119 w mo\u017cliwo\u015bci, jakie daje Android w <a href=\"https:\/\/developer.android.com\/training\/notify-user\/build-notification\">dokumentacji.<\/a><\/p>\n<pre><code class=\"language-Kotlin\">  private fun buildPendingIntent(data: NotificationData, actionId: String): PendingIntent {\n    val intent = PushNotificationIntent.from(data, actionId)\n    return PendingIntent.getActivity(this, Random.nextInt(), intent.toIntent(this), PendingIntent.FLAG_CANCEL_CURRENT)\n  }\n  private fun buildNotification(data: NotificationData): Notification {\n    val notificationBuilder = NotificationCompat.Builder(this, PushNotificationsPlugin.DEFAULT_CHANNEL_ID)\n      .setContentTitle(data.title)\n      .setContentText(data.body)\n      .setAutoCancel(true)\n      .setSmallIcon(R.drawable.ic_stat_notification)\n      .setContentIntent(buildPendingIntent(data, &quot;TAP&quot;))\n    data.actions.forEach { action -&gt;\n      notificationBuilder.addAction(\n        R.drawable.ic_stat_notification,\n        action.title,\n        buildPendingIntent(data, action.id)\n      )\n    }\n    return notificationBuilder.build()\n  }<\/code><\/pre>\n<p>W powy\u017cszym kodzie znajduj\u0105 si\u0119 dwie zupe\u0142nie nowe rzeczy, nad kt\u00f3rymi warto si\u0119 pochyli\u0107. Zacznijmy od tajemniczych `PendingIntet.` Kiedy u\u017cytkownik kliknie w notyfikacj\u0119 lub jedn\u0105 z akcji, to do naszej aplikacji zostanie wys\u0142any obiekt PendingIntent, kt\u00f3ry nale\u017cy przechwyci\u0107 i obs\u0142u\u017cy\u0107. Odpowiedzialny b\u0119dzie za to `PushNotificationsPlugin`. Jako \u017ce logik\u0119 chcemy przesun\u0105\u0107 w stron\u0119 JavaScriptu, to jedynym zadaniem Pluginu b\u0119dzie zamkni\u0119cie notyfikacji i przekazanie eventu do WebView.<\/p>\n<pre><code class=\"language-Kotlin\">data class PushNotificationIntent(\n  val notificationId: Int,\n  val vivedNotificationId: String?,\n  val actionId: String,\n  val url: String\n) {\n  companion object {\n    const val IS_FROM_PLUGIN_EXTRA = &quot;isFromPushNotificationPlugin&quot;\n    const val NOTIFICATION_ID_EXTRA = &quot;notificationId&quot;\n    const val VIVED_NOTIFICATION_ID_EXTRA = &quot;vivedNotificationId&quot;\n    const val ACTION_ID_EXTRA = &quot;actionId&quot;\n    const val URL_EXTRA = &quot;url&quot;\n    fun from(intent: Intent): PushNotificationIntent =\n      PushNotificationIntent(\n        notificationId = intent.extras?.getInt(NOTIFICATION_ID_EXTRA)!!,\n        vivedNotificationId = intent.extras?.getString(VIVED_NOTIFICATION_ID_EXTRA),\n        actionId = intent.extras?.getString(ACTION_ID_EXTRA)!!,\n        url = intent.extras?.getString(URL_EXTRA)!!\n      )\n    fun from(data: NotificationData, actionId: String): PushNotificationIntent =\n      PushNotificationIntent(\n        notificationId = data.id,\n        vivedNotificationId = data.vivedId,\n        actionId = actionId,\n        url = data.url\n      )\n  }\n  fun toIntent(context: Context): Intent =\n    Intent(context, MainActivity::class.java)\n      .putExtra(IS_FROM_PLUGIN_EXTRA, true)\n      .putExtra(ACTION_ID_EXTRA, actionId)\n      .putExtra(NOTIFICATION_ID_EXTRA, notificationId)\n      .putExtra(VIVED_NOTIFICATION_ID_EXTRA, vivedNotificationId)\n      .putExtra(URL_EXTRA, url)\n}\n@NativePlugin(name = &quot;VivedPushNotificationsPlugin&quot;)\nclass PushNotificationsPlugin : Plugin() {\n  private fun shouldHandleIntent(intent: Intent?): Boolean =\n    intent?.extras?.getBoolean(PushNotificationIntent.IS_FROM_PLUGIN_EXTRA) == true\n  private fun notifyJS(intent: PushNotificationIntent) {\n    val dataObject = JSObject()\n    dataObject.put(&quot;url&quot;, intent.url)\n    dataObject.put(&quot;vivedId&quot;, intent.vivedNotificationId)\n    val returnObject = JSObject()\n    returnObject.put(&quot;data&quot;, dataObject)\n    returnObject.put(&quot;action&quot;, intent.actionId)\n    notifyListeners(&quot;notificationClick&quot;, returnObject, true)\n  }\n  private fun cancelNotification(id: Int) {\n    val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n    notificationManager.cancel(id)\n  }\n  override fun handleOnNewIntent(intent: Intent?) {\n    super.handleOnNewIntent(intent)\n    if (shouldHandleIntent(intent)) {\n      val pushNotificationIntent = PushNotificationIntent.from(intent!!)\n      notifyJS(pushNotificationIntent)\n      cancelNotification(pushNotificationIntent.notificationId)\n    }\n  }<\/code><\/pre>\n<p>Przejd\u017amy teraz do drugiej tajemniczej funkcjonalno\u015bci, czyli `PushNotificationsPlugin.DEFAULT_CHANNEL_ID`. Android od wersji 8.0 (API level 26) wprowadzi\u0142 kana\u0142y notyfikacji, kt\u00f3rymi mo\u017cna sterowa\u0107 z poziomu ustawie\u0144 aplikacji. <\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b41936bc.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Tworzenie notyfikacji bez podania kana\u0142u, jest co prawda mo\u017cliwe, ale jest ju\u017c oznaczone jako deprecated. Aby mie\u0107 pewno\u015b\u0107, \u017ce kana\u0142 notyfikacji zostanie stworzony, funkcjonalno\u015b\u0107 ta zostanie dodana do odpowiedniego hook\u2019a w stworzonym wcze\u015bniej Pluginie<\/p>\n<pre><code class=\"language-Kotlin\">@NativePlugin(name = &quot;VivedPushNotificationsPlugin&quot;)\nclass PushNotificationsPlugin : Plugin() {\n  companion object {\n    const val DEFAULT_CHANNEL_ID = &quot;vived_default_notifications_channel&quot;\n  }\n  private fun createNotificationChannel() {\n    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {\n      val importance = NotificationManager.IMPORTANCE_HIGH\n      val notificationChannel = NotificationChannel(DEFAULT_CHANNEL_ID, &quot;Default&quot;, importance)\n      val notificationManager = this.activity.getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) as NotificationManager\n      notificationManager.createNotificationChannel(notificationChannel)\n    }\n  }\n  override fun load() {\n    createNotificationChannel()\n  }\n  ....\n}<\/code><\/pre>\n<p>Tak jak wspomnia\u0142em na pocz\u0105tku, wersje notyfikacji dostarczone przez Androida w zupe\u0142no\u015bci nam wystarczy\u0142y, ale dla wszystkich, kt\u00f3rzy oczekuj\u0105 wi\u0119cej, mam ma\u0142e preview takiej funkcjonalno\u015bci.<\/p>\n<p>Je\u015bli p\u00f3jdziecie w ca\u0142kowicie personalizowan\u0105 notyfikacj\u0119, to pami\u0119tajcie o <a href=\"https:\/\/developer.android.com\/training\/notify-user\/custom-notification#:~:text=The%20height%20available%20for%20a,are%20limited%20to%20256%20dp.\">ograniczeniu wysoko\u015bci widoku<\/a> i mo\u017cliwo\u015bci skorzystania tylko z <a href=\"https:\/\/developer.android.com\/reference\/android\/widget\/RemoteViews\">wybranych element\u00f3w UI<\/a>, a tak\u017ce o odg\u00f3rnym ograniczeniu wysoko\u015bci notyfikacji.<\/p>\n<p>W celach edukacyjnych nasza notyfikacja b\u0119dzie wy\u015bwietla\u0107 tylko prosty tekst. Prawdopodobnie od swojej notyfikacji oczekiwa\u0107 b\u0119dziecie jednak troch\u0119 wi\u0119cej, ale w tym celu b\u0119dziecie musieli zanurzy\u0107 si\u0119 w niuansach tworzenia androidowych widok\u00f3w.<\/p>\n<pre><code class=\"language-XML\">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;\n&lt;LinearLayout xmlns:android=&quot;http:\/\/schemas.android.com\/apk\/res\/android&quot;\n  android:orientation=&quot;vertical&quot; android:layout_width=&quot;match_parent&quot;\n  android:layout_height=&quot;match_parent&quot;&gt;\n  &lt;TextView\n    android:id=&quot;@+id\/text_view_id&quot;\n    android:layout_height=&quot;wrap_content&quot;\n    android:layout_width=&quot;wrap_content&quot;\n    android:text=&quot;Hello there Obi-Wan Kenobi&quot; \/&gt;\n&lt;\/LinearLayout&gt;<\/code><\/pre>\n<pre><code class=\"language-Kotlin\">\/\/ Get the layouts to use in the custom notification\nval notificationLayoutExpanded = RemoteViews(packageName, R.layout.notification_large)\n\/\/ Apply the layouts to the notification\nval customNotification = NotificationCompat.Builder(context, CHANNEL_ID)\n        .setSmallIcon(R.drawable.notification_icon)\n        .setStyle(NotificationCompat.DecoratedCustomViewStyle())\n        .setCustomBigContentView(notificationLayoutExpanded)\n        .build()<\/code><\/pre>\n<p>Ufff\u2026 Uda\u0142o nam si\u0119 zaimplementowa\u0107 cz\u0119\u015b\u0107 androidow\u0105, wi\u0119c mo\u017cemy rusza\u0107 dalej.<\/p>\n<h2 id=\"web-pwa\" data-num=4>Web\/PWA<\/h2>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b42a0a9a.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Je\u015bli chodzi o push notyfikacje na webie, to niestety wci\u0105\u017c mo\u017cemy m\u00f3wi\u0107 tutaj tylko o Webowym Androidzie. Pomimo niezliczonych pr\u00f3\u015bb i wniosk\u00f3w nic nie wskazuje na to, \u017ce Apple zacznie patrze\u0107 na t\u0105 funkcjonalno\u015b\u0107 chocia\u017c odrobin\u0119 przychylniej.<\/p>\n<figure class=\"kg-card kg-image-card kg-card-hascaption\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4311c88.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania Webowej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure>\n<p>Je\u015bli m\u00f3wimy natomiast o Webowym Androidzie, to niesamowicie wa\u017cne jest u\u015bwiadomienie sobie, \u017ce obowi\u0105zuj\u0105 tutaj te same zasady, co przy natywnych androidowych notyfikacjach. Mamy wi\u0119c te same mo\u017cliwo\u015bci konfiguracji, ale opakowane w zdecydowanie elastyczniejsze API. Wszystkie parametry mo\u017cemy przekazywa\u0107 w zapytaniu po stronie backendu. A\u017c dziwne, \u017ce Android nie wspiera podobnego API.<\/p>\n<p>Niestety web push notifications nie wspieraj\u0105 aktualnie \u017cadnych dodatkowych mo\u017cliwo\u015bci personalizacji.<\/p>\n<pre><code class=\"language-JSON\">method: POST\npath: &quot;https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send&quot;\nbody: {\n  &quot;message&quot;: {\n    &quot;name&quot;: &quot;69ab68da-1dae-4c25-9386-cca6346123a8&quot;,\n    &quot;token&quot;: &quot;firebase-token&quot;,\n    &quot;notification&quot;: null,\n    &quot;data&quot;: {\n      &quot;url&quot;: &quot;\/hub\/keep-up&quot;,\n      &quot;vivedId&quot;: &quot;vived-id&quot;\n    },\n    &quot;android&quot;: { ... },\n    &quot;apns&quot;: { ... },\n    &quot;webpush&quot;: {\n      &quot;notification&quot;: {\n        &quot;title&quot;: &quot;Keep Up with IT World &quot;,\n        &quot;body&quot;: &quot;Read all articles selected for you today:\\n\u2022 \\&quot;Easily create web extensions for Safari\\&quot;\\n\u2022...&quot;,\n        &quot;actions&quot;: [\n          {\n            &quot;action&quot;: &quot;KEEP_UP_READ&quot;,\n            &quot;title&quot;: &quot;Read now&quot;\n          }\n        ],\n        &quot;badge&quot;: &quot;https:\/\/uploads.vived.io\/images\/static\/notifications\/badge\/vived-badge.png&quot;,\n        &quot;icon&quot;: &quot;https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png&quot;,\n        &quot;image&quot;: &quot;https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png&quot;\n      }\n    },\n}<\/code><\/pre>\n<p>Zapytanie wysy\u0142ane po strone backendu do Firebase (okrojone tylko do niezb\u0119dnej cz\u0119\u015bci)<\/p>\n<p>Warto zwr\u00f3ci\u0107 uwag\u0119, \u017ce typem notyfikacji sterujemy, poprzez to kt\u00f3re pola pozostawimy wype\u0142nione.<\/p>\n<pre><code class=\"language-TypeScript\">declare const self: ServiceWorkerGlobalScope;\nconst openUrl = async (url: string): Promise&lt;void&gt; =&gt; {\n  const windowClients = (await self.clients.matchAll({\n    type: &#039;window&#039;,\n  })) as WindowClient[];\n  const activeClient = windowClients.find(\n    it =&gt; it.visibilityState === &#039;visible&#039;\n  );\n  if (activeClient) {\n    await activeClient.navigate(url);\n  } else if (self.clients.openWindow) {\n    await self.clients.openWindow(url);\n  }\n};\nexport const initializePushNotifications = (): void =&gt; {\n  self.addEventListener(&#039;notificationclick&#039;, event =&gt; {\n    const notification: MessagePayload = event.notification.data.FCM_MSG;\n    const data = notification.data as PushNotificationData;\n    event.notification.close(); \/\/ Android needs explicit close.\n    event.waitUntil(openUrl(data.url));\n  });\n};<\/code><\/pre>\n<h2 id=\"ios\" data-num=5>iOS<\/h2>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b435e2fa.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Dobrn\u0119li\u015bmy do ostatniej wspieranej przez nas platformy i jednocze\u015bnie tej, kt\u00f3ra sprawi\u0142a nam najwi\u0119cej problem\u00f3w. Na szcz\u0119\u015bcie nie ze wzgl\u0119du na s\u0142abe API, a na nasze skromne umiej\u0119tno\u015bci :).<\/p>\n<p>Podstawowe notyfikacje na iOS wspieraj\u0105 tylko nag\u0142\u00f3wek i tekst. Dodaj\u0105c pewn\u0105 modyfikacj\u0119 po stronie natywnej aplikacji, funkcjonalno\u015b\u0107 t\u0105 mo\u017cemy rozszerzy\u0107 o obrazek.<\/p>\n<p>Je\u015bli to dla Was za ma\u0142o, to mamy te\u017c mo\u017cliwo\u015b\u0107 dowolnej personalizacji rozwini\u0119tej notyfikacji przez dostarczenie natywnego widoku. I w tym przypadku m\u00f3wimy o naprawd\u0119 dowolnej personalizacji, bo nie mamy tutaj ogranicze\u0144 podobnych do tych z Androida.<\/p>\n<figure class=\"kg-card kg-image-card kg-card-hascaption\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b43962fa.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania iOS-owej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure>\n<pre><code class=\"language-JSON\">method: POST\npath: &quot;https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send&quot;\nbody: {\n  &quot;message&quot;: {\n    &quot;name&quot;: &quot;69ab68da-1dae-4c25-9386-cca6346123a8&quot;,\n    &quot;token&quot;: &quot;firebase-token&quot;,\n    &quot;notification&quot;: null,\n    &quot;data&quot;: {\n      &quot;url&quot;: &quot;\/hub\/keep-up&quot;,\n      &quot;vivedId&quot;: &quot;vived-id&quot;\n    },\n    &quot;android&quot;: { ... },\n    &quot;apns&quot;: {\n      &quot;payload&quot;: {\n        &quot;aps&quot;: {\n          &quot;alert&quot;: {\n            &quot;title&quot;: &quot;Keep Up with IT World &quot;,\n            &quot;subtitle&quot;: null,\n            &quot;body&quot;: &quot;Read all articles selected for you today:\\n\u2022 \\&quot;Easily create web extensions for Safari\\&quot;\\n\u2022...&quot;\n          },\n          &quot;category&quot;: &quot;KEEP_UP&quot;,\n          &quot;mutableContent&quot;: 1\n        },\n        &quot;imageUrl&quot;: &quot;https:\/\/uploads.vived.io\/images\/static\/notifications\/image\/rocket.png&quot;,\n        &quot;body&quot;: &quot;Read all articles selected for you today:\\n\u2022 \\&quot;Easily create web extensions for Safari\\&quot;\\n\u2022...&quot;\n      }\n    },\n    &quot;webpush&quot;: { ... },\n}<\/code><\/pre>\n<p>Zacznijmy od nadpisania obs\u0142ugi notyfikacji dostarczonej przez Capacitora. W tym celu musimy stworzy\u0107 w\u0142asny plugin i zarejestrowa\u0107 go jako instancj\u0119 obs\u0142uguj\u0105c\u0105 notyfikacje.<\/p>\n<pre><code class=\"language-Swift\">@objc(ExtendedPushNotificationsPlugin)\npublic class ExtendedPushNotificationsPlugin: CAPPlugin, UNUserNotificationCenterDelegate {\n    private func notifyJS(response: UNNotificationResponse) {\n        var notificationAction: String;\n        switch(response.actionIdentifier) {\n        case &quot;KEEP_UP_READ&quot;:\n            notificationAction = &quot;KEEP_UP_READ&quot;\n        default:\n            notificationAction = &quot;TAP&quot;\n        }\n        let notificationData = [\n            &quot;url&quot;: response.notification.request.content.userInfo[&quot;url&quot;],\n            &quot;vivedId&quot;: response.notification.request.content.userInfo[&quot;vivedId&quot;]\n        ];\n        self.notifyListeners(&quot;notificationClick&quot;, data: [\n            &quot;data&quot;: notificationData,\n            &quot;action&quot;: notificationAction\n        ], retainUntilConsumed: true);\n    }\n    private func registerCustomNotificationCategories() {\n        let readNowAction = UNNotificationAction(\n            identifier: &quot;KEEP_UP_READ&quot;,\n            title: &quot;Read Now&quot;,\n            options: [UNNotificationActionOptions.foreground]\n        )\n        \/\/ Define the notification type\n        let dailyKeepUpNotificationCategory = UNNotificationCategory(\n            identifier: &quot;KEEP_UP&quot;,\n            actions: [readNowAction],\n            intentIdentifiers: [],\n            hiddenPreviewsBodyPlaceholder: &quot;&quot;,\n            options: []\n        )\n        \/\/ Register the notification type.\n        let notificationCenter = UNUserNotificationCenter.current()\n        notificationCenter.setNotificationCategories([dailyKeepUpNotificationCategory])\n    }\n    @objc override public func load() {\n        UNUserNotificationCenter.current().delegate = self\n        registerCustomNotificationCategories()\n    }\n    public func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                       didReceive response: UNNotificationResponse,\n                                       withCompletionHandler completionHandler:\n                                        @escaping () -&gt; Void) {\n        completionHandler()\n        notifyJS(response: response)\n    }\n    @objc public func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                             willPresent notification: UNNotification,\n                                             withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -&gt; Void) {\n        completionHandler([\n            .badge,\n            .sound,\n            .alert\n        ])\n    }<\/code><\/pre>\n<p>Rzecz\u0105, kt\u00f3ra mo\u017ce wzbudza\u0107 Wasze w\u0105tpliwo\u015bci, jest r\u0119czne tworzenie akcji. W odr\u00f3\u017cnienia od Androida i Weba tutaj nie mamy mo\u017cliwo\u015bci spersonalizowania akcji po stronie serwera i nale\u017cy dokona\u0107 tego po stronie klienta. Jest to rozwi\u0105zanie, kt\u00f3re niestety blokuje nam mo\u017cliwo\u015b\u0107 dodania kolejnych przycisk\u00f3w po wypuszczeniu aplikacji, ale z drugiej strony pozwala nam ca\u0142kowicie unikn\u0105\u0107 niedzia\u0142aj\u0105cych przycisk\u00f3w (ich brak uwa\u017cam za lepsze zachowanie).<\/p>\n<p>Nasz plugin obs\u0142uguje ju\u017c notyfikacje, wi\u0119c mo\u017cemy przej\u015b\u0107 do wzbogacenia ich o obrazek. W tym celu musimy stworzy\u0107 NotificationServiceExtension, kt\u00f3ry b\u0119dzie modyfikowa\u0142 notyfikacj\u0119 zanim ta zostanie wy\u015bwietlona.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b43e21e7.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b45cc6c6.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b46d47a7.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Podobnie jak w przypadku Androida, tak i tutaj b\u0119dziemy chcieli wzbogaci\u0107 dane otrzymane od backendu. Tak samo jak poprzednio, b\u0119dzie to pobranie zdj\u0119cia, ale nic nie stoi na przeszkodzie, aby by\u0142o to np. dodanie tekstu do notyfikacji. Za modyfikacj\u0119 odpowiedzialna b\u0119dzie metoda `didReceive` z `NotificationService`.<\/p>\n<p>Note: Przekazanie notyfikacji do UNNotificationServiceExtension zale\u017cy od tego, czy po stronie serwera zdefiniujemy parametr `mutableContent`: 1.<\/p>\n<pre><code class=\"language-Swift\">import UserNotifications\nclass NotificationService: UNNotificationServiceExtension {\n    var contentHandler: ((UNNotificationContent) -&gt; Void)?\n    var bestAttemptContent: UNMutableNotificationContent?\n    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -&gt; Void) {\n        self.contentHandler = contentHandler\n        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)\n        guard let bestAttemptContent = bestAttemptContent,\n              let attachmentURLAsString = bestAttemptContent.userInfo[&quot;imageUrl&quot;] as? String,\n              let attachmentURL = URL(string: attachmentURLAsString) else {\n            return\n        }\n        addImageToAttachments(url: attachmentURL) { (attachment) in\n            if let attachment = attachment {\n                bestAttemptContent.attachments = [attachment]\n                contentHandler(bestAttemptContent)\n            }\n        }\n    }\n}<\/code><\/pre>\n<p>W pierwszym kroku odpakowujemy otrzymane dane, korzystaj\u0105c z mechanizmu <a href=\"https:\/\/docs.swift.org\/swift-book\/ReferenceManual\/Statements.html#grammar_if-statement\">guard statement.<\/a> \u00a0Nast\u0119pnie pobieramy asynchronicznie obrazek i dodajemy go do za\u0142\u0105cznik\u00f3w naszej notyfikacji. Na koniec korzystaj\u0105c z mechanizmu if-let, odpakowujemy wynik operacji i przekazujemy nasz\u0105 notyfikacj\u0119 do kolejnego handlera.<\/p>\n<pre><code class=\"language-Swift\">    private func addImageToAttachments(url: URL, with completitionHandler: @escaping (UNNotificationAttachment?) -&gt; Void) {\n        let task = URLSession.shared.downloadTask(with: url) { (downloadedUrl, response, error) in\n            guard let downloadedUrl = downloadedUrl else {\n                completitionHandler(nil)\n                return\n            }\n            let uniqueURLEnding = ProcessInfo.processInfo.globallyUniqueString + &quot;.jpg&quot;\n            let urlPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(uniqueURLEnding)\n            try? FileManager.default.moveItem(at: downloadedUrl, to: urlPath)\n            do {\n                let attachment = try UNNotificationAttachment(identifier: &quot;picture&quot;, url: urlPath, options: nil)\n                completitionHandler(attachment)\n            }\n            catch {\n                completitionHandler(nil)\n            }\n        }\n        task.resume()\n    }<\/code><\/pre>\n<p>Pobieranie zaczynamy od wywo\u0142ania w\u0142a\u015bciwego zasobu z sieci. Nast\u0119pnie odczytujemy adres, do jakiego pobrany zosta\u0142 obraz i kopiujemy go do tymczasowego pliku. Na koniec wywo\u0142ujemy completitionHandler.<\/p>\n<p>Tip: Je\u015bli nie mo\u017cecie za\u0142apa\u0107 koncepcji `completitionHandler`, to potraktujcie go jako odpowiednik Promise.resolve().<\/p>\n<p>Je\u015bli na tym etapie wy\u015blecie notyfikacj\u0119, to powinna ona zawiera\u0107 pobrany przez nas obrazek. Je\u015bli natomiast na tym nie ko\u0144cz\u0105 si\u0119 Wasze wymagania, to iOS umo\u017cliwia nam przygotowanie dowolnego widoku dla rozwini\u0119tej notyfikacji. W tym celu musimy stworzy\u0107 kolejny Application Target: Notification Content Extension.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b481e708.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b49cc743.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4b374a6.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Zanim zaczniemy implementacj\u0119 widoku, nale\u017cy doda\u0107 do pliku info.plist UNNotificationExtensionCategory zgodne z tym, kt\u00f3re definiujemy po stronie backendu.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4c72574.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Tworzenie natywnych widok\u00f3w przypomina troch\u0119 czarn\u0105 magi\u0119, polegaj\u0105c\u0105 na \u0142\u0105czeniu mi\u0119dzy sob\u0105 r\u00f3\u017cnych element\u00f3w i definiowaniu margines\u00f3w. Jest to proces na tyle skomplikowany, \u017ce gdybym chcia\u0142 dobrze go opisa\u0107, to musia\u0142bym kilkukrotnie rozszerzy\u0107 ten tutorial. Z tego wzgl\u0119du wszystkich zainteresowanych odsy\u0142am do innego tutoriala, kt\u00f3ry zrobi\u0142 to dobrze. W tym miejscu podziel\u0119 si\u0119 natomiast samym kodem definiuj\u0105cym logik\u0119 (tej na szcz\u0119\u015bcie nie ma du\u017co, bo ustawiamy tylko warto\u015bci poszczeg\u00f3lnym elementom interfejsu).<\/p>\n<pre><code class=\"language-Swift\">import UIKit\nimport UserNotifications\nimport UserNotificationsUI\nclass NotificationViewController: UIViewController, UNNotificationContentExtension {\n    @IBOutlet weak var titleLabel: UILabel!\n    @IBOutlet weak var bodyLabel: UILabel!\n    @IBOutlet weak var imageView: UIImageView!\n    override func viewDidLoad() {\n        super.viewDidLoad()\n    }\n    func didReceive(_ notification: UNNotification) {\n        self.titleLabel.text = notification.request.content.title\n        self.bodyLabel.text = notification.request.content.userInfo[&quot;longBody&quot;] as? String ?? notification.request.content.body\n        let attachments = notification.request.content.attachments\n        for attachment in attachments {\n            if attachment.identifier == &quot;picture&quot; &amp;&amp; attachment.url.startAccessingSecurityScopedResource() {\n                guard let data = try? Data(contentsOf: attachment.url) else {\n                    return\n                }\n                imageView.image = UIImage(data: data)\n            }\n        }\n    }\n}<\/code><\/pre>\n<h1 id=\"podsumowanie\">Podsumowanie<\/h1>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4e8af35.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure>\n<p>Mam nadziej\u0119, \u017ce wszyscy wygl\u0105dacie teraz tak samo jak Anakin na gifie powy\u017cej! Je\u015bli nie to niech moc b\u0119dzie z Wami\u2026 Notyfikacje to jeden z tych temat\u00f3w, kt\u00f3re na papierze wydaj\u0105 si\u0119 banalnie proste, a przy pr\u00f3bie implementacji okazuj\u0105 si\u0119 r\u00f3wnie zawi\u0142e, co dzieje rodu Skywalker\u00f3w w uniwersum Gwiezdnych Wojen. Mam nadziej\u0119, \u017ce praca w\u0142o\u017cona w ten tutorial, chocia\u017c odrobin\u0119 u\u0142atwi Wam prac\u0119 i sprawi, \u017ce Wasi managerowie b\u0119d\u0105 dumni ze zrealizowanych przez Was w tym sprincie punkt\u00f3w. Ja tymczasem \u017cegnam si\u0119 z Wami i do zobaczenia w kolejnej edycji Frontendowego Czwartku.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cze\u015b\u0107 Dzi\u015b widzimy si\u0119 w formacie jakiego na naszym blogu jeszcze nie by\u0142o. Chc\u0119 podzieli\u0107 si\u0119 z Wami naszymi prze\u017cyciami z pola bitwy, jakim by\u0142a implementacja rozbudowanych notyfikacji przy wykorzystaniu Capacitora. UWAGA: Materia\u0142 b\u0119dzie mia\u0142 charakter mocno instrukta\u017cowy, wi\u0119c je\u015bli nie planujecie podobnej funkcjonalno\u015bci w Waszych aplikacjach, to prawdopodobnie nie znajdziecie tu dzi\u015b wiele ciekawego. [&hellip;]<\/p>\n","protected":false},"author":12,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[],"tags":[],"class_list":["post-10153","post","type-post","status-publish","format-standard","hentry"],"acf":{"weekly_summary":false,"estimated_reading_time":"19"},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Extended Push Notifications with Capacitor &amp; Firebase - Vived<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/\" \/>\n<meta property=\"og:locale\" content=\"pl_PL\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Extended Push Notifications with Capacitor &amp; Firebase - Vived\" \/>\n<meta property=\"og:description\" content=\"Cze\u015b\u0107 Dzi\u015b widzimy si\u0119 w formacie jakiego na naszym blogu jeszcze nie by\u0142o. Chc\u0119 podzieli\u0107 si\u0119 z Wami naszymi prze\u017cyciami z pola bitwy, jakim by\u0142a implementacja rozbudowanych notyfikacji przy wykorzystaniu Capacitora. UWAGA: Materia\u0142 b\u0119dzie mia\u0142 charakter mocno instrukta\u017cowy, wi\u0119c je\u015bli nie planujecie podobnej funkcjonalno\u015bci w Waszych aplikacjach, to prawdopodobnie nie znajdziecie tu dzi\u015b wiele ciekawego. [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/\" \/>\n<meta property=\"og:site_name\" content=\"Vived\" \/>\n<meta property=\"article:published_time\" content=\"2021-04-14T14:09:25+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\" \/>\n<meta name=\"author\" content=\"Tomasz Borowicz\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/\"},\"author\":{\"name\":\"Tomasz Borowicz\",\"@id\":\"https:\/\/vived.io\/pl\/#\/schema\/person\/9d2a72fe7d0dfbb4092675afbab742bb\"},\"headline\":\"Extended Push Notifications with Capacitor &#038; Firebase\",\"datePublished\":\"2021-04-14T14:09:25+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/\"},\"wordCount\":2431,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/vived.io\/pl\/#organization\"},\"image\":{\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\",\"inLanguage\":\"pl-PL\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/\",\"url\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/\",\"name\":\"Extended Push Notifications with Capacitor & Firebase - Vived\",\"isPartOf\":{\"@id\":\"https:\/\/vived.io\/pl\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\",\"datePublished\":\"2021-04-14T14:09:25+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#breadcrumb\"},\"inLanguage\":\"pl-PL\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"pl-PL\",\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage\",\"url\":\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\",\"contentUrl\":\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Strona g\u0142\u00f3wna\",\"item\":\"https:\/\/vived.io\/pl\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Extended Push Notifications with Capacitor &#038; Firebase\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/vived.io\/pl\/#website\",\"url\":\"https:\/\/vived.io\/pl\/\",\"name\":\"Vived\",\"description\":\"platform empowering IT people and technology companies to synergic growth\",\"publisher\":{\"@id\":\"https:\/\/vived.io\/pl\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/vived.io\/pl\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"pl-PL\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/vived.io\/pl\/#organization\",\"name\":\"Vived\",\"url\":\"https:\/\/vived.io\/pl\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"pl-PL\",\"@id\":\"https:\/\/vived.io\/pl\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/vived.io\/wp-content\/uploads\/2020\/03\/logo_vived_color.png\",\"contentUrl\":\"https:\/\/vived.io\/wp-content\/uploads\/2020\/03\/logo_vived_color.png\",\"width\":136,\"height\":45,\"caption\":\"Vived\"},\"image\":{\"@id\":\"https:\/\/vived.io\/pl\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/vived.io\/pl\/#\/schema\/person\/9d2a72fe7d0dfbb4092675afbab742bb\",\"name\":\"Tomasz Borowicz\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"pl-PL\",\"@id\":\"https:\/\/vived.io\/pl\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/804536d2672538508d43f60ad2108e5aaea76c192653eaf95d4c3934b7d1dbb6?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/804536d2672538508d43f60ad2108e5aaea76c192653eaf95d4c3934b7d1dbb6?s=96&d=mm&r=g\",\"caption\":\"Tomasz Borowicz\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Extended Push Notifications with Capacitor & Firebase - Vived","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/","og_locale":"pl_PL","og_type":"article","og_title":"Extended Push Notifications with Capacitor & Firebase - Vived","og_description":"Cze\u015b\u0107 Dzi\u015b widzimy si\u0119 w formacie jakiego na naszym blogu jeszcze nie by\u0142o. Chc\u0119 podzieli\u0107 si\u0119 z Wami naszymi prze\u017cyciami z pola bitwy, jakim by\u0142a implementacja rozbudowanych notyfikacji przy wykorzystaniu Capacitora. UWAGA: Materia\u0142 b\u0119dzie mia\u0142 charakter mocno instrukta\u017cowy, wi\u0119c je\u015bli nie planujecie podobnej funkcjonalno\u015bci w Waszych aplikacjach, to prawdopodobnie nie znajdziecie tu dzi\u015b wiele ciekawego. [&hellip;]","og_url":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/","og_site_name":"Vived","article_published_time":"2021-04-14T14:09:25+00:00","og_image":[{"url":"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif","type":"","width":"","height":""}],"author":"Tomasz Borowicz","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#article","isPartOf":{"@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/"},"author":{"name":"Tomasz Borowicz","@id":"https:\/\/vived.io\/pl\/#\/schema\/person\/9d2a72fe7d0dfbb4092675afbab742bb"},"headline":"Extended Push Notifications with Capacitor &#038; Firebase","datePublished":"2021-04-14T14:09:25+00:00","mainEntityOfPage":{"@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/"},"wordCount":2431,"commentCount":0,"publisher":{"@id":"https:\/\/vived.io\/pl\/#organization"},"image":{"@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage"},"thumbnailUrl":"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif","inLanguage":"pl-PL","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/","url":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/","name":"Extended Push Notifications with Capacitor & Firebase - Vived","isPartOf":{"@id":"https:\/\/vived.io\/pl\/#website"},"primaryImageOfPage":{"@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage"},"image":{"@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage"},"thumbnailUrl":"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif","datePublished":"2021-04-14T14:09:25+00:00","breadcrumb":{"@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#breadcrumb"},"inLanguage":"pl-PL","potentialAction":[{"@type":"ReadAction","target":["https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/"]}]},{"@type":"ImageObject","inLanguage":"pl-PL","@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#primaryimage","url":"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif","contentUrl":"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif"},{"@type":"BreadcrumbList","@id":"https:\/\/vived.io\/pl\/capacitor-push-notifications-tutorial\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Strona g\u0142\u00f3wna","item":"https:\/\/vived.io\/pl\/"},{"@type":"ListItem","position":2,"name":"Extended Push Notifications with Capacitor &#038; Firebase"}]},{"@type":"WebSite","@id":"https:\/\/vived.io\/pl\/#website","url":"https:\/\/vived.io\/pl\/","name":"Vived","description":"platform empowering IT people and technology companies to synergic growth","publisher":{"@id":"https:\/\/vived.io\/pl\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/vived.io\/pl\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"pl-PL"},{"@type":"Organization","@id":"https:\/\/vived.io\/pl\/#organization","name":"Vived","url":"https:\/\/vived.io\/pl\/","logo":{"@type":"ImageObject","inLanguage":"pl-PL","@id":"https:\/\/vived.io\/pl\/#\/schema\/logo\/image\/","url":"https:\/\/vived.io\/wp-content\/uploads\/2020\/03\/logo_vived_color.png","contentUrl":"https:\/\/vived.io\/wp-content\/uploads\/2020\/03\/logo_vived_color.png","width":136,"height":45,"caption":"Vived"},"image":{"@id":"https:\/\/vived.io\/pl\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/vived.io\/pl\/#\/schema\/person\/9d2a72fe7d0dfbb4092675afbab742bb","name":"Tomasz Borowicz","image":{"@type":"ImageObject","inLanguage":"pl-PL","@id":"https:\/\/vived.io\/pl\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/804536d2672538508d43f60ad2108e5aaea76c192653eaf95d4c3934b7d1dbb6?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/804536d2672538508d43f60ad2108e5aaea76c192653eaf95d4c3934b7d1dbb6?s=96&d=mm&r=g","caption":"Tomasz Borowicz"}}]}},"blocks_vived":[{"blockName":null,"attrs":[],"innerBlocks":[],"innerHTML":"<p>Cze\u015b\u0107 <\/p><p>Dzi\u015b widzimy si\u0119 w formacie jakiego na naszym blogu jeszcze nie by\u0142o. Chc\u0119 podzieli\u0107 si\u0119 z Wami naszymi prze\u017cyciami z pola bitwy, jakim by\u0142a implementacja rozbudowanych notyfikacji przy wykorzystaniu Capacitora. <\/p><p><em>UWAGA: Materia\u0142 b\u0119dzie mia\u0142 charakter mocno instrukta\u017cowy, wi\u0119c je\u015bli nie planujecie podobnej funkcjonalno\u015bci w Waszych aplikacjach, to prawdopodobnie nie znajdziecie tu dzi\u015b wiele ciekawego. Pozosta\u0142ych zapraszam do lektury <\/em><\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>I niech moc b\u0119dzie z Wami!<\/figcaption><\/figure><h1 id=\"wst-p\">Wst\u0119p<\/h1><p>W grudniu 2020 roku zako\u0144czyli\u015bmy <a href=\"https:\/\/blog.vived.io\/keep-up-czyli-o-tym-jak-byc-na-biezaco-bez-uczucia-przytloczenia-contentem\/\">prac\u0119 nad pi\u0119kniejszym i bardziej funkcjonalnym Keep Upem<\/a>. Nowe funkcjonalno\u015bci pachnia\u0142y jeszcze jak \u015bwie\u017ce bu\u0142eczki, ale zdawali\u015bmy sobie ju\u017c spraw\u0119, \u017ce przed nami jeszcze jedna g\u00f3ra do pokonania. Jak wynika z naszych statystyk, <strong>ruch w Keep Upie pochodzi g\u0142\u00f3wnie z push notyfikacji<\/strong>.<\/p><p>W czym wi\u0119c problem? Z wywiad\u00f3w, kt\u00f3re przeprowadzili\u015bmy z naszymi najwierniejszymi u\u017cytkownikami wynika\u0142o, \u017ce ma\u0142a, generyczna notyfikacja cz\u0119sto gubi\u0142a si\u0119 w t\u0142umie i nie zach\u0119ca\u0142a w szczeg\u00f3lny spos\u00f3b do interakcji. Ci\u0119\u017cko oszacowa\u0107, ilu dok\u0142adnie u\u017cytkownik\u00f3w polubi\u0142o Keep Up i ze wzgl\u0119du na s\u0142abe notyfikacje porzuci\u0142o nawyk czytania artyku\u0142\u00f3w, ale te same statystyki pozwala\u0142y s\u0105dzi\u0107, \u017ce jest ich ca\u0142kiem sporo. Nie mogli\u015bmy tego tak zostawi\u0107 i przeszli\u015bmy do dzia\u0142ania. W efekcie powsta\u0142 nowy design notyfikacji i pomys\u0142 na cotygodniowy raport, kt\u00f3ry czytaj\u0105cych zmotywuje do utrzymania nawyku, a zapominalskim przypomni o istnieniu aplikacji.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3964bde.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Ju\u017c na etapie wst\u0119pnej analizy i dzielenia zadania na mniejsze kawa\u0142ki zorientowali\u015bmy si\u0119, \u017ce sprawa nie b\u0119dzie prosta, a do pokonania mamy zar\u00f3wno problemy po stronie Frontendu jak i Backendu. W komunikacji z Firebase korzystali\u015bmy z starego REST API (kt\u00f3re sami nazwali\u015bmy v0 i tak b\u0119d\u0119 go tytu\u0142owa\u0142 dalej), kt\u00f3re nie umo\u017cliwia\u0142o personalizacji notyfikacji w zale\u017cno\u015bci od urz\u0105dzenia, na kt\u00f3re ma ona trafi\u0107. Sprawa by\u0142a o tyle istotna, \u017ce nazwy niekt\u00f3rych p\u00f3l kolidowa\u0142y ze sob\u0105, a pr\u00f3ba wys\u0142ania notyfikacji na urz\u0105dzenie z Androidem, przy dodaniu p\u00f3l potrzebnych na iOS, zazwyczaj ko\u0144czy\u0142a si\u0119 b\u0142\u0119dem. Migracja do REST API v1 wydawa\u0142a si\u0119 krokiem w odpowiedni\u0105 stron\u0119. Pozwala\u0142a nam w ko\u0144cu wycofa\u0107 si\u0119 z legacy API i dawa\u0142a du\u017co wi\u0119ksz\u0105 elastyczno\u015b\u0107. Jak si\u0119 p\u00f3\u017aniej okaza\u0142o, nowe API mia\u0142o te\u017c swoje wady: mechanizm device group nie by\u0142 jeszcze wspierany i stan ten utrzymywa\u0142 si\u0119 co najmniej od roku. Opr\u00f3cz migracji do nowego API, musieli\u015bmy wi\u0119c zmigrowa\u0107 jeszcze model device group na ci\u0119\u017cszy w utrzymaniu model z tokenami\u2026 No c\u00f3\u017c, mo\u017ce kiedy\u015b Google postanowi doda\u0107 wsparcie dla Device Group do API v1. My tymczasem wr\u00f3\u0107my do clue, czyli frontendowej strony implementacji.<\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3bc1b35.gif\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Moja reakcja kiedy pierwszy raz zobaczy\u0142em dokumentacj\u0119 FCM v0<\/figcaption><\/figure><p>Tutaj r\u00f3wnie\u017c czeka\u0142o na nas kilka pu\u0142apek. Capacitor posiada absolutnie \u015bwietne API do obs\u0142ugi notyfikacji (swego czasu zmuszony by\u0142em do testowania r\u00f3\u017cnych wtyczek do Cordovy, kt\u00f3re pr\u00f3bowa\u0142y osi\u0105gn\u0105\u0107 ten sam cel wi\u0119c wiem, o czym m\u00f3wi\u0119), jest to jednak API do\u015b\u0107 prymitywne i nie spe\u0142niaj\u0105ce naszych potrzeb. Poszukiwania odpowiedniego Pluginu r\u00f3wnie\u017c zako\u0144czy\u0142y si\u0119 fiaskiem, a to oznacza\u0142o, \u017ce przed nami implementacja kawa\u0142ka natywnego kodu - sprawa prawdopodobnie b\u0142aha, je\u015bli dysponujecie zespo\u0142em Android i iOS deweloper\u00f3w, ale jest to spore wyzwanie dla Fullstack Developer\u00f3w z podstawow\u0105 wiedz\u0105 o Mobile Developmencie.<\/p><p>Ko\u0144cz\u0105c ten przyd\u0142ugi wst\u0119p przejd\u017amy do meritum, czyli tutoriala dla wszystkich tych, kt\u00f3rzy podobne notyfikacje chcieliby zaimplementowa\u0107 w swoich aplikacjach. Odpalajcie terminal i zaczynamy \u200d<\/p><h1 id=\"tutorial\">Tutorial<\/h1><h2 id=\"wymagania-pocz-tkowe\">Wymagania pocz\u0105tkowe<\/h2><p>W poni\u017cszym tutorialu skupimy si\u0119 na rozbudowie notyfikacji. Oznacza to, \u017ce pominiemy w nim konfiguracj\u0119 podstawowych notyfikacji i jest to zabieg celowy. \u015awietny tutorial dotycz\u0105cy natywnych aplikacji znajdziecie w <a href=\"https:\/\/capacitorjs.com\/docs\/guides\/push-notifications-firebase\">dokumentacji Capacitora<\/a>. Je\u015bli za\u015b chodzi o cz\u0119\u015b\u0107 webow\u0105, to polecam Wam te artyku\u0142y: <a href=\"https:\/\/medium.com\/mighty-ghost-hack\/angular-8-firebase-cloud-messaging-push-notifications-cc80d9b36f82\">Angular 8 + Firebase Cloud Messaging Push Notifications<\/a> i <a href=\"https:\/\/blog.logrocket.com\/push-notifications-with-react-and-firebase\/\">Push notifications with React and Firebase<\/a><\/p><p>Je\u015bli w Waszej aplikacji macie ju\u017c skonfigurowane podstawowe notyfikacje, to mo\u017cemy rusza\u0107 dalej, a je\u015bli nie, to nie zwlekajcie d\u0142u\u017cej - My tu na Was cierpliwie poczekamy .<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3c42df6.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><h2 id=\"plugin-interface\">Plugin interface<\/h2><p>Zar\u00f3wno implementacj\u0119 Androida jak i iOSa* oprzemy o w\u0142asny plugin. Oba pluginy b\u0119d\u0105 wsp\u00f3\u0142dzieli\u0107 jeden interfejs, wi\u0119c mo\u017cemy zacz\u0105\u0107 od przygotowania jego szkieletu.<\/p><p>*o ten sam interfejs mo\u017cecie oprze\u0107 te\u017c webow\u0105 implementacj\u0119, ale wymaga\u0107 to b\u0119dzie napisania odpowiedniego adaptera przekszta\u0142caj\u0105cego interfejs Firebase na ten zasugerowany poni\u017cej. \u00a0<\/p><pre><code class=\"language-typescript\">import { Plugins } from '@capacitor\/core';\nimport { PluginListenerHandle } from '@capacitor\/core';\nexport type NotificationData = {\n  data: {\n    url: string;\n    vivedId?: string;\n  };\n  action: 'TAP' | 'KEEP_UP_READ';\n};\nexport type ExtendedPushNotificationsPlugin = {\n  addListener(\n    eventName: 'notificationClick',\n    listenerFunc: (info: NotificationData) =&gt; void\n  ): PluginListenerHandle;\n};\nexport const ExtendedPushNotificationsPlugin = Plugins.VivedExtendedPushNotificationsPlugin as ExtendedPushNotificationsPlugin;<\/code><\/pre><h2 id=\"android\">Android<\/h2><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3cbdf27.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Zanim przejdziemy do implementacji, zatrzymajmy si\u0119 na chwil\u0119 przy stronie teoretycznej. Spos\u00f3b, w jaki Android obs\u0142uguje notyfikacje zale\u017cy od tego, czy aplikacja jest aktualnie uruchomiona oraz od formatu danych, jaki zosta\u0142 przekazany do notyfikacji po stronie serwera.<\/p><p>Notyfikacje podzieli\u0107 mo\u017cemy na dwa typy: standardowe push notyfikacje i data notyfikacje. Te pierwsze to notyfikacje, kt\u00f3re chcemy wy\u015bwietli\u0107 u\u017cytkownikowi, natomiast te drugie s\u0142u\u017c\u0105 do przekazania aplikacji danych, niekoniecznie wy\u015bwietlaj\u0105c sam\u0105 notyfikacj\u0105. Jak Android obs\u0142uguje poszczeg\u00f3lne sytuacje, najlepiej opisuje zaczerpni\u0119ta z dokumentacji tabela.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3d34006.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>W standardowym przypadku, je\u015bli aplikacja jest zamkni\u0119ta, to tracimy mo\u017cliwo\u015b\u0107 manipulowania ni\u0105. Niestety oznacza to, \u017ce je\u015bli chcemy pokaza\u0107 u\u017cytkownikom \u0142adne notyfikacje, nawet kiedy aplikacja jest wy\u0142\u0105czona, to zmuszeni b\u0119dziemy wykorzysta\u0107 mechanizm data notifications i troch\u0119 namiesza\u0107.<\/p><p>Android posiada rozbudowany wachlarz dost\u0119pnych typ\u00f3w notyfikacji, wi\u0119c zanim przejdziecie do tworzenia w\u0142asnej spersonalizowanej implementacji sprawd\u017acie, czy jedna z dost\u0119pnych nie spe\u0142nia Waszych wymaga\u0144. Warto zauwa\u017cy\u0107, \u017ce nie mo\u017cecie wymiesza\u0107 ze sob\u0105 r\u00f3\u017cnych typ\u00f3w notyfikacji. Oznacza to, \u017ce je\u015bli zdecydujecie si\u0119 na du\u017cy obrazek, to braknie Wam ju\u017c miejsca na du\u017cy tekst. Na t\u0119 zasad\u0119 nie pomo\u017ce pr\u00f3ba stworzenia swojego interfejsu notyfikacji, bo Android wprowadza odg\u00f3rny limit wysoko\u015bci notyfikacji.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3e66855.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p><br \/><\/p><p>Przejd\u017amy teraz do cz\u0119\u015bci praktycznej. Zgodnie ze sztuk\u0105, aby obs\u0142u\u017cy\u0107 push notyfikacje, musimy stworzy\u0107 implementacj\u0119 interfejsu FirebaseMessagingService i zarejestrowa\u0107 go w AndroidManifest.xml (b\u0119d\u0105c dok\u0142adnym nale\u017ca\u0142oby powiedzie\u0107 przechwycimy intent, ale przejdziemy do tego jeszcze w dalszej cz\u0119\u015bci tutoriala). Uwaga: stworzenie w\u0142asnej implementacji serwisu spowoduje, \u017ce metody do obs\u0142ugi notyfikacji z Capacitora przestan\u0105 dzia\u0142a\u0107, wi\u0119c sami b\u0119dziecie musieli zaimplementowa\u0107 brakuj\u0105ce funkcjonalno\u015bci. Nie przejmujcie si\u0119 natomiast b\u0142\u0119dem dotycz\u0105cym braku obs\u0142ugi rejestracji tokenu. Ta cz\u0119\u015b\u0107 jest \u015bwietnie obs\u0142ugiwana przez Capacitora i mo\u017cecie pozostawi\u0107 j\u0105 nietkni\u0119t\u0105.<br \/><\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b410fe3e.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania Androidowej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure><pre><code class=\"language-kotlin\">\/\/ Note: onNewToken is handled by Capacitor's Push Notifications plugin\nclass PushNotificationsService : FirebaseMessagingService() {\n  override fun onMessageReceived(message: RemoteMessage) {\n    val rawData = RawNotificationData.parse(message)\n    val notificationData = rawData.enrich()\n    val notification = buildNotification(notificationData)\n    sendNotification(rowData.id, notification)\n  }\n}<\/code><\/pre><pre><code class=\"language-XML\">&lt;application&gt;\n    &lt;service\n      android:name=\"com.virtuslab.vived.PushNotifications\"\n      android:stopWithTask=\"false\"&gt;\n      &lt;intent-filter&gt;\n        &lt;action android:name=\"com.google.firebase.MESSAGING_EVENT\" \/&gt;\n      &lt;\/intent-filter&gt;\n    &lt;\/service&gt;\n&lt;\/application&gt;<\/code><\/pre><p>Metoda `onMessageReceived` wype\u0142niona jest jeszcze niezaimplementowanymi metodami, kt\u00f3re pokazuj\u0105 schemat jej dzia\u0142ania. Przyjrzymy si\u0119 teraz dok\u0142adnie ka\u017cdej z nich, zaczynaj\u0105c od (1). Jako \u017ce nasze dane przekazujemy za pomoc\u0105 JSONa, musimy je odpowiednio sparsowa\u0107 do klasy, kt\u00f3ra b\u0119dzie zrozumiana dla naszej aplikacji. Schemat JSONa z g\u00f3ry narzucony jest przez firebase. Wyj\u0105tkiem w tej kwestii jest pole `data`, kt\u00f3re mo\u017cemy dowolnie personalizowa\u0107, wi\u0119c mo\u017cecie pu\u015bci\u0107 wodze fantazji. My starali\u015bmy si\u0119 przygotowa\u0107 API jak najbardziej elastyczne i kompatybilne z przysz\u0142ymi zmianami.<\/p><figure class=\"kg-card kg-code-card\"><pre><code class=\"language-JSON\">method: POST\npath: \"https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send\"\nbody: {\n  \"message\": {\n    \"name\": \"69ab68da-1dae-4c25-9386-cca6346123a8\",\n    \"token\": \"firebase-token\",\n    \"notification\": null,\n    \"data\": {\n      \"url\": \"\/hub\/keep-up\",\n      \"vivedId\": \"vived-id\"\n    },\n    \"android\": {\n      \"data\": {\n        \"title\": \"Keep Up with IT World \",\n        \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\",\n        \"imageUrl\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/image\/rocket.png\",\n        \"iconUrl\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png\",\n        \"type\": \"BigText\",\n        \"actions\": [\n          {\n            \"action\": \"KEEP_UP_READ\",\n            \"title\": \"Read now\"\n          }\n        ]\n      },\n    },\n    \"apns\": { ... },\n    \"webpush\": { ... },\n}<\/code><\/pre><figcaption>Zapytanie wysy\u0142ane po strone backendu do Firebase (okrojone tylko do niezb\u0119dnej cz\u0119\u015bci)<\/figcaption><\/figure><pre><code class=\"language-Kotlin\">enum class NotificationType {\n  Image, BigText, Default\n}\ndata class NotificationAction(\n  val id: String,\n  val title: String\n) {\n  companion object {\n    fun parseJson(json: JSONObject): NotificationAction =\nGson().fromJson(str , Array&lt;NotificationAction&gt;::class.java)\n  }\n}\ndata class RawNotificationData(\n  val id: Int = Random.nextInt(1, 1000000),\n  val title: String?,\n  val body: String?,\n  val actions: List&lt;NotificationAction&gt;,\n  val imageUrl: String?,\n  val iconUrl: String?,\n  val type: NotificationType?,\n  val vivedId: String?,\n  val url: String?\n)\n  companion object {\n    fun parse(message: RemoteMessage): RawNotificationData =\n      RawNotificationData(\n        title = message.data[\"title\"] ?: message.notification?.title,\n        body = message.data[\"body\"] ?: message.notification?.body,\n        actions = message.data[\"actions\"]?.let { NotificationAction.parseJsonArray(JSONArray(it)) } ?: emptyList(),\n        imageUrl = message.data[\"imageUrl\"],\n        iconUrl = message.data[\"iconUrl\"],\n        type = NotificationType.values().find { it.name == message.data[\"type\"] } ?: NotificationType.Default,\n        vivedId = message.data[\"vivedId\"],\n        url = message.data[\"url\"]\n      )\n  }\n}<\/code><\/pre><p>W kroku (2) odczytane dane wzbogacamy o dodatkowe informacje. W naszym przypadku jest to np. obrazek pobrany z sieci. Je\u015bli wystarcz\u0105 Wam suche dane, to spokojnie mo\u017cecie pomin\u0105\u0107 ten krok.<\/p><pre><code class=\"language-Kotlin\">data class NotificationData(\n  val id: Int,\n  val title: String,\n  val body: String,\n  val actions: List&lt;NotificationAction&gt;,\n  val image: Bitmap?,\n  val icon: Bitmap?,\n  val type: NotificationType?,\n  val vivedId: String?,\n  val url: String\n)\ndata class RawNotificationData(\n  ...\n) {\n  private fun downloadBitmap(url: String): Bitmap? {\n    return try {\n      val input = URL(url).openStream()\n      BitmapFactory.decodeStream(input)\n    } catch (e: IOException) {\n      null\n    }\n  }\n  fun enrich(): NotificationData =\n    NotificationData(\n      id = id,\n      title = title ?: \"\",\n      body = body ?: \"\",\n      actions = actions,\n      image = imageUrl?.let { downloadBitmap(it) },\n      icon = iconUrl?.let { downloadBitmap(it) },\n      type = type,\n      vivedId = vivedId,\n      url = url ?: \"\/\"\n    )\n}<\/code><\/pre><p>Na tym etapie mamy ju\u017c wszystkie potrzebne dane i mo\u017cemy przej\u015b\u0107 do zbudowania notyfikacji (3). Ponownie nasza implementacja stara si\u0119 by\u0107 maksymalnie elastyczna, ale je\u015bli chcecie wprowadzi\u0107 swoje modyfikacje, to najlepiej b\u0119dzie je\u015bli zag\u0142\u0119bicie si\u0119 w mo\u017cliwo\u015bci, jakie daje Android w <a href=\"https:\/\/developer.android.com\/training\/notify-user\/build-notification\">dokumentacji.<\/a><\/p><pre><code class=\"language-Kotlin\">  private fun buildPendingIntent(data: NotificationData, actionId: String): PendingIntent {\n    val intent = PushNotificationIntent.from(data, actionId)\n    return PendingIntent.getActivity(this, Random.nextInt(), intent.toIntent(this), PendingIntent.FLAG_CANCEL_CURRENT)\n  }\n  private fun buildNotification(data: NotificationData): Notification {\n    val notificationBuilder = NotificationCompat.Builder(this, PushNotificationsPlugin.DEFAULT_CHANNEL_ID)\n      .setContentTitle(data.title)\n      .setContentText(data.body)\n      .setAutoCancel(true)\n      .setSmallIcon(R.drawable.ic_stat_notification)\n      .setContentIntent(buildPendingIntent(data, \"TAP\"))\n    data.actions.forEach { action -&gt;\n      notificationBuilder.addAction(\n        R.drawable.ic_stat_notification,\n        action.title,\n        buildPendingIntent(data, action.id)\n      )\n    }\n    return notificationBuilder.build()\n  }<\/code><\/pre><p>W powy\u017cszym kodzie znajduj\u0105 si\u0119 dwie zupe\u0142nie nowe rzeczy, nad kt\u00f3rymi warto si\u0119 pochyli\u0107. Zacznijmy od tajemniczych `PendingIntet.` Kiedy u\u017cytkownik kliknie w notyfikacj\u0119 lub jedn\u0105 z akcji, to do naszej aplikacji zostanie wys\u0142any obiekt PendingIntent, kt\u00f3ry nale\u017cy przechwyci\u0107 i obs\u0142u\u017cy\u0107. Odpowiedzialny b\u0119dzie za to `PushNotificationsPlugin`. Jako \u017ce logik\u0119 chcemy przesun\u0105\u0107 w stron\u0119 JavaScriptu, to jedynym zadaniem Pluginu b\u0119dzie zamkni\u0119cie notyfikacji i przekazanie eventu do WebView.<\/p><pre><code class=\"language-Kotlin\">data class PushNotificationIntent(\n  val notificationId: Int,\n  val vivedNotificationId: String?,\n  val actionId: String,\n  val url: String\n) {\n  companion object {\n    const val IS_FROM_PLUGIN_EXTRA = \"isFromPushNotificationPlugin\"\n    const val NOTIFICATION_ID_EXTRA = \"notificationId\"\n    const val VIVED_NOTIFICATION_ID_EXTRA = \"vivedNotificationId\"\n    const val ACTION_ID_EXTRA = \"actionId\"\n    const val URL_EXTRA = \"url\"\n    fun from(intent: Intent): PushNotificationIntent =\n      PushNotificationIntent(\n        notificationId = intent.extras?.getInt(NOTIFICATION_ID_EXTRA)!!,\n        vivedNotificationId = intent.extras?.getString(VIVED_NOTIFICATION_ID_EXTRA),\n        actionId = intent.extras?.getString(ACTION_ID_EXTRA)!!,\n        url = intent.extras?.getString(URL_EXTRA)!!\n      )\n    fun from(data: NotificationData, actionId: String): PushNotificationIntent =\n      PushNotificationIntent(\n        notificationId = data.id,\n        vivedNotificationId = data.vivedId,\n        actionId = actionId,\n        url = data.url\n      )\n  }\n  fun toIntent(context: Context): Intent =\n    Intent(context, MainActivity::class.java)\n      .putExtra(IS_FROM_PLUGIN_EXTRA, true)\n      .putExtra(ACTION_ID_EXTRA, actionId)\n      .putExtra(NOTIFICATION_ID_EXTRA, notificationId)\n      .putExtra(VIVED_NOTIFICATION_ID_EXTRA, vivedNotificationId)\n      .putExtra(URL_EXTRA, url)\n}\n@NativePlugin(name = \"VivedPushNotificationsPlugin\")\nclass PushNotificationsPlugin : Plugin() {\n  private fun shouldHandleIntent(intent: Intent?): Boolean =\n    intent?.extras?.getBoolean(PushNotificationIntent.IS_FROM_PLUGIN_EXTRA) == true\n  private fun notifyJS(intent: PushNotificationIntent) {\n    val dataObject = JSObject()\n    dataObject.put(\"url\", intent.url)\n    dataObject.put(\"vivedId\", intent.vivedNotificationId)\n    val returnObject = JSObject()\n    returnObject.put(\"data\", dataObject)\n    returnObject.put(\"action\", intent.actionId)\n    notifyListeners(\"notificationClick\", returnObject, true)\n  }\n  private fun cancelNotification(id: Int) {\n    val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n    notificationManager.cancel(id)\n  }\n  override fun handleOnNewIntent(intent: Intent?) {\n    super.handleOnNewIntent(intent)\n    if (shouldHandleIntent(intent)) {\n      val pushNotificationIntent = PushNotificationIntent.from(intent!!)\n      notifyJS(pushNotificationIntent)\n      cancelNotification(pushNotificationIntent.notificationId)\n    }\n  }<\/code><\/pre><p>Przejd\u017amy teraz do drugiej tajemniczej funkcjonalno\u015bci, czyli `PushNotificationsPlugin.DEFAULT_CHANNEL_ID`. Android od wersji 8.0 (API level 26) wprowadzi\u0142 kana\u0142y notyfikacji, kt\u00f3rymi mo\u017cna sterowa\u0107 z poziomu ustawie\u0144 aplikacji. <br \/><\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b41936bc.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Tworzenie notyfikacji bez podania kana\u0142u, jest co prawda mo\u017cliwe, ale jest ju\u017c oznaczone jako deprecated. Aby mie\u0107 pewno\u015b\u0107, \u017ce kana\u0142 notyfikacji zostanie stworzony, funkcjonalno\u015b\u0107 ta zostanie dodana do odpowiedniego hook\u2019a w stworzonym wcze\u015bniej Pluginie<\/p><pre><code class=\"language-Kotlin\">@NativePlugin(name = \"VivedPushNotificationsPlugin\")\nclass PushNotificationsPlugin : Plugin() {\n  companion object {\n    const val DEFAULT_CHANNEL_ID = \"vived_default_notifications_channel\"\n  }\n  private fun createNotificationChannel() {\n    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {\n      val importance = NotificationManager.IMPORTANCE_HIGH\n      val notificationChannel = NotificationChannel(DEFAULT_CHANNEL_ID, \"Default\", importance)\n      val notificationManager = this.activity.getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) as NotificationManager\n      notificationManager.createNotificationChannel(notificationChannel)\n    }\n  }\n  override fun load() {\n    createNotificationChannel()\n  }\n  ....\n}<\/code><\/pre><p>Tak jak wspomnia\u0142em na pocz\u0105tku, wersje notyfikacji dostarczone przez Androida w zupe\u0142no\u015bci nam wystarczy\u0142y, ale dla wszystkich, kt\u00f3rzy oczekuj\u0105 wi\u0119cej, mam ma\u0142e preview takiej funkcjonalno\u015bci.<\/p><p>Je\u015bli p\u00f3jdziecie w ca\u0142kowicie personalizowan\u0105 notyfikacj\u0119, to pami\u0119tajcie o <a href=\"https:\/\/developer.android.com\/training\/notify-user\/custom-notification#:~:text=The%20height%20available%20for%20a,are%20limited%20to%20256%20dp.\">ograniczeniu wysoko\u015bci widoku<\/a> i mo\u017cliwo\u015bci skorzystania tylko z <a href=\"https:\/\/developer.android.com\/reference\/android\/widget\/RemoteViews\">wybranych element\u00f3w UI<\/a>, a tak\u017ce o odg\u00f3rnym ograniczeniu wysoko\u015bci notyfikacji.<\/p><p>W celach edukacyjnych nasza notyfikacja b\u0119dzie wy\u015bwietla\u0107 tylko prosty tekst. Prawdopodobnie od swojej notyfikacji oczekiwa\u0107 b\u0119dziecie jednak troch\u0119 wi\u0119cej, ale w tym celu b\u0119dziecie musieli zanurzy\u0107 si\u0119 w niuansach tworzenia androidowych widok\u00f3w.<\/p><pre><code class=\"language-XML\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\n&lt;LinearLayout xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\n  android:orientation=\"vertical\" android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"&gt;\n  &lt;TextView\n    android:id=\"@+id\/text_view_id\"\n    android:layout_height=\"wrap_content\"\n    android:layout_width=\"wrap_content\"\n    android:text=\"Hello there Obi-Wan Kenobi\" \/&gt;\n&lt;\/LinearLayout&gt;<\/code><\/pre><pre><code class=\"language-Kotlin\">\/\/ Get the layouts to use in the custom notification\nval notificationLayoutExpanded = RemoteViews(packageName, R.layout.notification_large)\n\/\/ Apply the layouts to the notification\nval customNotification = NotificationCompat.Builder(context, CHANNEL_ID)\n        .setSmallIcon(R.drawable.notification_icon)\n        .setStyle(NotificationCompat.DecoratedCustomViewStyle())\n        .setCustomBigContentView(notificationLayoutExpanded)\n        .build()<\/code><\/pre><p>Ufff\u2026 Uda\u0142o nam si\u0119 zaimplementowa\u0107 cz\u0119\u015b\u0107 androidow\u0105, wi\u0119c mo\u017cemy rusza\u0107 dalej.<\/p><h2 id=\"web-pwa\">Web\/PWA<\/h2><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b42a0a9a.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Je\u015bli chodzi o push notyfikacje na webie, to niestety wci\u0105\u017c mo\u017cemy m\u00f3wi\u0107 tutaj tylko o Webowym Androidzie. Pomimo niezliczonych pr\u00f3\u015bb i wniosk\u00f3w nic nie wskazuje na to, \u017ce Apple zacznie patrze\u0107 na t\u0105 funkcjonalno\u015b\u0107 chocia\u017c odrobin\u0119 przychylniej.<\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4311c88.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania Webowej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure><p>Je\u015bli m\u00f3wimy natomiast o Webowym Androidzie, to niesamowicie wa\u017cne jest u\u015bwiadomienie sobie, \u017ce obowi\u0105zuj\u0105 tutaj te same zasady, co przy natywnych androidowych notyfikacjach. Mamy wi\u0119c te same mo\u017cliwo\u015bci konfiguracji, ale opakowane w zdecydowanie elastyczniejsze API. Wszystkie parametry mo\u017cemy przekazywa\u0107 w zapytaniu po stronie backendu. A\u017c dziwne, \u017ce Android nie wspiera podobnego API.<\/p><p>Niestety web push notifications nie wspieraj\u0105 aktualnie \u017cadnych dodatkowych mo\u017cliwo\u015bci personalizacji.<\/p><pre><code class=\"language-JSON\">method: POST\npath: \"https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send\"\nbody: {\n  \"message\": {\n    \"name\": \"69ab68da-1dae-4c25-9386-cca6346123a8\",\n    \"token\": \"firebase-token\",\n    \"notification\": null,\n    \"data\": {\n      \"url\": \"\/hub\/keep-up\",\n      \"vivedId\": \"vived-id\"\n    },\n    \"android\": { ... },\n    \"apns\": { ... },\n    \"webpush\": {\n      \"notification\": {\n        \"title\": \"Keep Up with IT World \",\n        \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\",\n        \"actions\": [\n          {\n            \"action\": \"KEEP_UP_READ\",\n            \"title\": \"Read now\"\n          }\n        ],\n        \"badge\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/badge\/vived-badge.png\",\n        \"icon\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png\",\n        \"image\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png\"\n      }\n    },\n}<\/code><\/pre><p>Zapytanie wysy\u0142ane po strone backendu do Firebase (okrojone tylko do niezb\u0119dnej cz\u0119\u015bci)<\/p><p>Warto zwr\u00f3ci\u0107 uwag\u0119, \u017ce typem notyfikacji sterujemy, poprzez to kt\u00f3re pola pozostawimy wype\u0142nione.<\/p><pre><code class=\"language-TypeScript\">declare const self: ServiceWorkerGlobalScope;\nconst openUrl = async (url: string): Promise&lt;void&gt; =&gt; {\n  const windowClients = (await self.clients.matchAll({\n    type: 'window',\n  })) as WindowClient[];\n  const activeClient = windowClients.find(\n    it =&gt; it.visibilityState === 'visible'\n  );\n  if (activeClient) {\n    await activeClient.navigate(url);\n  } else if (self.clients.openWindow) {\n    await self.clients.openWindow(url);\n  }\n};\nexport const initializePushNotifications = (): void =&gt; {\n  self.addEventListener('notificationclick', event =&gt; {\n    const notification: MessagePayload = event.notification.data.FCM_MSG;\n    const data = notification.data as PushNotificationData;\n    event.notification.close(); \/\/ Android needs explicit close.\n    event.waitUntil(openUrl(data.url));\n  });\n};<\/code><\/pre><h2 id=\"ios\">iOS<\/h2><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b435e2fa.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Dobrn\u0119li\u015bmy do ostatniej wspieranej przez nas platformy i jednocze\u015bnie tej, kt\u00f3ra sprawi\u0142a nam najwi\u0119cej problem\u00f3w. Na szcz\u0119\u015bcie nie ze wzgl\u0119du na s\u0142abe API, a na nasze skromne umiej\u0119tno\u015bci :).<\/p><p>Podstawowe notyfikacje na iOS wspieraj\u0105 tylko nag\u0142\u00f3wek i tekst. Dodaj\u0105c pewn\u0105 modyfikacj\u0119 po stronie natywnej aplikacji, funkcjonalno\u015b\u0107 t\u0105 mo\u017cemy rozszerzy\u0107 o obrazek.<\/p><p>Je\u015bli to dla Was za ma\u0142o, to mamy te\u017c mo\u017cliwo\u015b\u0107 dowolnej personalizacji rozwini\u0119tej notyfikacji przez dostarczenie natywnego widoku. I w tym przypadku m\u00f3wimy o naprawd\u0119 dowolnej personalizacji, bo nie mamy tutaj ogranicze\u0144 podobnych do tych z Androida.<\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b43962fa.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania iOS-owej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure><pre><code class=\"language-JSON\">method: POST\npath: \"https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send\"\nbody: {\n  \"message\": {\n    \"name\": \"69ab68da-1dae-4c25-9386-cca6346123a8\",\n    \"token\": \"firebase-token\",\n    \"notification\": null,\n    \"data\": {\n      \"url\": \"\/hub\/keep-up\",\n      \"vivedId\": \"vived-id\"\n    },\n    \"android\": { ... },\n    \"apns\": {\n      \"payload\": {\n        \"aps\": {\n          \"alert\": {\n            \"title\": \"Keep Up with IT World \",\n            \"subtitle\": null,\n            \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\"\n          },\n          \"category\": \"KEEP_UP\",\n          \"mutableContent\": 1\n        },\n        \"imageUrl\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/image\/rocket.png\",\n        \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\"\n      }\n    },\n    \"webpush\": { ... },\n}<\/code><\/pre><p>Zacznijmy od nadpisania obs\u0142ugi notyfikacji dostarczonej przez Capacitora. W tym celu musimy stworzy\u0107 w\u0142asny plugin i zarejestrowa\u0107 go jako instancj\u0119 obs\u0142uguj\u0105c\u0105 notyfikacje.<\/p><pre><code class=\"language-Swift\">@objc(ExtendedPushNotificationsPlugin)\npublic class ExtendedPushNotificationsPlugin: CAPPlugin, UNUserNotificationCenterDelegate {\n    private func notifyJS(response: UNNotificationResponse) {\n        var notificationAction: String;\n        switch(response.actionIdentifier) {\n        case \"KEEP_UP_READ\":\n            notificationAction = \"KEEP_UP_READ\"\n        default:\n            notificationAction = \"TAP\"\n        }\n        let notificationData = [\n            \"url\": response.notification.request.content.userInfo[\"url\"],\n            \"vivedId\": response.notification.request.content.userInfo[\"vivedId\"]\n        ];\n        self.notifyListeners(\"notificationClick\", data: [\n            \"data\": notificationData,\n            \"action\": notificationAction\n        ], retainUntilConsumed: true);\n    }\n    private func registerCustomNotificationCategories() {\n        let readNowAction = UNNotificationAction(\n            identifier: \"KEEP_UP_READ\",\n            title: \"Read Now\",\n            options: [UNNotificationActionOptions.foreground]\n        )\n        \/\/ Define the notification type\n        let dailyKeepUpNotificationCategory = UNNotificationCategory(\n            identifier: \"KEEP_UP\",\n            actions: [readNowAction],\n            intentIdentifiers: [],\n            hiddenPreviewsBodyPlaceholder: \"\",\n            options: []\n        )\n        \/\/ Register the notification type.\n        let notificationCenter = UNUserNotificationCenter.current()\n        notificationCenter.setNotificationCategories([dailyKeepUpNotificationCategory])\n    }\n    @objc override public func load() {\n        UNUserNotificationCenter.current().delegate = self\n        registerCustomNotificationCategories()\n    }\n    public func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                       didReceive response: UNNotificationResponse,\n                                       withCompletionHandler completionHandler:\n                                        @escaping () -&gt; Void) {\n        completionHandler()\n        notifyJS(response: response)\n    }\n    @objc public func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                             willPresent notification: UNNotification,\n                                             withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -&gt; Void) {\n        completionHandler([\n            .badge,\n            .sound,\n            .alert\n        ])\n    }<\/code><\/pre><p>Rzecz\u0105, kt\u00f3ra mo\u017ce wzbudza\u0107 Wasze w\u0105tpliwo\u015bci, jest r\u0119czne tworzenie akcji. W odr\u00f3\u017cnienia od Androida i Weba tutaj nie mamy mo\u017cliwo\u015bci spersonalizowania akcji po stronie serwera i nale\u017cy dokona\u0107 tego po stronie klienta. Jest to rozwi\u0105zanie, kt\u00f3re niestety blokuje nam mo\u017cliwo\u015b\u0107 dodania kolejnych przycisk\u00f3w po wypuszczeniu aplikacji, ale z drugiej strony pozwala nam ca\u0142kowicie unikn\u0105\u0107 niedzia\u0142aj\u0105cych przycisk\u00f3w (ich brak uwa\u017cam za lepsze zachowanie).<\/p><p>Nasz plugin obs\u0142uguje ju\u017c notyfikacje, wi\u0119c mo\u017cemy przej\u015b\u0107 do wzbogacenia ich o obrazek. W tym celu musimy stworzy\u0107 NotificationServiceExtension, kt\u00f3ry b\u0119dzie modyfikowa\u0142 notyfikacj\u0119 zanim ta zostanie wy\u015bwietlona.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b43e21e7.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b45cc6c6.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b46d47a7.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Podobnie jak w przypadku Androida, tak i tutaj b\u0119dziemy chcieli wzbogaci\u0107 dane otrzymane od backendu. Tak samo jak poprzednio, b\u0119dzie to pobranie zdj\u0119cia, ale nic nie stoi na przeszkodzie, aby by\u0142o to np. dodanie tekstu do notyfikacji. Za modyfikacj\u0119 odpowiedzialna b\u0119dzie metoda `didReceive` z `NotificationService`.<\/p><p>Note: Przekazanie notyfikacji do UNNotificationServiceExtension zale\u017cy od tego, czy po stronie serwera zdefiniujemy parametr `mutableContent`: 1.<\/p><pre><code class=\"language-Swift\">import UserNotifications\nclass NotificationService: UNNotificationServiceExtension {\n    var contentHandler: ((UNNotificationContent) -&gt; Void)?\n    var bestAttemptContent: UNMutableNotificationContent?\n    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -&gt; Void) {\n        self.contentHandler = contentHandler\n        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)\n        guard let bestAttemptContent = bestAttemptContent,\n              let attachmentURLAsString = bestAttemptContent.userInfo[\"imageUrl\"] as? String,\n              let attachmentURL = URL(string: attachmentURLAsString) else {\n            return\n        }\n        addImageToAttachments(url: attachmentURL) { (attachment) in\n            if let attachment = attachment {\n                bestAttemptContent.attachments = [attachment]\n                contentHandler(bestAttemptContent)\n            }\n        }\n    }\n}<\/code><\/pre><p>W pierwszym kroku odpakowujemy otrzymane dane, korzystaj\u0105c z mechanizmu <a href=\"https:\/\/docs.swift.org\/swift-book\/ReferenceManual\/Statements.html#grammar_if-statement\">guard statement.<\/a> \u00a0Nast\u0119pnie pobieramy asynchronicznie obrazek i dodajemy go do za\u0142\u0105cznik\u00f3w naszej notyfikacji. Na koniec korzystaj\u0105c z mechanizmu if-let, odpakowujemy wynik operacji i przekazujemy nasz\u0105 notyfikacj\u0119 do kolejnego handlera.<\/p><pre><code class=\"language-Swift\">    private func addImageToAttachments(url: URL, with completitionHandler: @escaping (UNNotificationAttachment?) -&gt; Void) {\n        let task = URLSession.shared.downloadTask(with: url) { (downloadedUrl, response, error) in\n            guard let downloadedUrl = downloadedUrl else {\n                completitionHandler(nil)\n                return\n            }\n            let uniqueURLEnding = ProcessInfo.processInfo.globallyUniqueString + \".jpg\"\n            let urlPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(uniqueURLEnding)\n            try? FileManager.default.moveItem(at: downloadedUrl, to: urlPath)\n            do {\n                let attachment = try UNNotificationAttachment(identifier: \"picture\", url: urlPath, options: nil)\n                completitionHandler(attachment)\n            }\n            catch {\n                completitionHandler(nil)\n            }\n        }\n        task.resume()\n    }<\/code><\/pre><p>Pobieranie zaczynamy od wywo\u0142ania w\u0142a\u015bciwego zasobu z sieci. Nast\u0119pnie odczytujemy adres, do jakiego pobrany zosta\u0142 obraz i kopiujemy go do tymczasowego pliku. Na koniec wywo\u0142ujemy completitionHandler.<\/p><p>Tip: Je\u015bli nie mo\u017cecie za\u0142apa\u0107 koncepcji `completitionHandler`, to potraktujcie go jako odpowiednik Promise.resolve().<\/p><p>Je\u015bli na tym etapie wy\u015blecie notyfikacj\u0119, to powinna ona zawiera\u0107 pobrany przez nas obrazek. Je\u015bli natomiast na tym nie ko\u0144cz\u0105 si\u0119 Wasze wymagania, to iOS umo\u017cliwia nam przygotowanie dowolnego widoku dla rozwini\u0119tej notyfikacji. W tym celu musimy stworzy\u0107 kolejny Application Target: Notification Content Extension.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b481e708.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b49cc743.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4b374a6.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Zanim zaczniemy implementacj\u0119 widoku, nale\u017cy doda\u0107 do pliku info.plist UNNotificationExtensionCategory zgodne z tym, kt\u00f3re definiujemy po stronie backendu.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4c72574.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Tworzenie natywnych widok\u00f3w przypomina troch\u0119 czarn\u0105 magi\u0119, polegaj\u0105c\u0105 na \u0142\u0105czeniu mi\u0119dzy sob\u0105 r\u00f3\u017cnych element\u00f3w i definiowaniu margines\u00f3w. Jest to proces na tyle skomplikowany, \u017ce gdybym chcia\u0142 dobrze go opisa\u0107, to musia\u0142bym kilkukrotnie rozszerzy\u0107 ten tutorial. Z tego wzgl\u0119du wszystkich zainteresowanych odsy\u0142am do innego tutoriala, kt\u00f3ry zrobi\u0142 to dobrze. W tym miejscu podziel\u0119 si\u0119 natomiast samym kodem definiuj\u0105cym logik\u0119 (tej na szcz\u0119\u015bcie nie ma du\u017co, bo ustawiamy tylko warto\u015bci poszczeg\u00f3lnym elementom interfejsu).<\/p><pre><code class=\"language-Swift\">import UIKit\nimport UserNotifications\nimport UserNotificationsUI\nclass NotificationViewController: UIViewController, UNNotificationContentExtension {\n    @IBOutlet weak var titleLabel: UILabel!\n    @IBOutlet weak var bodyLabel: UILabel!\n    @IBOutlet weak var imageView: UIImageView!\n    override func viewDidLoad() {\n        super.viewDidLoad()\n    }\n    func didReceive(_ notification: UNNotification) {\n        self.titleLabel.text = notification.request.content.title\n        self.bodyLabel.text = notification.request.content.userInfo[\"longBody\"] as? String ?? notification.request.content.body\n        let attachments = notification.request.content.attachments\n        for attachment in attachments {\n            if attachment.identifier == \"picture\" &amp;&amp; attachment.url.startAccessingSecurityScopedResource() {\n                guard let data = try? Data(contentsOf: attachment.url) else {\n                    return\n                }\n                imageView.image = UIImage(data: data)\n            }\n        }\n    }\n}<\/code><\/pre><h1 id=\"podsumowanie\">Podsumowanie<\/h1><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4e8af35.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Mam nadziej\u0119, \u017ce wszyscy wygl\u0105dacie teraz tak samo jak Anakin na gifie powy\u017cej! Je\u015bli nie to niech moc b\u0119dzie z Wami\u2026 Notyfikacje to jeden z tych temat\u00f3w, kt\u00f3re na papierze wydaj\u0105 si\u0119 banalnie proste, a przy pr\u00f3bie implementacji okazuj\u0105 si\u0119 r\u00f3wnie zawi\u0142e, co dzieje rodu Skywalker\u00f3w w uniwersum Gwiezdnych Wojen. Mam nadziej\u0119, \u017ce praca w\u0142o\u017cona w ten tutorial, chocia\u017c odrobin\u0119 u\u0142atwi Wam prac\u0119 i sprawi, \u017ce Wasi managerowie b\u0119d\u0105 dumni ze zrealizowanych przez Was w tym sprincie punkt\u00f3w. Ja tymczasem \u017cegnam si\u0119 z Wami i do zobaczenia w kolejnej edycji Frontendowego Czwartku.<\/p>","innerContent":["<p>Cze\u015b\u0107 <\/p><p>Dzi\u015b widzimy si\u0119 w formacie jakiego na naszym blogu jeszcze nie by\u0142o. Chc\u0119 podzieli\u0107 si\u0119 z Wami naszymi prze\u017cyciami z pola bitwy, jakim by\u0142a implementacja rozbudowanych notyfikacji przy wykorzystaniu Capacitora. <\/p><p><em>UWAGA: Materia\u0142 b\u0119dzie mia\u0142 charakter mocno instrukta\u017cowy, wi\u0119c je\u015bli nie planujecie podobnej funkcjonalno\u015bci w Waszych aplikacjach, to prawdopodobnie nie znajdziecie tu dzi\u015b wiele ciekawego. Pozosta\u0142ych zapraszam do lektury <\/em><\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3901934.gif\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>I niech moc b\u0119dzie z Wami!<\/figcaption><\/figure><h1 id=\"wst-p\">Wst\u0119p<\/h1><p>W grudniu 2020 roku zako\u0144czyli\u015bmy <a href=\"https:\/\/blog.vived.io\/keep-up-czyli-o-tym-jak-byc-na-biezaco-bez-uczucia-przytloczenia-contentem\/\">prac\u0119 nad pi\u0119kniejszym i bardziej funkcjonalnym Keep Upem<\/a>. Nowe funkcjonalno\u015bci pachnia\u0142y jeszcze jak \u015bwie\u017ce bu\u0142eczki, ale zdawali\u015bmy sobie ju\u017c spraw\u0119, \u017ce przed nami jeszcze jedna g\u00f3ra do pokonania. Jak wynika z naszych statystyk, <strong>ruch w Keep Upie pochodzi g\u0142\u00f3wnie z push notyfikacji<\/strong>.<\/p><p>W czym wi\u0119c problem? Z wywiad\u00f3w, kt\u00f3re przeprowadzili\u015bmy z naszymi najwierniejszymi u\u017cytkownikami wynika\u0142o, \u017ce ma\u0142a, generyczna notyfikacja cz\u0119sto gubi\u0142a si\u0119 w t\u0142umie i nie zach\u0119ca\u0142a w szczeg\u00f3lny spos\u00f3b do interakcji. Ci\u0119\u017cko oszacowa\u0107, ilu dok\u0142adnie u\u017cytkownik\u00f3w polubi\u0142o Keep Up i ze wzgl\u0119du na s\u0142abe notyfikacje porzuci\u0142o nawyk czytania artyku\u0142\u00f3w, ale te same statystyki pozwala\u0142y s\u0105dzi\u0107, \u017ce jest ich ca\u0142kiem sporo. Nie mogli\u015bmy tego tak zostawi\u0107 i przeszli\u015bmy do dzia\u0142ania. W efekcie powsta\u0142 nowy design notyfikacji i pomys\u0142 na cotygodniowy raport, kt\u00f3ry czytaj\u0105cych zmotywuje do utrzymania nawyku, a zapominalskim przypomni o istnieniu aplikacji.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3964bde.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Ju\u017c na etapie wst\u0119pnej analizy i dzielenia zadania na mniejsze kawa\u0142ki zorientowali\u015bmy si\u0119, \u017ce sprawa nie b\u0119dzie prosta, a do pokonania mamy zar\u00f3wno problemy po stronie Frontendu jak i Backendu. W komunikacji z Firebase korzystali\u015bmy z starego REST API (kt\u00f3re sami nazwali\u015bmy v0 i tak b\u0119d\u0119 go tytu\u0142owa\u0142 dalej), kt\u00f3re nie umo\u017cliwia\u0142o personalizacji notyfikacji w zale\u017cno\u015bci od urz\u0105dzenia, na kt\u00f3re ma ona trafi\u0107. Sprawa by\u0142a o tyle istotna, \u017ce nazwy niekt\u00f3rych p\u00f3l kolidowa\u0142y ze sob\u0105, a pr\u00f3ba wys\u0142ania notyfikacji na urz\u0105dzenie z Androidem, przy dodaniu p\u00f3l potrzebnych na iOS, zazwyczaj ko\u0144czy\u0142a si\u0119 b\u0142\u0119dem. Migracja do REST API v1 wydawa\u0142a si\u0119 krokiem w odpowiedni\u0105 stron\u0119. Pozwala\u0142a nam w ko\u0144cu wycofa\u0107 si\u0119 z legacy API i dawa\u0142a du\u017co wi\u0119ksz\u0105 elastyczno\u015b\u0107. Jak si\u0119 p\u00f3\u017aniej okaza\u0142o, nowe API mia\u0142o te\u017c swoje wady: mechanizm device group nie by\u0142 jeszcze wspierany i stan ten utrzymywa\u0142 si\u0119 co najmniej od roku. Opr\u00f3cz migracji do nowego API, musieli\u015bmy wi\u0119c zmigrowa\u0107 jeszcze model device group na ci\u0119\u017cszy w utrzymaniu model z tokenami\u2026 No c\u00f3\u017c, mo\u017ce kiedy\u015b Google postanowi doda\u0107 wsparcie dla Device Group do API v1. My tymczasem wr\u00f3\u0107my do clue, czyli frontendowej strony implementacji.<\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3bc1b35.gif\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Moja reakcja kiedy pierwszy raz zobaczy\u0142em dokumentacj\u0119 FCM v0<\/figcaption><\/figure><p>Tutaj r\u00f3wnie\u017c czeka\u0142o na nas kilka pu\u0142apek. Capacitor posiada absolutnie \u015bwietne API do obs\u0142ugi notyfikacji (swego czasu zmuszony by\u0142em do testowania r\u00f3\u017cnych wtyczek do Cordovy, kt\u00f3re pr\u00f3bowa\u0142y osi\u0105gn\u0105\u0107 ten sam cel wi\u0119c wiem, o czym m\u00f3wi\u0119), jest to jednak API do\u015b\u0107 prymitywne i nie spe\u0142niaj\u0105ce naszych potrzeb. Poszukiwania odpowiedniego Pluginu r\u00f3wnie\u017c zako\u0144czy\u0142y si\u0119 fiaskiem, a to oznacza\u0142o, \u017ce przed nami implementacja kawa\u0142ka natywnego kodu - sprawa prawdopodobnie b\u0142aha, je\u015bli dysponujecie zespo\u0142em Android i iOS deweloper\u00f3w, ale jest to spore wyzwanie dla Fullstack Developer\u00f3w z podstawow\u0105 wiedz\u0105 o Mobile Developmencie.<\/p><p>Ko\u0144cz\u0105c ten przyd\u0142ugi wst\u0119p przejd\u017amy do meritum, czyli tutoriala dla wszystkich tych, kt\u00f3rzy podobne notyfikacje chcieliby zaimplementowa\u0107 w swoich aplikacjach. Odpalajcie terminal i zaczynamy \u200d<\/p><h1 id=\"tutorial\">Tutorial<\/h1><h2 id=\"wymagania-pocz-tkowe\">Wymagania pocz\u0105tkowe<\/h2><p>W poni\u017cszym tutorialu skupimy si\u0119 na rozbudowie notyfikacji. Oznacza to, \u017ce pominiemy w nim konfiguracj\u0119 podstawowych notyfikacji i jest to zabieg celowy. \u015awietny tutorial dotycz\u0105cy natywnych aplikacji znajdziecie w <a href=\"https:\/\/capacitorjs.com\/docs\/guides\/push-notifications-firebase\">dokumentacji Capacitora<\/a>. Je\u015bli za\u015b chodzi o cz\u0119\u015b\u0107 webow\u0105, to polecam Wam te artyku\u0142y: <a href=\"https:\/\/medium.com\/mighty-ghost-hack\/angular-8-firebase-cloud-messaging-push-notifications-cc80d9b36f82\">Angular 8 + Firebase Cloud Messaging Push Notifications<\/a> i <a href=\"https:\/\/blog.logrocket.com\/push-notifications-with-react-and-firebase\/\">Push notifications with React and Firebase<\/a><\/p><p>Je\u015bli w Waszej aplikacji macie ju\u017c skonfigurowane podstawowe notyfikacje, to mo\u017cemy rusza\u0107 dalej, a je\u015bli nie, to nie zwlekajcie d\u0142u\u017cej - My tu na Was cierpliwie poczekamy .<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3c42df6.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><h2 id=\"plugin-interface\">Plugin interface<\/h2><p>Zar\u00f3wno implementacj\u0119 Androida jak i iOSa* oprzemy o w\u0142asny plugin. Oba pluginy b\u0119d\u0105 wsp\u00f3\u0142dzieli\u0107 jeden interfejs, wi\u0119c mo\u017cemy zacz\u0105\u0107 od przygotowania jego szkieletu.<\/p><p>*o ten sam interfejs mo\u017cecie oprze\u0107 te\u017c webow\u0105 implementacj\u0119, ale wymaga\u0107 to b\u0119dzie napisania odpowiedniego adaptera przekszta\u0142caj\u0105cego interfejs Firebase na ten zasugerowany poni\u017cej. \u00a0<\/p><pre><code class=\"language-typescript\">import { Plugins } from '@capacitor\/core';\nimport { PluginListenerHandle } from '@capacitor\/core';\nexport type NotificationData = {\n  data: {\n    url: string;\n    vivedId?: string;\n  };\n  action: 'TAP' | 'KEEP_UP_READ';\n};\nexport type ExtendedPushNotificationsPlugin = {\n  addListener(\n    eventName: 'notificationClick',\n    listenerFunc: (info: NotificationData) =&gt; void\n  ): PluginListenerHandle;\n};\nexport const ExtendedPushNotificationsPlugin = Plugins.VivedExtendedPushNotificationsPlugin as ExtendedPushNotificationsPlugin;<\/code><\/pre><h2 id=\"android\">Android<\/h2><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3cbdf27.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Zanim przejdziemy do implementacji, zatrzymajmy si\u0119 na chwil\u0119 przy stronie teoretycznej. Spos\u00f3b, w jaki Android obs\u0142uguje notyfikacje zale\u017cy od tego, czy aplikacja jest aktualnie uruchomiona oraz od formatu danych, jaki zosta\u0142 przekazany do notyfikacji po stronie serwera.<\/p><p>Notyfikacje podzieli\u0107 mo\u017cemy na dwa typy: standardowe push notyfikacje i data notyfikacje. Te pierwsze to notyfikacje, kt\u00f3re chcemy wy\u015bwietli\u0107 u\u017cytkownikowi, natomiast te drugie s\u0142u\u017c\u0105 do przekazania aplikacji danych, niekoniecznie wy\u015bwietlaj\u0105c sam\u0105 notyfikacj\u0105. Jak Android obs\u0142uguje poszczeg\u00f3lne sytuacje, najlepiej opisuje zaczerpni\u0119ta z dokumentacji tabela.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3d34006.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>W standardowym przypadku, je\u015bli aplikacja jest zamkni\u0119ta, to tracimy mo\u017cliwo\u015b\u0107 manipulowania ni\u0105. Niestety oznacza to, \u017ce je\u015bli chcemy pokaza\u0107 u\u017cytkownikom \u0142adne notyfikacje, nawet kiedy aplikacja jest wy\u0142\u0105czona, to zmuszeni b\u0119dziemy wykorzysta\u0107 mechanizm data notifications i troch\u0119 namiesza\u0107.<\/p><p>Android posiada rozbudowany wachlarz dost\u0119pnych typ\u00f3w notyfikacji, wi\u0119c zanim przejdziecie do tworzenia w\u0142asnej spersonalizowanej implementacji sprawd\u017acie, czy jedna z dost\u0119pnych nie spe\u0142nia Waszych wymaga\u0144. Warto zauwa\u017cy\u0107, \u017ce nie mo\u017cecie wymiesza\u0107 ze sob\u0105 r\u00f3\u017cnych typ\u00f3w notyfikacji. Oznacza to, \u017ce je\u015bli zdecydujecie si\u0119 na du\u017cy obrazek, to braknie Wam ju\u017c miejsca na du\u017cy tekst. Na t\u0119 zasad\u0119 nie pomo\u017ce pr\u00f3ba stworzenia swojego interfejsu notyfikacji, bo Android wprowadza odg\u00f3rny limit wysoko\u015bci notyfikacji.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b3e66855.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p><br \/><\/p><p>Przejd\u017amy teraz do cz\u0119\u015bci praktycznej. Zgodnie ze sztuk\u0105, aby obs\u0142u\u017cy\u0107 push notyfikacje, musimy stworzy\u0107 implementacj\u0119 interfejsu FirebaseMessagingService i zarejestrowa\u0107 go w AndroidManifest.xml (b\u0119d\u0105c dok\u0142adnym nale\u017ca\u0142oby powiedzie\u0107 przechwycimy intent, ale przejdziemy do tego jeszcze w dalszej cz\u0119\u015bci tutoriala). Uwaga: stworzenie w\u0142asnej implementacji serwisu spowoduje, \u017ce metody do obs\u0142ugi notyfikacji z Capacitora przestan\u0105 dzia\u0142a\u0107, wi\u0119c sami b\u0119dziecie musieli zaimplementowa\u0107 brakuj\u0105ce funkcjonalno\u015bci. Nie przejmujcie si\u0119 natomiast b\u0142\u0119dem dotycz\u0105cym braku obs\u0142ugi rejestracji tokenu. Ta cz\u0119\u015b\u0107 jest \u015bwietnie obs\u0142ugiwana przez Capacitora i mo\u017cecie pozostawi\u0107 j\u0105 nietkni\u0119t\u0105.<br \/><\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b410fe3e.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania Androidowej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure><pre><code class=\"language-kotlin\">\/\/ Note: onNewToken is handled by Capacitor's Push Notifications plugin\nclass PushNotificationsService : FirebaseMessagingService() {\n  override fun onMessageReceived(message: RemoteMessage) {\n    val rawData = RawNotificationData.parse(message)\n    val notificationData = rawData.enrich()\n    val notification = buildNotification(notificationData)\n    sendNotification(rowData.id, notification)\n  }\n}<\/code><\/pre><pre><code class=\"language-XML\">&lt;application&gt;\n    &lt;service\n      android:name=\"com.virtuslab.vived.PushNotifications\"\n      android:stopWithTask=\"false\"&gt;\n      &lt;intent-filter&gt;\n        &lt;action android:name=\"com.google.firebase.MESSAGING_EVENT\" \/&gt;\n      &lt;\/intent-filter&gt;\n    &lt;\/service&gt;\n&lt;\/application&gt;<\/code><\/pre><p>Metoda `onMessageReceived` wype\u0142niona jest jeszcze niezaimplementowanymi metodami, kt\u00f3re pokazuj\u0105 schemat jej dzia\u0142ania. Przyjrzymy si\u0119 teraz dok\u0142adnie ka\u017cdej z nich, zaczynaj\u0105c od (1). Jako \u017ce nasze dane przekazujemy za pomoc\u0105 JSONa, musimy je odpowiednio sparsowa\u0107 do klasy, kt\u00f3ra b\u0119dzie zrozumiana dla naszej aplikacji. Schemat JSONa z g\u00f3ry narzucony jest przez firebase. Wyj\u0105tkiem w tej kwestii jest pole `data`, kt\u00f3re mo\u017cemy dowolnie personalizowa\u0107, wi\u0119c mo\u017cecie pu\u015bci\u0107 wodze fantazji. My starali\u015bmy si\u0119 przygotowa\u0107 API jak najbardziej elastyczne i kompatybilne z przysz\u0142ymi zmianami.<\/p><figure class=\"kg-card kg-code-card\"><pre><code class=\"language-JSON\">method: POST\npath: \"https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send\"\nbody: {\n  \"message\": {\n    \"name\": \"69ab68da-1dae-4c25-9386-cca6346123a8\",\n    \"token\": \"firebase-token\",\n    \"notification\": null,\n    \"data\": {\n      \"url\": \"\/hub\/keep-up\",\n      \"vivedId\": \"vived-id\"\n    },\n    \"android\": {\n      \"data\": {\n        \"title\": \"Keep Up with IT World \",\n        \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\",\n        \"imageUrl\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/image\/rocket.png\",\n        \"iconUrl\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png\",\n        \"type\": \"BigText\",\n        \"actions\": [\n          {\n            \"action\": \"KEEP_UP_READ\",\n            \"title\": \"Read now\"\n          }\n        ]\n      },\n    },\n    \"apns\": { ... },\n    \"webpush\": { ... },\n}<\/code><\/pre><figcaption>Zapytanie wysy\u0142ane po strone backendu do Firebase (okrojone tylko do niezb\u0119dnej cz\u0119\u015bci)<\/figcaption><\/figure><pre><code class=\"language-Kotlin\">enum class NotificationType {\n  Image, BigText, Default\n}\ndata class NotificationAction(\n  val id: String,\n  val title: String\n) {\n  companion object {\n    fun parseJson(json: JSONObject): NotificationAction =\nGson().fromJson(str , Array&lt;NotificationAction&gt;::class.java)\n  }\n}\ndata class RawNotificationData(\n  val id: Int = Random.nextInt(1, 1000000),\n  val title: String?,\n  val body: String?,\n  val actions: List&lt;NotificationAction&gt;,\n  val imageUrl: String?,\n  val iconUrl: String?,\n  val type: NotificationType?,\n  val vivedId: String?,\n  val url: String?\n)\n  companion object {\n    fun parse(message: RemoteMessage): RawNotificationData =\n      RawNotificationData(\n        title = message.data[\"title\"] ?: message.notification?.title,\n        body = message.data[\"body\"] ?: message.notification?.body,\n        actions = message.data[\"actions\"]?.let { NotificationAction.parseJsonArray(JSONArray(it)) } ?: emptyList(),\n        imageUrl = message.data[\"imageUrl\"],\n        iconUrl = message.data[\"iconUrl\"],\n        type = NotificationType.values().find { it.name == message.data[\"type\"] } ?: NotificationType.Default,\n        vivedId = message.data[\"vivedId\"],\n        url = message.data[\"url\"]\n      )\n  }\n}<\/code><\/pre><p>W kroku (2) odczytane dane wzbogacamy o dodatkowe informacje. W naszym przypadku jest to np. obrazek pobrany z sieci. Je\u015bli wystarcz\u0105 Wam suche dane, to spokojnie mo\u017cecie pomin\u0105\u0107 ten krok.<\/p><pre><code class=\"language-Kotlin\">data class NotificationData(\n  val id: Int,\n  val title: String,\n  val body: String,\n  val actions: List&lt;NotificationAction&gt;,\n  val image: Bitmap?,\n  val icon: Bitmap?,\n  val type: NotificationType?,\n  val vivedId: String?,\n  val url: String\n)\ndata class RawNotificationData(\n  ...\n) {\n  private fun downloadBitmap(url: String): Bitmap? {\n    return try {\n      val input = URL(url).openStream()\n      BitmapFactory.decodeStream(input)\n    } catch (e: IOException) {\n      null\n    }\n  }\n  fun enrich(): NotificationData =\n    NotificationData(\n      id = id,\n      title = title ?: \"\",\n      body = body ?: \"\",\n      actions = actions,\n      image = imageUrl?.let { downloadBitmap(it) },\n      icon = iconUrl?.let { downloadBitmap(it) },\n      type = type,\n      vivedId = vivedId,\n      url = url ?: \"\/\"\n    )\n}<\/code><\/pre><p>Na tym etapie mamy ju\u017c wszystkie potrzebne dane i mo\u017cemy przej\u015b\u0107 do zbudowania notyfikacji (3). Ponownie nasza implementacja stara si\u0119 by\u0107 maksymalnie elastyczna, ale je\u015bli chcecie wprowadzi\u0107 swoje modyfikacje, to najlepiej b\u0119dzie je\u015bli zag\u0142\u0119bicie si\u0119 w mo\u017cliwo\u015bci, jakie daje Android w <a href=\"https:\/\/developer.android.com\/training\/notify-user\/build-notification\">dokumentacji.<\/a><\/p><pre><code class=\"language-Kotlin\">  private fun buildPendingIntent(data: NotificationData, actionId: String): PendingIntent {\n    val intent = PushNotificationIntent.from(data, actionId)\n    return PendingIntent.getActivity(this, Random.nextInt(), intent.toIntent(this), PendingIntent.FLAG_CANCEL_CURRENT)\n  }\n  private fun buildNotification(data: NotificationData): Notification {\n    val notificationBuilder = NotificationCompat.Builder(this, PushNotificationsPlugin.DEFAULT_CHANNEL_ID)\n      .setContentTitle(data.title)\n      .setContentText(data.body)\n      .setAutoCancel(true)\n      .setSmallIcon(R.drawable.ic_stat_notification)\n      .setContentIntent(buildPendingIntent(data, \"TAP\"))\n    data.actions.forEach { action -&gt;\n      notificationBuilder.addAction(\n        R.drawable.ic_stat_notification,\n        action.title,\n        buildPendingIntent(data, action.id)\n      )\n    }\n    return notificationBuilder.build()\n  }<\/code><\/pre><p>W powy\u017cszym kodzie znajduj\u0105 si\u0119 dwie zupe\u0142nie nowe rzeczy, nad kt\u00f3rymi warto si\u0119 pochyli\u0107. Zacznijmy od tajemniczych `PendingIntet.` Kiedy u\u017cytkownik kliknie w notyfikacj\u0119 lub jedn\u0105 z akcji, to do naszej aplikacji zostanie wys\u0142any obiekt PendingIntent, kt\u00f3ry nale\u017cy przechwyci\u0107 i obs\u0142u\u017cy\u0107. Odpowiedzialny b\u0119dzie za to `PushNotificationsPlugin`. Jako \u017ce logik\u0119 chcemy przesun\u0105\u0107 w stron\u0119 JavaScriptu, to jedynym zadaniem Pluginu b\u0119dzie zamkni\u0119cie notyfikacji i przekazanie eventu do WebView.<\/p><pre><code class=\"language-Kotlin\">data class PushNotificationIntent(\n  val notificationId: Int,\n  val vivedNotificationId: String?,\n  val actionId: String,\n  val url: String\n) {\n  companion object {\n    const val IS_FROM_PLUGIN_EXTRA = \"isFromPushNotificationPlugin\"\n    const val NOTIFICATION_ID_EXTRA = \"notificationId\"\n    const val VIVED_NOTIFICATION_ID_EXTRA = \"vivedNotificationId\"\n    const val ACTION_ID_EXTRA = \"actionId\"\n    const val URL_EXTRA = \"url\"\n    fun from(intent: Intent): PushNotificationIntent =\n      PushNotificationIntent(\n        notificationId = intent.extras?.getInt(NOTIFICATION_ID_EXTRA)!!,\n        vivedNotificationId = intent.extras?.getString(VIVED_NOTIFICATION_ID_EXTRA),\n        actionId = intent.extras?.getString(ACTION_ID_EXTRA)!!,\n        url = intent.extras?.getString(URL_EXTRA)!!\n      )\n    fun from(data: NotificationData, actionId: String): PushNotificationIntent =\n      PushNotificationIntent(\n        notificationId = data.id,\n        vivedNotificationId = data.vivedId,\n        actionId = actionId,\n        url = data.url\n      )\n  }\n  fun toIntent(context: Context): Intent =\n    Intent(context, MainActivity::class.java)\n      .putExtra(IS_FROM_PLUGIN_EXTRA, true)\n      .putExtra(ACTION_ID_EXTRA, actionId)\n      .putExtra(NOTIFICATION_ID_EXTRA, notificationId)\n      .putExtra(VIVED_NOTIFICATION_ID_EXTRA, vivedNotificationId)\n      .putExtra(URL_EXTRA, url)\n}\n@NativePlugin(name = \"VivedPushNotificationsPlugin\")\nclass PushNotificationsPlugin : Plugin() {\n  private fun shouldHandleIntent(intent: Intent?): Boolean =\n    intent?.extras?.getBoolean(PushNotificationIntent.IS_FROM_PLUGIN_EXTRA) == true\n  private fun notifyJS(intent: PushNotificationIntent) {\n    val dataObject = JSObject()\n    dataObject.put(\"url\", intent.url)\n    dataObject.put(\"vivedId\", intent.vivedNotificationId)\n    val returnObject = JSObject()\n    returnObject.put(\"data\", dataObject)\n    returnObject.put(\"action\", intent.actionId)\n    notifyListeners(\"notificationClick\", returnObject, true)\n  }\n  private fun cancelNotification(id: Int) {\n    val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n    notificationManager.cancel(id)\n  }\n  override fun handleOnNewIntent(intent: Intent?) {\n    super.handleOnNewIntent(intent)\n    if (shouldHandleIntent(intent)) {\n      val pushNotificationIntent = PushNotificationIntent.from(intent!!)\n      notifyJS(pushNotificationIntent)\n      cancelNotification(pushNotificationIntent.notificationId)\n    }\n  }<\/code><\/pre><p>Przejd\u017amy teraz do drugiej tajemniczej funkcjonalno\u015bci, czyli `PushNotificationsPlugin.DEFAULT_CHANNEL_ID`. Android od wersji 8.0 (API level 26) wprowadzi\u0142 kana\u0142y notyfikacji, kt\u00f3rymi mo\u017cna sterowa\u0107 z poziomu ustawie\u0144 aplikacji. <br \/><\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b41936bc.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Tworzenie notyfikacji bez podania kana\u0142u, jest co prawda mo\u017cliwe, ale jest ju\u017c oznaczone jako deprecated. Aby mie\u0107 pewno\u015b\u0107, \u017ce kana\u0142 notyfikacji zostanie stworzony, funkcjonalno\u015b\u0107 ta zostanie dodana do odpowiedniego hook\u2019a w stworzonym wcze\u015bniej Pluginie<\/p><pre><code class=\"language-Kotlin\">@NativePlugin(name = \"VivedPushNotificationsPlugin\")\nclass PushNotificationsPlugin : Plugin() {\n  companion object {\n    const val DEFAULT_CHANNEL_ID = \"vived_default_notifications_channel\"\n  }\n  private fun createNotificationChannel() {\n    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {\n      val importance = NotificationManager.IMPORTANCE_HIGH\n      val notificationChannel = NotificationChannel(DEFAULT_CHANNEL_ID, \"Default\", importance)\n      val notificationManager = this.activity.getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) as NotificationManager\n      notificationManager.createNotificationChannel(notificationChannel)\n    }\n  }\n  override fun load() {\n    createNotificationChannel()\n  }\n  ....\n}<\/code><\/pre><p>Tak jak wspomnia\u0142em na pocz\u0105tku, wersje notyfikacji dostarczone przez Androida w zupe\u0142no\u015bci nam wystarczy\u0142y, ale dla wszystkich, kt\u00f3rzy oczekuj\u0105 wi\u0119cej, mam ma\u0142e preview takiej funkcjonalno\u015bci.<\/p><p>Je\u015bli p\u00f3jdziecie w ca\u0142kowicie personalizowan\u0105 notyfikacj\u0119, to pami\u0119tajcie o <a href=\"https:\/\/developer.android.com\/training\/notify-user\/custom-notification#:~:text=The%20height%20available%20for%20a,are%20limited%20to%20256%20dp.\">ograniczeniu wysoko\u015bci widoku<\/a> i mo\u017cliwo\u015bci skorzystania tylko z <a href=\"https:\/\/developer.android.com\/reference\/android\/widget\/RemoteViews\">wybranych element\u00f3w UI<\/a>, a tak\u017ce o odg\u00f3rnym ograniczeniu wysoko\u015bci notyfikacji.<\/p><p>W celach edukacyjnych nasza notyfikacja b\u0119dzie wy\u015bwietla\u0107 tylko prosty tekst. Prawdopodobnie od swojej notyfikacji oczekiwa\u0107 b\u0119dziecie jednak troch\u0119 wi\u0119cej, ale w tym celu b\u0119dziecie musieli zanurzy\u0107 si\u0119 w niuansach tworzenia androidowych widok\u00f3w.<\/p><pre><code class=\"language-XML\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\n&lt;LinearLayout xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\n  android:orientation=\"vertical\" android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"&gt;\n  &lt;TextView\n    android:id=\"@+id\/text_view_id\"\n    android:layout_height=\"wrap_content\"\n    android:layout_width=\"wrap_content\"\n    android:text=\"Hello there Obi-Wan Kenobi\" \/&gt;\n&lt;\/LinearLayout&gt;<\/code><\/pre><pre><code class=\"language-Kotlin\">\/\/ Get the layouts to use in the custom notification\nval notificationLayoutExpanded = RemoteViews(packageName, R.layout.notification_large)\n\/\/ Apply the layouts to the notification\nval customNotification = NotificationCompat.Builder(context, CHANNEL_ID)\n        .setSmallIcon(R.drawable.notification_icon)\n        .setStyle(NotificationCompat.DecoratedCustomViewStyle())\n        .setCustomBigContentView(notificationLayoutExpanded)\n        .build()<\/code><\/pre><p>Ufff\u2026 Uda\u0142o nam si\u0119 zaimplementowa\u0107 cz\u0119\u015b\u0107 androidow\u0105, wi\u0119c mo\u017cemy rusza\u0107 dalej.<\/p><h2 id=\"web-pwa\">Web\/PWA<\/h2><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b42a0a9a.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Je\u015bli chodzi o push notyfikacje na webie, to niestety wci\u0105\u017c mo\u017cemy m\u00f3wi\u0107 tutaj tylko o Webowym Androidzie. Pomimo niezliczonych pr\u00f3\u015bb i wniosk\u00f3w nic nie wskazuje na to, \u017ce Apple zacznie patrze\u0107 na t\u0105 funkcjonalno\u015b\u0107 chocia\u017c odrobin\u0119 przychylniej.<\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4311c88.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania Webowej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure><p>Je\u015bli m\u00f3wimy natomiast o Webowym Androidzie, to niesamowicie wa\u017cne jest u\u015bwiadomienie sobie, \u017ce obowi\u0105zuj\u0105 tutaj te same zasady, co przy natywnych androidowych notyfikacjach. Mamy wi\u0119c te same mo\u017cliwo\u015bci konfiguracji, ale opakowane w zdecydowanie elastyczniejsze API. Wszystkie parametry mo\u017cemy przekazywa\u0107 w zapytaniu po stronie backendu. A\u017c dziwne, \u017ce Android nie wspiera podobnego API.<\/p><p>Niestety web push notifications nie wspieraj\u0105 aktualnie \u017cadnych dodatkowych mo\u017cliwo\u015bci personalizacji.<\/p><pre><code class=\"language-JSON\">method: POST\npath: \"https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send\"\nbody: {\n  \"message\": {\n    \"name\": \"69ab68da-1dae-4c25-9386-cca6346123a8\",\n    \"token\": \"firebase-token\",\n    \"notification\": null,\n    \"data\": {\n      \"url\": \"\/hub\/keep-up\",\n      \"vivedId\": \"vived-id\"\n    },\n    \"android\": { ... },\n    \"apns\": { ... },\n    \"webpush\": {\n      \"notification\": {\n        \"title\": \"Keep Up with IT World \",\n        \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\",\n        \"actions\": [\n          {\n            \"action\": \"KEEP_UP_READ\",\n            \"title\": \"Read now\"\n          }\n        ],\n        \"badge\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/badge\/vived-badge.png\",\n        \"icon\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png\",\n        \"image\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/icon\/rocket.png\"\n      }\n    },\n}<\/code><\/pre><p>Zapytanie wysy\u0142ane po strone backendu do Firebase (okrojone tylko do niezb\u0119dnej cz\u0119\u015bci)<\/p><p>Warto zwr\u00f3ci\u0107 uwag\u0119, \u017ce typem notyfikacji sterujemy, poprzez to kt\u00f3re pola pozostawimy wype\u0142nione.<\/p><pre><code class=\"language-TypeScript\">declare const self: ServiceWorkerGlobalScope;\nconst openUrl = async (url: string): Promise&lt;void&gt; =&gt; {\n  const windowClients = (await self.clients.matchAll({\n    type: 'window',\n  })) as WindowClient[];\n  const activeClient = windowClients.find(\n    it =&gt; it.visibilityState === 'visible'\n  );\n  if (activeClient) {\n    await activeClient.navigate(url);\n  } else if (self.clients.openWindow) {\n    await self.clients.openWindow(url);\n  }\n};\nexport const initializePushNotifications = (): void =&gt; {\n  self.addEventListener('notificationclick', event =&gt; {\n    const notification: MessagePayload = event.notification.data.FCM_MSG;\n    const data = notification.data as PushNotificationData;\n    event.notification.close(); \/\/ Android needs explicit close.\n    event.waitUntil(openUrl(data.url));\n  });\n};<\/code><\/pre><h2 id=\"ios\">iOS<\/h2><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b435e2fa.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Dobrn\u0119li\u015bmy do ostatniej wspieranej przez nas platformy i jednocze\u015bnie tej, kt\u00f3ra sprawi\u0142a nam najwi\u0119cej problem\u00f3w. Na szcz\u0119\u015bcie nie ze wzgl\u0119du na s\u0142abe API, a na nasze skromne umiej\u0119tno\u015bci :).<\/p><p>Podstawowe notyfikacje na iOS wspieraj\u0105 tylko nag\u0142\u00f3wek i tekst. Dodaj\u0105c pewn\u0105 modyfikacj\u0119 po stronie natywnej aplikacji, funkcjonalno\u015b\u0107 t\u0105 mo\u017cemy rozszerzy\u0107 o obrazek.<\/p><p>Je\u015bli to dla Was za ma\u0142o, to mamy te\u017c mo\u017cliwo\u015b\u0107 dowolnej personalizacji rozwini\u0119tej notyfikacji przez dostarczenie natywnego widoku. I w tym przypadku m\u00f3wimy o naprawd\u0119 dowolnej personalizacji, bo nie mamy tutaj ogranicze\u0144 podobnych do tych z Androida.<\/p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b43962fa.png\" class=\"kg-image\" alt loading=\"lazy\"><figcaption>Schemat dzia\u0142ania iOS-owej cz\u0119\u015bci obs\u0142ugi notyfikacji<\/figcaption><\/figure><pre><code class=\"language-JSON\">method: POST\npath: \"https:\/\/fcm.googleapis.com\/v1\/projects\/${firebaseProjectName}\/messages:send\"\nbody: {\n  \"message\": {\n    \"name\": \"69ab68da-1dae-4c25-9386-cca6346123a8\",\n    \"token\": \"firebase-token\",\n    \"notification\": null,\n    \"data\": {\n      \"url\": \"\/hub\/keep-up\",\n      \"vivedId\": \"vived-id\"\n    },\n    \"android\": { ... },\n    \"apns\": {\n      \"payload\": {\n        \"aps\": {\n          \"alert\": {\n            \"title\": \"Keep Up with IT World \",\n            \"subtitle\": null,\n            \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\"\n          },\n          \"category\": \"KEEP_UP\",\n          \"mutableContent\": 1\n        },\n        \"imageUrl\": \"https:\/\/uploads.vived.io\/images\/static\/notifications\/image\/rocket.png\",\n        \"body\": \"Read all articles selected for you today:\\n\u2022 \\\"Easily create web extensions for Safari\\\"\\n\u2022...\"\n      }\n    },\n    \"webpush\": { ... },\n}<\/code><\/pre><p>Zacznijmy od nadpisania obs\u0142ugi notyfikacji dostarczonej przez Capacitora. W tym celu musimy stworzy\u0107 w\u0142asny plugin i zarejestrowa\u0107 go jako instancj\u0119 obs\u0142uguj\u0105c\u0105 notyfikacje.<\/p><pre><code class=\"language-Swift\">@objc(ExtendedPushNotificationsPlugin)\npublic class ExtendedPushNotificationsPlugin: CAPPlugin, UNUserNotificationCenterDelegate {\n    private func notifyJS(response: UNNotificationResponse) {\n        var notificationAction: String;\n        switch(response.actionIdentifier) {\n        case \"KEEP_UP_READ\":\n            notificationAction = \"KEEP_UP_READ\"\n        default:\n            notificationAction = \"TAP\"\n        }\n        let notificationData = [\n            \"url\": response.notification.request.content.userInfo[\"url\"],\n            \"vivedId\": response.notification.request.content.userInfo[\"vivedId\"]\n        ];\n        self.notifyListeners(\"notificationClick\", data: [\n            \"data\": notificationData,\n            \"action\": notificationAction\n        ], retainUntilConsumed: true);\n    }\n    private func registerCustomNotificationCategories() {\n        let readNowAction = UNNotificationAction(\n            identifier: \"KEEP_UP_READ\",\n            title: \"Read Now\",\n            options: [UNNotificationActionOptions.foreground]\n        )\n        \/\/ Define the notification type\n        let dailyKeepUpNotificationCategory = UNNotificationCategory(\n            identifier: \"KEEP_UP\",\n            actions: [readNowAction],\n            intentIdentifiers: [],\n            hiddenPreviewsBodyPlaceholder: \"\",\n            options: []\n        )\n        \/\/ Register the notification type.\n        let notificationCenter = UNUserNotificationCenter.current()\n        notificationCenter.setNotificationCategories([dailyKeepUpNotificationCategory])\n    }\n    @objc override public func load() {\n        UNUserNotificationCenter.current().delegate = self\n        registerCustomNotificationCategories()\n    }\n    public func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                       didReceive response: UNNotificationResponse,\n                                       withCompletionHandler completionHandler:\n                                        @escaping () -&gt; Void) {\n        completionHandler()\n        notifyJS(response: response)\n    }\n    @objc public func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                             willPresent notification: UNNotification,\n                                             withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -&gt; Void) {\n        completionHandler([\n            .badge,\n            .sound,\n            .alert\n        ])\n    }<\/code><\/pre><p>Rzecz\u0105, kt\u00f3ra mo\u017ce wzbudza\u0107 Wasze w\u0105tpliwo\u015bci, jest r\u0119czne tworzenie akcji. W odr\u00f3\u017cnienia od Androida i Weba tutaj nie mamy mo\u017cliwo\u015bci spersonalizowania akcji po stronie serwera i nale\u017cy dokona\u0107 tego po stronie klienta. Jest to rozwi\u0105zanie, kt\u00f3re niestety blokuje nam mo\u017cliwo\u015b\u0107 dodania kolejnych przycisk\u00f3w po wypuszczeniu aplikacji, ale z drugiej strony pozwala nam ca\u0142kowicie unikn\u0105\u0107 niedzia\u0142aj\u0105cych przycisk\u00f3w (ich brak uwa\u017cam za lepsze zachowanie).<\/p><p>Nasz plugin obs\u0142uguje ju\u017c notyfikacje, wi\u0119c mo\u017cemy przej\u015b\u0107 do wzbogacenia ich o obrazek. W tym celu musimy stworzy\u0107 NotificationServiceExtension, kt\u00f3ry b\u0119dzie modyfikowa\u0142 notyfikacj\u0119 zanim ta zostanie wy\u015bwietlona.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b43e21e7.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b45cc6c6.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b46d47a7.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Podobnie jak w przypadku Androida, tak i tutaj b\u0119dziemy chcieli wzbogaci\u0107 dane otrzymane od backendu. Tak samo jak poprzednio, b\u0119dzie to pobranie zdj\u0119cia, ale nic nie stoi na przeszkodzie, aby by\u0142o to np. dodanie tekstu do notyfikacji. Za modyfikacj\u0119 odpowiedzialna b\u0119dzie metoda `didReceive` z `NotificationService`.<\/p><p>Note: Przekazanie notyfikacji do UNNotificationServiceExtension zale\u017cy od tego, czy po stronie serwera zdefiniujemy parametr `mutableContent`: 1.<\/p><pre><code class=\"language-Swift\">import UserNotifications\nclass NotificationService: UNNotificationServiceExtension {\n    var contentHandler: ((UNNotificationContent) -&gt; Void)?\n    var bestAttemptContent: UNMutableNotificationContent?\n    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -&gt; Void) {\n        self.contentHandler = contentHandler\n        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)\n        guard let bestAttemptContent = bestAttemptContent,\n              let attachmentURLAsString = bestAttemptContent.userInfo[\"imageUrl\"] as? String,\n              let attachmentURL = URL(string: attachmentURLAsString) else {\n            return\n        }\n        addImageToAttachments(url: attachmentURL) { (attachment) in\n            if let attachment = attachment {\n                bestAttemptContent.attachments = [attachment]\n                contentHandler(bestAttemptContent)\n            }\n        }\n    }\n}<\/code><\/pre><p>W pierwszym kroku odpakowujemy otrzymane dane, korzystaj\u0105c z mechanizmu <a href=\"https:\/\/docs.swift.org\/swift-book\/ReferenceManual\/Statements.html#grammar_if-statement\">guard statement.<\/a> \u00a0Nast\u0119pnie pobieramy asynchronicznie obrazek i dodajemy go do za\u0142\u0105cznik\u00f3w naszej notyfikacji. Na koniec korzystaj\u0105c z mechanizmu if-let, odpakowujemy wynik operacji i przekazujemy nasz\u0105 notyfikacj\u0119 do kolejnego handlera.<\/p><pre><code class=\"language-Swift\">    private func addImageToAttachments(url: URL, with completitionHandler: @escaping (UNNotificationAttachment?) -&gt; Void) {\n        let task = URLSession.shared.downloadTask(with: url) { (downloadedUrl, response, error) in\n            guard let downloadedUrl = downloadedUrl else {\n                completitionHandler(nil)\n                return\n            }\n            let uniqueURLEnding = ProcessInfo.processInfo.globallyUniqueString + \".jpg\"\n            let urlPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(uniqueURLEnding)\n            try? FileManager.default.moveItem(at: downloadedUrl, to: urlPath)\n            do {\n                let attachment = try UNNotificationAttachment(identifier: \"picture\", url: urlPath, options: nil)\n                completitionHandler(attachment)\n            }\n            catch {\n                completitionHandler(nil)\n            }\n        }\n        task.resume()\n    }<\/code><\/pre><p>Pobieranie zaczynamy od wywo\u0142ania w\u0142a\u015bciwego zasobu z sieci. Nast\u0119pnie odczytujemy adres, do jakiego pobrany zosta\u0142 obraz i kopiujemy go do tymczasowego pliku. Na koniec wywo\u0142ujemy completitionHandler.<\/p><p>Tip: Je\u015bli nie mo\u017cecie za\u0142apa\u0107 koncepcji `completitionHandler`, to potraktujcie go jako odpowiednik Promise.resolve().<\/p><p>Je\u015bli na tym etapie wy\u015blecie notyfikacj\u0119, to powinna ona zawiera\u0107 pobrany przez nas obrazek. Je\u015bli natomiast na tym nie ko\u0144cz\u0105 si\u0119 Wasze wymagania, to iOS umo\u017cliwia nam przygotowanie dowolnego widoku dla rozwini\u0119tej notyfikacji. W tym celu musimy stworzy\u0107 kolejny Application Target: Notification Content Extension.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b481e708.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b49cc743.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4b374a6.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Zanim zaczniemy implementacj\u0119 widoku, nale\u017cy doda\u0107 do pliku info.plist UNNotificationExtensionCategory zgodne z tym, kt\u00f3re definiujemy po stronie backendu.<\/p><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4c72574.png\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Tworzenie natywnych widok\u00f3w przypomina troch\u0119 czarn\u0105 magi\u0119, polegaj\u0105c\u0105 na \u0142\u0105czeniu mi\u0119dzy sob\u0105 r\u00f3\u017cnych element\u00f3w i definiowaniu margines\u00f3w. Jest to proces na tyle skomplikowany, \u017ce gdybym chcia\u0142 dobrze go opisa\u0107, to musia\u0142bym kilkukrotnie rozszerzy\u0107 ten tutorial. Z tego wzgl\u0119du wszystkich zainteresowanych odsy\u0142am do innego tutoriala, kt\u00f3ry zrobi\u0142 to dobrze. W tym miejscu podziel\u0119 si\u0119 natomiast samym kodem definiuj\u0105cym logik\u0119 (tej na szcz\u0119\u015bcie nie ma du\u017co, bo ustawiamy tylko warto\u015bci poszczeg\u00f3lnym elementom interfejsu).<\/p><pre><code class=\"language-Swift\">import UIKit\nimport UserNotifications\nimport UserNotificationsUI\nclass NotificationViewController: UIViewController, UNNotificationContentExtension {\n    @IBOutlet weak var titleLabel: UILabel!\n    @IBOutlet weak var bodyLabel: UILabel!\n    @IBOutlet weak var imageView: UIImageView!\n    override func viewDidLoad() {\n        super.viewDidLoad()\n    }\n    func didReceive(_ notification: UNNotification) {\n        self.titleLabel.text = notification.request.content.title\n        self.bodyLabel.text = notification.request.content.userInfo[\"longBody\"] as? String ?? notification.request.content.body\n        let attachments = notification.request.content.attachments\n        for attachment in attachments {\n            if attachment.identifier == \"picture\" &amp;&amp; attachment.url.startAccessingSecurityScopedResource() {\n                guard let data = try? Data(contentsOf: attachment.url) else {\n                    return\n                }\n                imageView.image = UIImage(data: data)\n            }\n        }\n    }\n}<\/code><\/pre><h1 id=\"podsumowanie\">Podsumowanie<\/h1><figure class=\"kg-card kg-image-card\"><img src=\"https:\/\/vived.io\/wp-content\/uploads\/2021\/08\/img_610d0b4e8af35.gif\" class=\"kg-image\" alt loading=\"lazy\"><\/figure><p>Mam nadziej\u0119, \u017ce wszyscy wygl\u0105dacie teraz tak samo jak Anakin na gifie powy\u017cej! Je\u015bli nie to niech moc b\u0119dzie z Wami\u2026 Notyfikacje to jeden z tych temat\u00f3w, kt\u00f3re na papierze wydaj\u0105 si\u0119 banalnie proste, a przy pr\u00f3bie implementacji okazuj\u0105 si\u0119 r\u00f3wnie zawi\u0142e, co dzieje rodu Skywalker\u00f3w w uniwersum Gwiezdnych Wojen. Mam nadziej\u0119, \u017ce praca w\u0142o\u017cona w ten tutorial, chocia\u017c odrobin\u0119 u\u0142atwi Wam prac\u0119 i sprawi, \u017ce Wasi managerowie b\u0119d\u0105 dumni ze zrealizowanych przez Was w tym sprincie punkt\u00f3w. Ja tymczasem \u017cegnam si\u0119 z Wami i do zobaczenia w kolejnej edycji Frontendowego Czwartku.<\/p>"]}],"_links":{"self":[{"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/posts\/10153","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/users\/12"}],"replies":[{"embeddable":true,"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/comments?post=10153"}],"version-history":[{"count":0,"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/posts\/10153\/revisions"}],"wp:attachment":[{"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/media?parent=10153"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/categories?post=10153"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vived.io\/pl\/wp-json\/wp\/v2\/tags?post=10153"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}