Skip to content
This repository has been archived by the owner on Sep 20, 2023. It is now read-only.

Commit

Permalink
Notifications server with push subscriptions and service worker
Browse files Browse the repository at this point in the history
  • Loading branch information
micahalcorn committed Oct 22, 2018
1 parent 2994303 commit 5250352
Show file tree
Hide file tree
Showing 25 changed files with 1,570 additions and 37 deletions.
Binary file added origin-dapp/public/images/app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions origin-dapp/public/images/notifications-computer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions origin-dapp/public/images/notifications-warning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions origin-dapp/public/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
self.addEventListener('push', event => {
const { account, body, offerId, title } = event.data.json()
const promiseChain = self.registration.showNotification(title, {
body,
data: { account, offerId },
icon: '/images/app-icon.png',
lang: 'en-US',
requireInteraction: true,
vibrate: [300, 100, 400]
})

event.waitUntil(promiseChain)
})

self.addEventListener('notificationclick', event => {
event.notification.close()

const { account, offerId } = event.notification.data
const purchaseDetailPath = `/#/purchases/${offerId}?account=${account}`
const urlToOpen = new URL(purchaseDetailPath, self.location.origin).href

const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
}).then((windowClients) => {
let matchingClient = null

for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i]

if (windowClient.url === urlToOpen) {
matchingClient = windowClient

break
}
}

if (matchingClient) {
return matchingClient.focus()
} else {
return clients.openWindow(urlToOpen)
}
})

event.waitUntil(promiseChain)
})

self.addEventListener('notificationclose', event => {
// track dismissal
})
62 changes: 61 additions & 1 deletion origin-dapp/src/actions/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { unblock } from 'actions/Onboarding'
import { showAlert } from 'actions/Alert'

import keyMirror from 'utils/keyMirror'
import { createSubscription } from 'utils/notifications'
import {
addLocales,
getAvailableLanguages,
Expand All @@ -24,10 +25,14 @@ export const AppConstants = keyMirror(
MESSAGING_ENABLED: null,
MESSAGING_INITIALIZED: null,
NOTIFICATIONS_DISMISSED: null,
NOTIFICATIONS_HARD_PERMISSION: null,
NOTIFICATIONS_SOFT_PERMISSION: null,
NOTIFICATIONS_SUBSCRIPTION_PROMPT: null,
ON_MOBILE: null,
SAVE_SERVICE_WORKER_REGISTRATION: null,
TRANSLATIONS: null,
WEB3_ACCOUNT: null,
WEB3_INTENT: null,
TRANSLATIONS: null,
WEB3_NETWORK: null
},
'APP'
Expand Down Expand Up @@ -64,6 +69,54 @@ export function dismissNotifications(ids) {
}
}

export function handleNotificationsSubscription(role, props = {}) {
return async function(dispatch) {
const {
notificationsHardPermission,
notificationsSoftPermission,
pushNotificationsSupported,
serviceWorkerRegistration,
web3Account
} = props

if (!pushNotificationsSupported) {
return
}

if (notificationsHardPermission === 'default') {
if ([null, 'warning'].includes(notificationsSoftPermission)) {
dispatch(handleNotificationsSubscriptionPrompt(role))
}
// existing subscription may need to be replicated for current account
} else if (notificationsHardPermission === 'granted') {
createSubscription(serviceWorkerRegistration, web3Account)
}
}
}

export function handleNotificationsSubscriptionPrompt() {
return {
type: AppConstants.NOTIFICATIONS_SUBSCRIPTION_PROMPT,
role
}
}

export function setNotificationsHardPermission(result) {
return {
type: AppConstants.NOTIFICATIONS_HARD_PERMISSION,
result
}
}

export function setNotificationsSoftPermission(result) {
localStorage.setItem('notificationsPermissionResponse', result)

return {
type: AppConstants.NOTIFICATIONS_SOFT_PERMISSION,
result
}
}

export function enableMessaging() {
return function(dispatch) {
try {
Expand Down Expand Up @@ -155,3 +208,10 @@ export function localizeApp() {
messages
}
}

export function saveServiceWorkerRegistration(registration) {
return {
type: AppConstants.SAVE_SERVICE_WORKER_REGISTRATION,
registration
}
}
14 changes: 12 additions & 2 deletions origin-dapp/src/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HashRouter as Router, Route, Switch } from 'react-router-dom'
import { connect } from 'react-redux'
import { IntlProvider } from 'react-intl'

import { localizeApp, setMobile } from 'actions/App'
import { localizeApp, saveServiceWorkerRegistration, setMobile } from 'actions/App'
import { fetchProfile } from 'actions/Profile'
import {
getEthBalance,
Expand Down Expand Up @@ -41,6 +41,7 @@ import SearchBar from 'components/search/searchbar'
import 'bootstrap/dist/js/bootstrap'

import { setClickEventHandler } from 'utils/analytics'
import { initServiceWorker } from 'utils/notifications'

// CSS
import 'bootstrap/dist/css/bootstrap.css'
Expand Down Expand Up @@ -100,13 +101,21 @@ class App extends Component {
setClickEventHandler()
}

componentDidMount() {
async componentDidMount() {
this.props.fetchProfile()
this.props.initWallet()
this.props.getEthBalance()
this.props.getOgnBalance()

this.detectMobile()

try {
const reg = await initServiceWorker()

this.props.saveServiceWorkerRegistration(reg)
} catch (error) {
console.error(error)
}
}

componentDidUpdate() {
Expand Down Expand Up @@ -205,6 +214,7 @@ const mapDispatchToProps = dispatch => ({
getEthBalance: () => dispatch(getEthBalance()),
getOgnBalance: () => dispatch(getOgnBalance()),
initWallet: () => dispatch(initWallet()),
saveServiceWorkerRegistration: reg => dispatch(saveServiceWorkerRegistration(reg)),
setMobile: device => dispatch(setMobile(device)),
localizeApp: () => dispatch(localizeApp()),
fetchFeaturedHiddenListings: (networkId) => dispatch(fetchFeaturedHiddenListings(networkId))
Expand Down
14 changes: 11 additions & 3 deletions origin-dapp/src/components/listing-create.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'
import Form from 'react-jsonschema-form'

import { showAlert } from 'actions/Alert'
import { handleNotificationsSubscription } from 'actions/App'
import {
update as updateTransaction,
upsert as upsertTransaction
Expand Down Expand Up @@ -274,6 +275,7 @@ class ListingCreate extends Component {
})
this.props.getOgnBalance()
this.setState({ step: this.STEP.SUCCESS })
this.props.handleNotificationsSubscription('seller', this.props)
} catch (error) {
console.error(error)
this.setState({ step: this.STEP.ERROR })
Expand Down Expand Up @@ -985,14 +987,20 @@ class ListingCreate extends Component {
}
}

const mapStateToProps = state => {
const mapStateToProps = ({ app, exchangeRates, wallet }) => {
return {
wallet: state.wallet,
exchangeRates: state.exchangeRates
exchangeRates,
notificationsHardPermission: app.notificationsHardPermission,
notificationsSoftPermission: app.notificationsSoftPermission,
pushNotificationsSupported: app.pushNotificationsSupported,
serviceWorkerRegistration: app.serviceWorkerRegistration,
wallet,
web3Account: app.web3.account
}
}

const mapDispatchToProps = dispatch => ({
handleNotificationsSubscription: (role, props) => dispatch(handleNotificationsSubscription(role, props)),
showAlert: msg => dispatch(showAlert(msg)),
updateTransaction: (hash, confirmationCount) =>
dispatch(updateTransaction(hash, confirmationCount)),
Expand Down
15 changes: 12 additions & 3 deletions origin-dapp/src/components/listing-detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
} from 'react-intl'

import { showAlert } from 'actions/Alert'
import { storeWeb3Intent } from 'actions/App'
import {
handleNotificationsSubscription,
storeWeb3Intent
} from 'actions/App'
import {
update as updateTransaction,
upsert as upsertTransaction
Expand Down Expand Up @@ -142,6 +145,7 @@ class ListingsDetail extends Component {
transactionTypeKey: 'makeOffer'
})
this.setState({ step: this.STEP.PURCHASED })
this.props.handleNotificationsSubscription('buyer', this.props)
} catch (error) {
console.error(error)
this.setState({ step: this.STEP.ERROR })
Expand Down Expand Up @@ -755,15 +759,20 @@ class ListingsDetail extends Component {

const mapStateToProps = ({ app, profile, listings }) => {
return {
featuredListingIds: listings.featured,
notificationsHardPermission: app.notificationsHardPermission,
notificationsSoftPermission: app.notificationsSoftPermission,
profile,
pushNotificationsSupported: app.pushNotificationsSupported,
onMobile: app.onMobile,
serviceWorkerRegistration: app.serviceWorkerRegistration,
web3Account: app.web3.account,
web3Intent: app.web3.intent,
featuredListingIds: listings.featured
web3Intent: app.web3.intent
}
}

const mapDispatchToProps = dispatch => ({
handleNotificationsSubscription: (role, props) => dispatch(handleNotificationsSubscription(role, props)),
showAlert: msg => dispatch(showAlert(msg)),
storeWeb3Intent: intent => dispatch(storeWeb3Intent(intent)),
updateTransaction: (confirmationCount, transactionReceipt) =>
Expand Down
115 changes: 115 additions & 0 deletions origin-dapp/src/components/modals/notifications-modals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react'
import { FormattedMessage } from 'react-intl'

import Modal from 'components/modal'

export const RecommendationModal = ({
isOpen = false,
onCancel,
onSubmit,
role
}) => (
<Modal
className="notifications-modal recommendation"
isOpen={isOpen}
handleToggle={onCancel}
>
<div className="image-container">
<img src="images/notifications-computer.svg" role="presentation" />
</div>
<h2>
<FormattedMessage
id={'notificationsModals.recommendationHeading'}
defaultMessage={'Get Updates'}
/>
</h2>
<div className="text">
<p>
<em>
<FormattedMessage
id={'notificationsModals.questionBuyer'}
defaultMessage={'We highly recommend enabling notifications.'}
/>
</em>
<br />
{role === 'buyer' &&
<FormattedMessage
id={'arbitrationModals.valuePropositionBuyer'}
defaultMessage={`Without them, you will have to return to the Origin marketplace to know if your offer is accepted or when the seller leaves you a review.`}
/>
}
{role === 'seller' &&
<FormattedMessage
id={'notificationsModals.valuePropositionSeller'}
defaultMessage={`Without them, you will have to return to the Origin marketplace to know when a buyer makes an offer on your listing or leaves you a review.`}
/>
}
</p>
</div>
<div className="button-container">
<button className="btn btn-success" onClick={onSubmit}>
<FormattedMessage
id={'notificationsModals.enable'}
defaultMessage={'Enable Notifications'}
/>
</button>
</div>
<a href="#" onClick={onCancel}>
<FormattedMessage
id={'notificationsModals.dismiss'}
defaultMessage={'Dismiss'}
/>
</a>
</Modal>
)

export const WarningModal = ({
isOpen = false,
onCancel,
onSubmit
}) => (
<Modal
className="notifications-modal warning"
isOpen={isOpen}
handleToggle={onCancel}
>
<div className="image-container">
<img src="images/notifications-warning.svg" role="presentation" />
</div>
<h2>
<FormattedMessage
id={'notificationsModals.warningHeading'}
defaultMessage={`Wait! Don't you want updates?`}
/>
</h2>
<div className="text">
<p>
<em>
<FormattedMessage
id={'notificationsModals.disclaimer'}
defaultMessage={'Notifications are critical.'}
/>
</em>
&nbsp;
<FormattedMessage
id={'notificationsModals.explanation'}
defaultMessage={`Because the Origin marketplace is fully-decentralized, you won't receive any emails when there is something important to tell you. You are likely to miss important updates about your transactions.`}
/>
</p>
</div>
<div className="button-container">
<button className="btn btn-success" onClick={onSubmit}>
<FormattedMessage
id={'notificationsModals.enable'}
defaultMessage={'Enable Notifications'}
/>
</button>
</div>
<a href="#" onClick={onCancel}>
<FormattedMessage
id={'notificationsModals.dismiss'}
defaultMessage={'Dismiss'}
/>
</a>
</Modal>
)
Loading

0 comments on commit 5250352

Please sign in to comment.