This library allows you to create a common application using ReactJS and use it inside a BOS Component. Therefore, the BOS Component talks to the React application and vice versa, making it possible to consume Discovery API resources within the React application.
Library intended only for applications that will run within Near Social
Install it using npm or yarn:
# npm
npm install near-social-bridge
# yarn
yarn add near-social-bridge
Check out some examples:
Here's a quick guide to you get to know how to use Near Social Bridge with basic stuff.
- Setup
- BOS API
- Requests
- Persist Storage
- Hooks
- Preparing a new Widget
- Testing the Application Inside the Widget
Here's a complete guide where you can go over all features provided by Near Social Bridge.
- Setup
- BOS API
- Requests
- Mock
- Use Navigation
- Session Storage
- Persist Storage
- Hooks
- Utils
- Preparing a new Widget
- Good to know
- Testing the Application Inside the Widget
You should import the near-social-bridge.css
to your application.
import 'near-social-bridge/near-social-bridge.css'
Then, you need to wrap your app with NearSocialBridgeProvider
which will start the connection between the React App and the Widget inside Near Social. The connection only occurs when the application is running inside the Widget.
This component accepts a fallback component that's going to be shown until the connection with the Widget is established or the Widget response timeout is reached. You can set it using the fallback
prop.
If your app is using (or has dependencies using) localStorage
you'll need to override the window.localStorage
with the Widget's Storage
API as localStorage
is not supported by the VM. You can do it using overrideLocalStorage
like so:
import { overrideLocalStorage } from 'near-social-bridge/utils'
overrideLocalStorage()
When using overrideLocalStorage
, it's recommended that you set NearSocialBridgeProvider.waitForStorage
as true
, so that, the bridge is going to wait for the storage to be hydrated before rendering the children.
import { NearSocialBridgeProvider, Spinner, overrideLocalStorage } from 'near-social-bridge'
import 'near-social-bridge/near-social-bridge.css'
overrideLocalStorage()
const App = () => {
return (
<NearSocialBridgeProvider waitForStorage fallback={<Spinner />}>
<Components />
</NearSocialBridgeProvider>
)
}
We've incorporated some APIs to allow your app to interact with different blockchains, websites, and store data in a decentralized way. These features are basically mirroring the BOS API features.
There is a deployed Hello World
smart contract in the NEAR network at nearsocialexamples.near
that we're going to use. The contract exposes two methods:
set_greeting(message: string): void
, which accepts a message and stores it in the contract state.get_greeting(): string
which returns the stored greeting.
A convenient API to interact with the NEAR blockchain. Complete Docs Here.
Near.view
This will conduct a call to a smart contract that will get a stored message onchain.
import { Near } from 'near-social-bridge/api'
// Contract
const CONTRACT_ID = 'nearsocialexamples.near'
Near.view<string>(CONTRACT_ID, 'get_greeting').then((response) => console.log(response))
// {message: "The most recent stored greeting message"}
Near.call
This will conduct a call to a smart contract that will store a message onchain.
import { Near } from 'near-social-bridge/api'
// Contract
const CONTRACT_ID = 'nearsocialexamples.near'
// Set data
Near.call<{ message: string }>(CONTRACT_ID, 'set_greeting', { message: greeting })
A convenient API to get data from the SocialDB contract. Complete Docs Here.
Social.get
import { Social } from 'near-social-bridge/api'
Social.get('wendersonpires.testnet/widget/*').then((response) => console.log(response))
// {HelloWorld: '...', Chat: '...', ChatV2: '...'}
Social.getr
Social.getr('wendersonpires.testnet/profile').then((response) => console.log(response))
// {name: 'Wenderson Pires'}
Social.index
Social.index('widget-chatv2-dev', 'room', {
limit: 1000,
order: 'desc',
}).then((response) => console.log(response))
// [{accountId: 'xyz', blockHeight: 99, value: 'xyz'}, {...}, {...}, {...}]
Social.set
const data = { experimental: { test: 'test' } }
Social.set(data).then((response) => console.log(response))
// If Success: {wendersonpires.testnet: {experimental: {...}}}
// If Canceled: {error: 'the action was canceled'}
Social.keys
Social.keys('wendersonpires.testnet/experimental').then((response) => console.log(response))
// {wendersonpires.testnet: {experimental: {...}}}
Storage
object to store data for components that is persistent across refreshes. Complete Docs Here.
Storage.set
Storage.set(key, value)
- sets the public value for a given key under the current widget. The value will be public, so other widgets can read it.
import { Storage } from 'near-social-bridge/api'
Storage.set('my-storage-key', JSON.stringify({ age: 33, name: 'Wendz' })).then((response) => console.log(response))
// {ok: true}
Storage.get
Storage.get(key, widgetSrc?)
- returns the public value for a given key under the given widgetSrc or the current component if widgetSrc
is omitted. Can only read public values.
Storage.get('my-storage-key').then((response) => console.log(response))
// {"age":33,"name":"Wendz"}
Storage.privateSet
Storage.privateSet(key, value)
- sets the private value for a given key under the current component. The value is private, only the current component can read it.
Storage.privateSet('my-private-key', JSON.stringify({ age: 18, name: 'Wendz Private' })).then((response) =>
console.log(response)
)
// {ok: true}
Storage.privateGet
Storage.privateGet(key)
- returns the private value for a given key under the current component.
Storage.privateGet('my-private-key').then((response) => console.log(response))
// {"age":18,"name":"Wendz Private"}
You can make requests to the Widget and each request must be handled appropriately by the Widget. You can also mock your requests so that they work locally while you are developing the application. All mocks will be automatically disabled when the app is running inside the Widget.
import { request } from 'near-social-bridge'
// service
const getRoomsList = (payload: { limit: number }) => request('get-rooms-list', payload)
// using the service
getRoomsList({ limit: 2 })
.then((response) => console.log(response))
// Error: when not connected to the widget | has no mock | handler not found inside the Widget
.catch((error) => console.error(error))
This resource is extremely useful, as it allows you to simulate the contract of your application's requests, making it fully work locally. Therefore, you can create the handlers inside the Widget only when you start running the app inside the Widget.
Mock the "get-rooms-list" request:
import { createMock } from 'near-social-bridge'
const rooms = ['room-1', 'room-2', 'room-3', 'room-4']
const getRoomsListMock = (payload: { limit: number }) => {
return {
// always use immutable pattern
roomsList: [...rooms.slice(0, payload.limit)],
}
}
// Create mock
createMock('get-rooms-list', getRoomsListMock)
Then in the app (running locally):
// service
const getRoomsList = (payload: { limit: number }) => request('get-rooms-list', payload)
// using the service
getRoomsList({ limit: 2 }).then((response) => console.log(response)) // ['room-1', 'room-2']
Then, when you test the app inside the widget, the mocks will be automatically disabled. Create a handler for each type of request:
const requestHandler = (request, response) => {
switch (request.type) {
case 'get-rooms-list':
getRoomsListHandler(request, response)
break
}
}
const getRoomsListHandler = (request, response) => {
const { limit } = request.payload;
// ... use Discovery API to fetch rooms list
const rooms = Storage.privateGet("app:rooms-list"),
// ... some logic
response(request).send({ roomsList: rooms.slice(0, limit) });
};
In the Widget side, the handler is going to provide 3 props: request
with its type and payload, response
that is the way the app send a answer back to the React App and utils
that provides some useful features like the promisify
.
The promisify
needs 4 parameters: caller
which is going to request something, resolve
, a method that is going to be called as soon as the caller find an answer, reject
, method that will be called when the service times out and timeout
, a optional parameter where you can set the timeout for this promise. The default timeout is 10 seconds.
So, promisify implementation is
promisify(caller: () => any, resolve: () => void, reject: () => void, timeout: number)
Example of using the promisify feature inside the Widget:
const requestHandler = (request, response, Utils) => {
switch (request.type) {
case 'get-rooms-list':
getRoomsListHandler(request, response, Utils)
break
}
}
const getRoomsListHandler = (request, response, Utils) => {
const { limit } = request.payload
Utils.promisify(
// Cached data (may take a while to return a value)
() => Storage.privateGet('app:rooms-list'),
(rooms) => {
// Send the rooms list
response(request).send({ roomsList: rooms.slice(0, limit) })
}
)
}
Sometimes more than one request can be sent simultaneously. The library automatically controls request concurrency, however, you can force a request to be re-executed until a response is obtained. A maximum of 10 rerun attempts will be made. To use this function, just set forceTryAgain
in the options to true
.
import { request } from 'near-social-bridge'
// service
const options = { forceTryAgain: true }
const getRoomsList = (payload: { limit: number }) => request('get-rooms-list', payload, options)
// using the service
getRoomsList({ limit: 2 }).then((response) => console.log(response))
// Now, if for some reason this call does not get a response, new attempts to request the data will be made.
As said before, the mock features can help you a lot while developing the app locally. Below are some more useful resources.
You can configure how the mock will handle the requests. The only prop available is delay
where you can set the delay before sending an answer for each request (simulates async call).
setupMock({ delay: 1000 })
// From now on, each mock request is going to take 1 second to get an answer.
Use mockUser
to mock authenticated user. You can use createMockUser()
method to provide the user object.
import { createMockUser, mockUser } from 'near-social-bridge'
// You can optionally set default data. All the data is randomly generated.
const fakeUser = createMockUser({ firstName: 'Wenderson' })
mockUser(fakeUser)
// Now your app has an "authenticated" user
Use mockInitialPayload
to mock the initial payload (sent by the Widget).
// Mock
import { mockInitialPayload } from 'near-social-bridge'
mockInitialPayload({
defaultRoom: 'dragon-ball-z',
})
// App
import { useInitialPayload } from 'near-social-bridge/hooks'
const MyComponent = () => {
const { defaultRoom } = useInitialPayload() // 'dragon-ball-z'
}
You can revisit this session here.
This feature was created to facilitate data passing between routes as the main domain will always be https://near.social or another fixed domain. It'll also maintain the same route after a page refresh during the development process. Please note that you will still be able to use any other routing solution.
To force the app to start in a specific route, you should set a path
parameter like so https://near.social/#/wendersonpires.near/widget/MyWidget?path=/profile
where the ?path=
is the param with the route value. E.g: ?path=/timeline
.
You can optionally create properties for each route. It will be useful for you to have a typed navigation
and route
object.
import { IFrameStackScreenProps } from 'near-social-bridge/navigation'
// Navigation props
export type NavigationProps = {
Home: {
title?: string
}
Profile: {
ipfsCidAvatar?: string
userName?: string
}
}
// Screen props
export type PreHomeScreenProps = IFrameStackScreenProps<NavigationProps, 'Home'>
export type ProfileScreenProps = IFrameStackScreenProps<NavigationProps, 'Profile'>
Use the createStackNavigator
method to receive the Navigator
and Screen
components. They will be used to manage each screen.
You can also set a fallback component to show while the connection is being established.
import { createStackNavigator } from 'near-social-bridge/navigation'
import { Spinner } from 'near-social-bridge'
import { NavigationProps } from './NavigationProps'
// Optional Fallback Loading component to show while the connection is being established. Using
// Spinner component provided by the lib
const { Navigator, Screen } = createStackNavigator<NavigationProps>(<Spinner />)
When using Navigator
with autoHeightSync
set as true
, the height of the iframe is automatically adjusted to the initial screen content. If more content is inserted inside the screen after the first render, you can use useSyncContentHeight
hook to sync the height again.
The Screen
component allows you to pass some useful properties, one of them is the iframeHeight
which will set the initial iframe's height needed to show this screen within the Widget even before the first render. If Navigator
was called with autoHeightSync
, the height is going to be adjusted automatically when the screen content is rendered.
return (
<Navigator autoHeightSync>
<Screen name="Home" component={Home} iframeHeight={420} />
<Screen name="Profile" component={Profile} />
</Navigator>
)
You can use the route
property injected into each Screen
to access the passed properties as well as use navigation
to go to another route. It is possible to use the useNavigation
hook to get access to some route resources.
const Profile: React.FC<ProfileScreenProps> = ({ navigation, route }) => {
const { ipfsCidAvatar, userName } = route.params
//...
const goToHomeHandler = () => {
// push new Screen sending data
navigation.push('Home', { title: 'My Nice App!' })
}
}
This feature stores data for components that is persistent across refreshes. You will have access to methods like setItem
, getItem
, removeItem
, clear
and keys
. Data is automatically synchronized between the React App and the BOS Component.
import { sessionStorage } from 'near-social-bridge'
sessionStorage.setItem('name', 'Wenderson')
sessionStorage.getItem('name') // Wenderson
This feature provides automatic Redux state persistence for session. It relies on sessionStorage
.
// Example using redux with @rematch
import { init } from '@rematch/core'
import persistPlugin from '@rematch/persist'
import { persistStorage } from 'near-social-bridge'
import { RootModel } from './models'
type FullModel = ExtraModelsFromLoading<RootModel, { type: 'full' }>
export const store = init<RootModel>({
models,
plugins: [
// Provides automatic Redux state persistence.
// https://rematchjs.org/docs/plugins/persist/
persistPlugin({
key: 'root',
storage: persistStorage,
}),
],
})
Allow to get message from BOS Component and send messages to BOS Component:
import { useNearSocialBridge } from 'near-social-bridge'
const MyComponent = () => {
const { onGetMessage, postMessage } = useNearSocialBridge()
useEffect(() => {
// Receives a message from BOS Component
onGetMessage((message: any) => {
console.log('Got message from BOS Component:', message)
})
return () => onGetMessage(null)
}, [])
const sendMessageToWidget = () => {
// Sends a message to BOS Component
postMessage('My awesome message! :D')
}
// ...
}
Returns the initial payload sent by the BOS Component:
import { useInitialPayload } from 'near-social-bridge'
const MyComponent = () => {
const initialPayload = useInitialPayload()
console.log(initialPayload) // initial payload sent by the BOS Component
// ...
}
Expose features like push
new route, goBack
to the previous route, location
with the current route location and props, history
with the history of all routes visited and their props:
import { useNavigation } from 'near-social-bridge'
const MyComponent = () => {
const navigation = useNavigation()
// ...
navigation.push('ProfileScreen')
// ...
}
Returns storage with the most updated items:
import { useSessionStorage } from 'near-social-bridge'
// Set item
sessionStorage.setItem('age', 32)
// Component
const MyComponent = () => {
const storage = useSessionStorage()
console.log(storage?.age) // 32
}
Returns the authenticated user info:
import { useAuth } from 'near-social-bridge'
const MyComponent = () => {
const auth = useAuth()
console.log(auth.ready) // true or false (it's true when the request to get the user info is completed)
console.log(auth.user?.accountId) // E.g: wendersonpires.near
}
Provides access to methods and props which can affect the BOS Component View:
import { useWidgetView } from 'near-social-bridge'
const MyComponent = () => {
const widgetView = useWidgetView()
// Set the BOS Component view height to 700px
widgetView.setHeight(700)
}
You can use this hook to do a content height sync. Thus, the height of the viewer's iframe will always have the updated height.
import { useSyncContentHeight } from 'near-social-bridge'
const MyComponent = () => {
const { done, syncAgain } = useSyncContentHeight()
console.log('is sync done?', done)
const [list, setList] = useState(['a'])
useEffect(() => {
setList(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'])
syncAgain()
}, [])
return (
<div className="flex flex-col">
<p>list</p>
{list.map((item) => (
<p key={item}>{item}</p>
))}
</div>
)
}
Or, you can just use useSyncContentHeight()
:
import { useSyncContentHeight } from 'near-social-bridge'
const MyComponent = () => {
useSyncContentHeight()
const list = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']
return (
<div className="flex flex-col">
<p>list</p>
{list.map((item) => (
<p key={item}>{item}</p>
))}
</div>
)
}
If you use this service during the development process, when you press "R" key 3 times, the app is going to refresh.
import { initRefreshService } from 'near-social-bridge/utils'
// ...
useEffect(() => {
if (isDev) {
initRefreshService()
}
}, [])
// ...
This is a feature that overrides the window.localStorage
with the BOS Component's Storage
, so that, you can keep using window.localStorage
but the BOS Component's Storage
is going to be the source of data.
If using CSR:
import { overrideLocalStorage } from 'near-social-bridge/utils'
// using `sessionStorage` under the hood
overrideLocalStorage()
// The BOS Component won't break
localStorage.setItem('name', 'Wenderson')
localStorage.getItem('name') // "Wenderson"
If using SSR:
// Page or index.tsx
import { useEffect } from 'react'
import { NearSocialBridgeProvider, overrideLocalStorage } from 'near-social-bridge'
import MyComponent from './MyComponent'
import MyComponent2 from './MyComponent2'
overrideLocalStorage()
const SSRApp = () => {
useEffect(() => {
localStorage.setItem('name', 'Wenderson')
}, [])
return (
<NearSocialBridgeProvider waitForStorage>
<MyComponent />
<MyComponent2 />
</NearSocialBridgeProvider>
)
}
export default SSRApp
// MyComponent
const MyComponent = () => {
console.log(localStorage.getItem('name')) // "Wenderson"
}
// MyComponent2
import { sessionStorage } from 'near-social-bridge'
const MyComponent2 = () => {
console.log(sessionStorage.getItem('name')) // "Wenderson"
}
Create a new BOS Component, copy the content of file widget-setup.js and paste it inside your new BOS Component. Then set its initial props as you wish:
/**
* External App URL (must)
*/
const externalAppUrl = 'https://<external-app-link-here>'
/**
* Initial Path (optional)
*/
const path = props.path
/**
* Initial view height (optional)
*/
const initialViewHeight = 500
/**
* Initial Payload (optional)
*/
const initialPayload = {}
/**
* Request Handlers here
*/
const requestHandler = (request, response) => {
switch (request.type) {
case 'get-account-id':
getAccountIdHandler(request, response)
break
}
}
const getAccountIdHandler = (request, response) => {
// You have access to the request payload
console.log(request.payload) // Any data sent by React App
const accountId = context.accountId
// Send a response to the React App
// "response" needs the "request" object to know the type of the request
response(request).send({ accountId })
}
// NearSocialBridgeCore BOS Component is the core that makes all the "magic" happens
// use `wendersonpires.testnet/widget/NearSocialBridgeCore` as source if you want to use "testnet" environment
return (
<Widget
src={'wendersonpires.near/widget/NearSocialBridgeCore'}
props={{
externalAppUrl,
path,
initialViewHeight,
initialPayload,
requestHandler,
}}
/>
)
testnet: Use wendersonpires.testnet/widget/NearSocialBridgeCore
while creating your application using the testnet environment.
And that's basically it. Again, remember that once your application is running inside the BOS Component, if it is making requests, you must handle each one of them inside the BOS Component, otherwise the unhandled requests will fail.
SSR is supported starting with version 1.3.0!
To test your app, you can install the NEAR Social Local Viewer CLI. It will allow you to execute and test your BOS Component locally using all the Discovery API resources without any problem.
1 - Install NEAR Social Local Viewer CLI using npm or yarn:
# npm
npm install near-social-local-viewer --save-dev
# yarn
yarn add near-social-local-viewer -D
2 - Now you can create a script within package.json
file:
{
"scripts": {
"start:widget": "npx init-viewer path/to/widgets/"
}
}
3 - or just run:
npx init-viewer path/to/widgets/
# e.g: npx init-viewer widgets/
4 - Once your BOS Component is ready, you can deploy it to Near Social:
4.1 - You can deploy it by copying and pasting;
4.2 - Or using near-social CLI.