This starter-kit is meant to showcase developers on how to use the PUSH SDK packages -
-
@pushprotocol/restapi Provides access to PUSH backend APIs.
-
@pushprotocol/uiweb Provides React based components to show Notifications, Spam, SubscribedModal etc for dApps.
-
@pushprotocol/uiembed Provides vanilla JS sidebar notifications for any dApp.
-
@pushprotocol/socket Provides a socket client to connect with Push Websockets
This particular kit is built out using CRA, Typescript. The SDK packages should also work out for React using plain JS.
git clone https://github.com/ethereum-push-notification-service/push-sdk-starter-kit.git
cd push-sdk-starter-kit
yarn install
yarn start
If your are trying to build out a separate dApp following this starter-kit example, some of the following dependencies are needed for the SDK and any dApp to work.
@pushprotocol/uiweb
has apeerDependency
onstyled-components
yarn add styled-components
- Since its a dApp, the following are the web3 dependencies that you can install for wallet connection Latest version of Ethers (v6) introduces some breaking changes, for best results use Ethers v5 (ethers@^5.6)
yarn add [email protected]
- Needed only if you are using web3-react. You are free to use any other React based web3 solution.
yarn add @web3-react/core @web3-react/injected-connector
But no need to install these if you are using the starter-kit
itself since we have already installed these for you so that you can focus on how to use the PUSH-SDK packages
The App has following features-
Page | Features | SDK package used |
---|---|---|
Notifications | notifications, spams, subscribed modal |
@pushprotocol/uiweb, @pushprotocol/restapi |
Channels | get channel details for a specific channel, search for channel(s), get channel subscribers, is the logged-in user subscribed to the channel, opt in a channel, opt out a channel |
@pushprotocol/restapi |
Payloads | send notification for different use cases | @pushprotocol/restapi |
Embed | sidebar notifications for the logged in user if subscribed on PUSH protocol | @pushprotocol/uiembed |
We have extracted some snippets from the actual source code of the starter-kit
files mentioned below to give you a snapshot view of what all SDK features are used in this dApp. But to make sure you are following along correctly please refer to the source code itself in the files mentioned.
Also the detailed SDK docs are hyperlinked in the feature's header itself
If you have got the wallet connection logic down, you can start referring from section 3 onwards.
Any dApp will require a wallet connection logic before it is usable. We have handled that for you in App.tsx
. If you want to tinker around with that, check the below component.
import ConnectButton from './components/connect';
We basically derive the account, signer and some other wallet connection properties to use throughout the dApp with the SDK.
const { chainId, account, active, error, library } = useWeb3React();
We store this data in the web3Context and make it available across the dApp for later use.
NOTIFICATIONS PAGE (src/pages/notifications/index.tsx
)
import * as PushAPI from "@pushprotocol/restapi";
import { NotificationItem, chainNameType, SubscribedModal } from '@pushprotocol/uiweb';
const notifications = await PushAPI.user.getFeeds({
user: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // user address in CAIP
env: 'staging'
});
{notifications.map((oneNotification, i) => {
const {
cta,
title,
message,
app,
icon,
image,
url,
blockchain,
secret,
notification
} = oneNotification;
return (
<NotificationItem
key={`notif-${i}`}
notificationTitle={secret ? notification['title'] : title}
notificationBody={secret ? notification['body'] : message}
cta={cta}
app={app}
icon={icon}
image={image}
url={url}
theme={theme}
chainName={blockchain as chainNameType}
/>
);
})}
const spams = await PushAPI.user.getFeeds({
user: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // user address in CAIP
spam: true,
env: 'staging'
});
{spams ? (
<NotificationListContainer>
{spams.map((oneNotification, i) => {
const {
cta,
title,
message,
app,
icon,
image,
url,
blockchain,
secret,
notification
} = oneNotification;
return (
<NotificationItem
key={`spam-${i}`}
notificationTitle={secret ? notification['title'] : title}
notificationBody={secret ? notification['body'] : message}
cta={cta}
app={app}
icon={icon}
image={image}
url={url}
theme={theme}
chainName={blockchain as chainNameType}
// optional parameters for rendering spambox
isSpam
subscribeFn={subscribeFn} // see below
isSubscribedFn={isSubscribedFn} // see below
/>
);
})}
const subscribeFn = async () => {
// opt in to the spam notification item channel
}
we can use this @pushprotocol/restapi
method to do that - subscribe
const isSubscribedFn = async () => {
// return boolean which says whether the channel for the
// spam notification item is subscribed or not by the user.
}
we can use this @pushprotocol/restapi
method to find out that - getSubscriptions
Parsing raw Feeds API data using utils method parseApiResponse
Utils method to parse raw Push Feeds API response into a pre-defined shape as below.
// fetch some raw feeds data
const apiResponse = await PushAPI.user.getFeeds({
user: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // user address
raw: true,
env: 'staging'
});
// parse it to get a specific shape of object.
const parsedResults = PushAPI.utils.parseApiResponse(apiResponse);
const [oneNotification] = parsedResults;
// Now this object can be directly used by for e.g. "@pushprotocol/uiweb" NotificationItem component as props.
const {
cta,
title,
message,
app,
icon,
image,
url,
blockchain,
secret,
notification
} = oneNotification;
We get the above keys
after the parsing of the API repsonse.
const [showSubscribe, setShowSubscribe] = useState(false);
const toggleSubscribedModal = () => {
setShowSubscribe((lastVal) => !lastVal);
};
// JSX
{showSubscribe ? <SubscribedModal onClose={toggleSubscribedModal}/> : null}
CHANNELS PAGE (src/pages/channels/index.tsx
)
import * as PushAPI from '@pushprotocol/restapi';
const channelData = await PushAPI.channels.getChannel({
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // channel address in CAIP
env: 'staging'
});
const channelsData = await PushAPI.channels.search({
query: 'push', // a search query
page: 1, // page index
limit: 20, // no of items per page
env: 'staging'
});
const subscribers = await PushAPI.channels._getSubscribers({
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // channel address in CAIP
env: 'staging'
});
const subscriptions = await PushAPI.user.getSubscriptions({
user: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // user address in CAIP
env: 'staging'
});
const _signer = library.getSigner(account); // from useWeb3()
//
//
//
await PushAPI.channels.subscribe({
signer: _signer,
channelAddress: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // channel address in CAIP
userAddress: 'eip155:42:0x52f856A160733A860ae7DC98DC71061bE33A28b3', // user address in CAIP
onSuccess: () => {
console.log('opt in success');
},
onError: () => {
console.error('opt in error');
},
env: 'staging'
})
const _signer = library.getSigner(account); // from useWeb3()
//
//
//
await PushAPI.channels.unsubscribe({
signer: _signer,
channelAddress: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // channel address in CAIP
userAddress: 'eip155:42:0x52f856A160733A860ae7DC98DC71061bE33A28b3', // user address in CAIP
onSuccess: () => {
console.log('opt out success');
},
onError: () => {
console.error('opt out error');
},
env: 'staging'
})
PAYLOADS PAGE (src/pages/payloads/index.tsx
)
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 3, // target
identityType: 2, // direct payload
notification: {
title: `[SDK-TEST] notification TITLE:`,
body: `[sdk-test] notification BODY`
},
payload: {
title: `[sdk-test] payload title`,
body: `sample msg body`,
cta: '',
img: ''
},
recipients: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // recipient address
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 4, // subset
identityType: 2, // direct payload
notification: {
title: `[SDK-TEST] notification TITLE:`,
body: `[sdk-test] notification BODY`
},
payload: {
title: `[sdk-test] payload title`,
body: `sample msg body`,
cta: '',
img: ''
},
recipients: ['eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', 'eip155:42:0xCdBE6D076e05c5875D90fa35cc85694E1EAFBBd1'], // recipients addresses
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 1, // broadcast
identityType: 2, // direct payload
notification: {
title: `[SDK-TEST] notification TITLE:`,
body: `[sdk-test] notification BODY`
},
payload: {
title: `[sdk-test] payload title`,
body: `sample msg body`,
cta: '',
img: ''
},
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 3, // target
identityType: 1, // ipfs payload
ipfsHash: 'bafkreicuttr5gpbyzyn6cyapxctlr7dk2g6fnydqxy6lps424mcjcn73we', // IPFS hash of the payload
recipients: 'eip155:42:0xCdBE6D076e05c5875D90fa35cc85694E1EAFBBd1', // recipient address
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 4, // subset
identityType: 1, // ipfs payload
ipfsHash: 'bafkreicuttr5gpbyzyn6cyapxctlr7dk2g6fnydqxy6lps424mcjcn73we', // IPFS hash of the payload
recipients: ['eip155:42:0xCdBE6D076e05c5875D90fa35cc85694E1EAFBBd1', 'eip155:42:0x52f856A160733A860ae7DC98DC71061bE33A28b3'], // recipients addresses
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 1, // broadcast
identityType: 1, // direct payload
ipfsHash: 'bafkreicuttr5gpbyzyn6cyapxctlr7dk2g6fnydqxy6lps424mcjcn73we', // IPFS hash of the payload
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 3, // target
identityType: 0, // Minimal payload
notification: {
title: `[SDK-TEST] notification TITLE:`,
body: `[sdk-test] notification BODY`
},
payload: {
title: `[sdk-test] payload title`,
body: `sample msg body`,
cta: '',
img: ''
},
recipients: 'eip155:42:0xCdBE6D076e05c5875D90fa35cc85694E1EAFBBd1', // recipient address
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 4, // subset
identityType: 0, // Minimal payload
notification: {
title: `[SDK-TEST] notification TITLE:`,
body: `[sdk-test] notification BODY`
},
payload: {
title: `[sdk-test] payload title`,
body: `sample msg body`,
cta: '',
img: ''
},
recipients: ['eip155:42:0xCdBE6D076e05c5875D90fa35cc85694E1EAFBBd1', 'eip155:42:0x52f856A160733A860ae7DC98DC71061bE33A28b3'], // recipients address
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 1, // broadcast
identityType: 0, // Minimal payload
notification: {
title: `[SDK-TEST] notification TITLE:`,
body: `[sdk-test] notification BODY`
},
payload: {
title: `[sdk-test] payload title`,
body: `sample msg body`,
cta: '',
img: ''
},
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
Make sure the channel has the graph id you are providing!!
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 3, // target
identityType: 3, // Subgraph payload
graph: {
id: '_your_graph_id',
counter: 3
},
recipients: 'eip155:42:0xCdBE6D076e05c5875D90fa35cc85694E1EAFBBd1', // recipient address
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
Make sure the channel has the graph id you are providing!!
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 4, // subset
identityType: 3, // graph payload
graph: {
id: '_your_graph_id',
counter: 3
},
recipients: ['eip155:42:0xCdBE6D076e05c5875D90fa35cc85694E1EAFBBd1', 'eip155:42:0x52f856A160733A860ae7DC98DC71061bE33A28b3'], // recipients addresses
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
Make sure the channel has the graph id you are providing!!
// apiResponse?.status === 204, if sent successfully!
const apiResponse = await PushAPI.payloads.sendNotification({
signer,
type: 1, // broadcast
identityType: 3, // graph payload
graph: {
id: '_your_graph_id',
counter: 3
},
channel: 'eip155:42:0xD8634C39BBFd4033c0d3289C4515275102423681', // your channel address
env: 'staging'
});
EMBED PAGE (src/pages/embed/index.tsx
)
import { useEffect, useContext } from 'react';
import { EmbedSDK } from "@pushprotocol/uiembed";
import Web3Context from '../../context/web3Context';
const EmbedPage = () => {
const { account, chainId } = useContext<any>(Web3Context);
useEffect(() => {
if (account) { // 'your connected wallet address'
EmbedSDK.init({
chainId,
headerText: 'Hello Hacker Dashboard', // optional
targetID: 'sdk-trigger-id', // mandatory
appName: 'hackerApp', // mandatory
user: account, // mandatory
viewOptions: {
type: 'sidebar', // optional [default: 'sidebar', 'modal']
showUnreadIndicator: true, // optional
unreadIndicatorColor: '#cc1919',
unreadIndicatorPosition: 'top-right',
},
theme: 'light',
onOpen: () => {
console.log('-> client dApp onOpen callback');
},
onClose: () => {
console.log('-> client dApp onClose callback');
}
});
}
return () => {
EmbedSDK.cleanup();
};
}, [account, chainId]);
return (
<div>
<h2>Embed Test page</h2>
<button id="sdk-trigger-id">trigger button</button>
</div>
);
}
SOCKET PAGE (src/pages/socket/index.tsx
, src/hooks/useSDKSocket.ts
)
Use this hook in your app to connect to the feeds from the Push Websockets.
export const useSDKSocket = ({ account, env = '', chainId, isCAIP }: SDKSocketHookOptions) => {
const [pushSDKSocket, setPushSDKSocket] = useState<any>(null);
const [feedsSinceLastConnection, setFeedsSinceLastConnection] = useState<any>([]);
const [isSDKSocketConnected, setIsSDKSocketConnected] = useState(pushSDKSocket?.connected);
const [lastConnectionTimestamp, setLastConnectionTimestamp] = useState('');
const addSocketEvents = () => {
// console.warn('\n--> addSocketEvents');
pushSDKSocket?.on(EVENTS.CONNECT, () => {
// console.log('CONNECTED: ');
setIsSDKSocketConnected(true);
setLastConnectionTimestamp((new Date()).toUTCString());
});
pushSDKSocket?.on(EVENTS.DISCONNECT, () => {
// console.log('DIS-CONNECTED: ');
setIsSDKSocketConnected(false);
setFeedsSinceLastConnection([]);
setLastConnectionTimestamp('');
});
// console.log('\t-->will attach eachFeed event now');
pushSDKSocket?.on(EVENTS.USER_FEEDS, (feedList: any) => {
/**
* We receive a feed list which has 1 item.
*/
console.log("\n\n\n\neachFeed event: ", feedList);
// do stuff with data
setFeedsSinceLastConnection((oldFeeds: any) => {
return [...oldFeeds, ...feedList]
});
});
};
const removeSocketEvents = () => {
// console.warn('\n--> removeSocketEvents');
pushSDKSocket?.off(EVENTS.CONNECT);
pushSDKSocket?.off(EVENTS.DISCONNECT);
pushSDKSocket?.off(EVENTS.USER_FEEDS);
};
useEffect(() => {
if (pushSDKSocket) {
addSocketEvents();
}
return () => {
if (pushSDKSocket) {
removeSocketEvents();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pushSDKSocket]);
/**
* Whenever the requisite params to create a connection object change
* - disconnect the old connection
* - create a new connection object
*/
useEffect(() => {
if (account) {
if (pushSDKSocket) {
// console.log('=================>>> disconnection in the hook');
pushSDKSocket?.disconnect();
}
const connectionObject = createSocketConnection({
user: account,
env,
socketOptions: { autoConnect: false }
});
// console.warn('new connection object: ', connectionObject);
// set to context
setPushSDKSocket(connectionObject);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [account, env, chainId, isCAIP]);
return {
pushSDKSocket,
isSDKSocketConnected,
feedsSinceLastConnection,
lastConnectionTimestamp
}
};