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

Push Notifications (Chrome & Firefox) #795

Merged
merged 9 commits into from
Oct 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(role) {
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