OneSignal を使って 以下機能を実装してみました。
- WebPush 通知 Icon クリックで、購読の状態を切り替える。
- Segment
All
で、APサーバーから通知する。
実装時に実施したことを記載します。
前提
バックエンド、フロントエンドには、それぞれ以下、ライブラリを使用しています。
バックエンド
Django 、 wagtail、 puput を使用しています。
- Django (1.11.11)
- wagtail (1.13.1)
- puput (0.9.2.1)
フロントエンド
react と、 React Material Web Components を使用しています。
- react@16.2.0
- rmwc@1.1.2
WebPush 通知 Icon クリックで、購読の状態を切り替える。
OneSignal の デフォルトのベルを非表示にして、別途通知 Icon を使って、WebPush 購読の状態を切り替えるようにしてみました。
OneSingnal の初期化時に、ベルを非表示にする
notifyButton: { enable: false }
を設定することで、ベルが非表示になります。
特にそこまで気に入らないというわけでなければ、色を変えたり、表示位置を変更することはできるので、サイトの色に合わせて調整することはできるかと思います。
Custom Code Examples
- main.js
<script async src="https://cdn.onesignal.com/sdks/OneSignalSDK.js" /> <script type="text/javascript">{` var OneSignal = window.OneSignal || []; OneSignal.push(["init", { appId: "${config.oneSignalAppId}", autoRegister: false, allowLocalhostAsSecureOrigin: true, notifyButton: { enable: false /* Set to false to hide */ } }]); `}</script>
画面右上に、WebPush 通知 Icon を設置する
以下、画面上部の Navbar の JavaScript になります。
プログラムの下部に説明を記載します。
-
Navbar.js
import React, {Component} from "react"; import {Toolbar, ToolbarRow, ToolbarSection, ToolbarMenuIcon, ToolbarTitle, ToolbarIcon} from "rmwc"; import config from "~/data/siteConfig"; const getSubscriptionState = function() { var OneSignal = window.OneSignal || []; return Promise.all([ OneSignal.isPushNotificationsEnabled(), OneSignal.isOptedOut(), OneSignal.getNotificationPermission() ]).then(function(result) { var isPushEnabled = result[0]; var isOptedOut = result[1]; var notificationPermission = result[2]; return { isPushEnabled: isPushEnabled, isOptedOut: isOptedOut, notificationPermission: notificationPermission }; }); }; export default class Navbar extends Component { constructor(props) { super(props); var self = this; self.state = {}; self.getNotificationsIcon = self.getNotificationsIcon.bind(self); self.setCurrentNotificationState = self.setCurrentNotificationState.bind(self); } setCurrentNotificationState() { var self = this; getSubscriptionState().then(function(state) { self.setState({isPushEnabled: state.isPushEnabled}); self.setState({isOptedOut: state.isOptedOut}); self.setState({notificationPermission: state.notificationPermission}); }); } componentDidMount() { var self = this; window.addEventListener("load", function() { self.setCurrentNotificationState(); var OneSignal = window.OneSignal || []; /* This example assumes you've already initialized OneSignal */ OneSignal.push(function() { // If we're on an unsupported browser, do nothing if (!OneSignal.isPushNotificationsSupported()) { return; } OneSignal.on("subscriptionChange", function() { self.setCurrentNotificationState(); }); OneSignal.on("notificationPermissionChange", function(permissionChange) { var currentPermission = permissionChange.to; if(currentPermission === "granted") { // Subscription false > true の場合は、subscriptionChange が呼び出される // self.setCurrentNotificationState(); は呼び出さなくてOK OneSignal.setSubscription(true); } else { self.setCurrentNotificationState(); } }); }); }); } onNotificationButtonClicked(e) { var self = this; getSubscriptionState().then(function(state) { var OneSignal = window.OneSignal || []; if (state.isPushEnabled) { /* Subscribed, opt them out */ OneSignal.setSubscription(false); } else { if (state.isOptedOut) { /* Opted out, opt them back in */ OneSignal.setSubscription(true); } else { /* Unsubscribed, subscribe them */ OneSignal.registerForPushNotifications(); } } }).then(function(){ // true > false への変化の場合、subscriptionChange が呼び出されないので、ここで、 // self.setCurrentNotificationState(); を呼び出しておく。 self.setCurrentNotificationState(); }); e.preventDefault(); } getNotificationsIcon() { if(this.state.notificationPermission === "denied") { return "notifications_off"; } if(this.state.isPushEnabled) { return "notifications_active"; } else { if(this.state.isOptedOut) { return "notifications_paused"; } } return "notifications_none"; } render() { return ( <Toolbar> <ToolbarRow> <ToolbarSection alignStart> <ToolbarMenuIcon use="menu" onClick={this.props.toggle}/> <ToolbarTitle>{config.siteTitle}</ToolbarTitle> </ToolbarSection> <ToolbarSection alignEnd> <ToolbarIcon tag="a" href={config.siteUrl + "/feed"} strategy="ligature" use="rss_feed"/> <ToolbarIcon tag="a" onClick={e => this.onNotificationButtonClicked(e)} strategy="ligature" use={this.getNotificationsIcon()} /> </ToolbarSection> </ToolbarRow> </Toolbar> ); } }
-
説明
-
WebPush ToolbarIcon について
が WebPush Iconになります。ToolbarIcon は rmwc/toolbar.md at master · jamesmfriedman/rmwc に説明が記載されています。<ToolbarIcon tag="a" onClick={e => this.onNotificationButtonClicked(e)} strategy="ligature" use={this.getNotificationsIcon()} />
use
属性で、getNotificationsIcon
ファンクションを指定して、通知の状態で、Icon を切り替えています。
onClick
属性で、onNotificationButtonClicked
ファンクションを指定して、通知の状態を切り替えています。 -
componentDidMount について
コンポーネントの Mount 完了時に、EventListener を追加しています。タイミングが、load
なのは、ComtentDomLoaded
のタイミングではOneSignal の Javascript が読み込まれておらず、未定義エラーになったためです。
EventListener では、OneSignal のsubscriptionChange
と、notificationPermissionChange
を追加しています。
subscriptionChange
は、通知購読の状態が変更された場合に呼び出される処理で、notificationPermissionChange
は 通知のパーミッションの変更時に呼び出される処理になります。subscriptionChange
はfalse > true
のケースのみ呼び出される処理のようで、true > false
のケースでも行いたい処理がある場合は、明示的に呼び出す必要があります。
notificationPermissionChange
では、パーミッションがgranted
の場合、OneSignal.setSubscription(true);
を呼び出していますが、これによって、false > true
となり、subscriptionChange
が起動し、Icon の表示が切り替わります。 -
getSubscriptionState について
Custom Code Examples に記載のあるプログラムを参考にしつつ、OneSignal.getNotificationPermission()
を追加して、現状の通知パーミッションの状態を取得するようにしました。
これは、通知 Icon の切り替えで、通知の状態も加味したかったため取得しています。 -
通知 Icon について
Icon は以下、4つの状態があります。-
notifications_off 通知は拒否された場合に、この Icon 表示になります。
-
notifications_active
通知は許可されていて、購読も許可されている場合、この Icon 表示になります。 -
notifications_paused
通知は許可されていて、購読は不許可(停止した)に設定されている場合、この Icon 表示になります。 -
notifications_none
通知に対するアクションをユーザ が何もとっていないか、画面上のオペレーション以外の方法で WebPush 通知の状態を編集した場合にこの Icon 表示になります。
-
-
Segment All
で、APサーバーから通知する。
記事の更新があった場合、Segment All
に対して、Webpush 通知を送信する Django コマンドを作成しました。
実装には、joaobarbosa/onesignal-python: Python client for OneSignal push notification service という OneSignal の python クライアントがありましたので、それを使用しました。
設定方法
-
インストール
python3 -m pip install onesignal-python
-
onesignalclient
を settings.py に追加する
INSTALLED_APPS = [ .... 'onesignalclient' ]
-
settings.py に、ONESIGNAL_REST_API_KEY と、ONESIGNAL_APP_ID を追加する
OneSignal の アプリケーションを作成すると、ONESIGNAL APP ID と、REST API KEY が払い出されます。
払い出された、ID と KEY を settings.py に追加します。
これは、後述する Django コマンドで使用しています。ONESIGNAL_APP_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ONESIGNAL_REST_API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Django コマンド
新着投稿を WebPush する Django コマンドを作成しました。
-
send_notigications.py
# -*- coding: utf-8 -*- from __future__ import print_function from django.core.management.base import BaseCommand from requests.exceptions import HTTPError from onesignalclient.app_client import OneSignalAppClient from onesignalclient.notification import Notification from puput.models import EntryPage from django.conf import settings import datetime class Command(BaseCommand): def handle(self, **options): now = datetime.datetime.now() entries = EntryPage.objects.filter(date__gte=now - datetime.timedelta(hours=4)) if entries: entry = entries.first() content = entry.title # Init the client client = OneSignalClient(app_id=settings.ONESIGNAL_APP_ID, app_api_key=settings.ONESIGNAL_REST_API_KEY) # Creates a new notification notification = Notification(settings.ONESIGNAL_APP_ID, Notification.SEGMENTS_MODE) notification.contents = {"en": content} notification.headings = {"en": "New article"} option = {} option.update({"included_segments" : ["All"]}); try: # Sends it! result = client.create_notification_with_option(notification, option) except HTTPError as e: result = e.response.json() class OneSignalClient(OneSignalAppClient): def create_notification_with_option(self, notification, option): """ Creates a new notification. :param notification: onesignalclient.notification.Notification object """ payload = notification.get_payload_for_request() payload.update(option) print(payload) return self.post(self._url(self.ENDPOINTS['notifications']), payload=payload)
-
説明
-
Bodyに設定している文字列について WebPush は スペースの都合なのかあまり長い文字列を body 部に設定できず、ブラウザにより設定できる文字数も異なるようです。
このため、1つの記事のタイトルを取り出して、body に設定するようにしました。 -
OneSignalClientについて
nesignal-python の、OneSignalAppClient は、セグメントに対する送信ができないため、OneSignalAppClient を拡張して、Segment に対して、送信できるようにしました。 -
コマンドのスケジューリングについて
実行タイミングの4時間前までに作成された記事を取得して、WebPush 通知を行うようにしたため、crontab でのスケジュール実行も4時間 ごとに実装するようにしました。
ライブラリを使わないで実装する
Create notification に Python で実装された クライアントのサンプルがあります。ライブラリを使わずに、実装しても、それほど複雑にはならなそうだったので、最終的にライブラリの使用しない実装に書き換えました。
-
-
send_notigications.py
余計な継承クラスがない分、ライブラリを使用しないほうが、シンプルですね。
# -*- coding: utf-8 -*- from __future__ import print_function from django.core.management.base import BaseCommand from requests.exceptions import HTTPError from puput.models import EntryPage from django.conf import settings import datetime import requests import json class Command(BaseCommand): def handle(self, **options): now = datetime.datetime.now() entries = EntryPage.objects.filter(date__gte=now - datetime.timedelta(hours=4)) if entries: entry = entries.first() content = entry.title header = {"Content-Type": "application/json; charset=utf-8", "Authorization": "Basic " + settings.ONESIGNAL_REST_API_KEY} payload = {"app_id": settings.ONESIGNAL_APP_ID, "included_segments": ["All"], "contents": {"en": content}, "headings": {"en": "New article"} } req = requests.post("https://onesignal.com/api/v1/notifications", headers=header, data=json.dumps(payload))
コメント