From ccaedace81c842bb10158e97331a73d0f86cbf7e Mon Sep 17 00:00:00 2001 From: Wenderson Pires Date: Fri, 21 Apr 2023 19:18:39 -0300 Subject: [PATCH] added `overrideLocalStorage`; now it is possible to wait for storage to be hydrated before rendering the children --- README.md | 74 ++++++++++++++++++- package.json | 2 +- .../contexts/NearSocialBridgeProvider.tsx | 40 +++++++++- src/index.ts | 2 +- src/session-storage/sessionStorage.ts | 18 ++++- src/utils/index.ts | 3 +- src/utils/isBrowser.ts | 4 + src/utils/overrideLocalStorage.ts | 31 ++++++++ tsconfig.json | 5 +- 9 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 src/utils/overrideLocalStorage.ts diff --git a/README.md b/README.md index 6db355c..72189a5 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ yarn add near-social-bridge - [useSyncContentHeight](#usesynccontentheight) - [Utils](#utils) - [initRefreshService](#initrefreshservice) + - [overrideLocalStorage](#overridelocalstorage) - [Preparing a new Widget](#preparing-a-new-widget) - [Good to know](#good-to-know) - [Server-Side Rendering](#server-side-rendering) @@ -68,14 +69,26 @@ 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. +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: ```tsx -import { NearSocialBridgeProvider } from 'near-social-bridge' -import { Spinner } from 'near-social-bridge' +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. + +```tsx +import { NearSocialBridgeProvider, Spinner, overrideLocalStorage } from 'near-social-bridge' +import 'near-social-bridge/near-social-bridge.css' + +overrideLocalStorage() + // ... return ( - }> + }> ) @@ -536,6 +549,59 @@ useEffect(() => { // ... ``` +### overrideLocalStorage + +This is a feature that overrides the `window.localStorage` with the Widget's `Storage`, so that, you can keep using `window.localStorage` but the Widget's `Storage` is going to be the source of data. + +**If using CSR:** + +```ts +import { overrideLocalStorage } from 'near-social-bridge/utils' + +// using `sessionStorage` under the hood +overrideLocalStorage() + +localStorage.setItem('name', 'Wenderson') +localStorage.getItem('name') // "Wenderson" +``` + +**If using SSR:** + +```ts +// Page or index.tsx +import { useEffect } from 'react' +import { NearSocialBridgeProvider, overrideLocalStorage } from 'near-social-bridge' +import MyComponent from './MyComponent' + +overrideLocalStorage() + +const SSRApp = () => { + useEffect(() => { + localStorage.setItem('name', 'Wenderson') + }, []) + + return ( + + + + + ) +} + +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" +} +``` + ## Preparing a new Widget Create a new Widget, copy [the content of file **widget-setup.js**](./widget-setup.js) and paste it inside your new Widget. Then set its initial props as you wish: diff --git a/package.json b/package.json index ec0e46a..e490e34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "near-social-bridge", - "version": "1.3.6", + "version": "1.4.0", "description": "This library allows you to create a common application using ReactJS and inject it in a controlled way into a Widget on Near Social. Therefore, the Widget talks to the React application and vice versa, making it possible to consume Discovery API resources within the React application.", "main": "./dist/cjs/index.js", "module": "./index.js", diff --git a/src/bridge/contexts/NearSocialBridgeProvider.tsx b/src/bridge/contexts/NearSocialBridgeProvider.tsx index cdf0815..96c0dde 100644 --- a/src/bridge/contexts/NearSocialBridgeProvider.tsx +++ b/src/bridge/contexts/NearSocialBridgeProvider.tsx @@ -9,6 +9,7 @@ import { postMessage as postMessageService, } from '../../services/bridge-service' import { GetMessageCallBack, NearSocialBridgeProps } from '../types' +import { sessionStorageUpdateObservable } from '../../session-storage/sessionStorage' const defaultValue: NearSocialBridgeProps = { postMessage: () => { @@ -26,7 +27,14 @@ export const NearSocialBridgeContext = createContext(defaultValue) interface Props { children: React.ReactNode + /** + * Fallback component that's going to be shown until the connection with the Widget is established or the Widget response timeout is reached. + */ fallback?: React.ReactNode + /** + * Wait for storage to be hydrated before render the children. Fallback component is going to be shown if provided. + */ + waitForStorage?: boolean } /** @@ -35,11 +43,12 @@ interface Props { * Fallback component is displayed (if provided) until the connection is established with the Widget * @returns */ -const NearSocialBridgeProvider: React.FC = ({ children, fallback }) => { +const NearSocialBridgeProvider: React.FC = ({ children, fallback, waitForStorage }) => { const [_onGetMessage, set_onGetMessage] = useState<{ cb: GetMessageCallBack }>({ cb: () => {} }) const [isConnected, setIsConnected] = useState(false) + const [isStorageReady, setIsStorageReady] = useState(false) /** * Post Message @@ -93,8 +102,33 @@ const NearSocialBridgeProvider: React.FC = ({ children, fallback }) => { } }, []) - if (!isConnected && fallback) { - return <>{fallback} + // Check if storage is ready + if (waitForStorage) { + const handler = () => { + sessionStorageUpdateObservable.unsubscribe(handler) + setIsStorageReady(true) + } + sessionStorageUpdateObservable.subscribe(handler) + } + + // Wait connection + if (!isConnected) { + if (fallback) { + return <>{fallback} + } + + // SSR - It's necessary. So that, `overrideLocalStorage`, it's going to work without issue + return
+ } + + // Wait storage to be ready (optional) + if (waitForStorage && !isStorageReady) { + if (fallback) { + return <>{fallback} + } + + // SSR - It's necessary. So that, `overrideLocalStorage`, it's going to work without issue + return
} return ( diff --git a/src/index.ts b/src/index.ts index 2c44516..3a3e4c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,4 +12,4 @@ export * from './hooks' export * from './components' // Utils -export { initRefreshService } from './utils/refresh' +export * from './utils' diff --git a/src/session-storage/sessionStorage.ts b/src/session-storage/sessionStorage.ts index 827fef1..66ceee6 100644 --- a/src/session-storage/sessionStorage.ts +++ b/src/session-storage/sessionStorage.ts @@ -1,6 +1,6 @@ import { REQUEST_KEYS } from '../constants' import request from '../request/request' -import { onConnectObservable } from '../services/bridge-service' +import { getConnectionStatus, onConnectObservable } from '../services/bridge-service' import Observable from '../utils/observable' /** @@ -14,7 +14,11 @@ const setItem = (key: string, value: any) => { const updatedStorage: any = { ..._storage } updatedStorage[key] = value _storage = updatedStorage - hydrateViewer() + + // Hydrate only when connection is established + if (getConnectionStatus() === 'connected') { + hydrateViewer() + } } const getItem = (key: string) => _storage[key] || null @@ -55,9 +59,17 @@ const hydrate = async () => { forceTryAgain: false, }) + // Check if there're data inside the _storage before the connection, if so, update the viewer + const hasPreviousData = Object.keys(_storage).length > 0 + // Hydrate _storage with data stored in the Viewer "sessionStorageClone" state - _storage = view_sessionStorageClone || {} + _storage = { ...view_sessionStorageClone, ..._storage } || {} sessionStorageUpdateObservable.notify(_storage) + + // Update the viewer (only if there're data inside the _storage before the connection) + if (hasPreviousData) { + hydrateViewer() + } } // Every time the bridge connection is established, hydrate the storage diff --git a/src/utils/index.ts b/src/utils/index.ts index 7abdf7a..291b9f4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ import { initRefreshService } from './refresh' +import { overrideLocalStorage } from './overrideLocalStorage' -export { initRefreshService } +export { initRefreshService, overrideLocalStorage } diff --git a/src/utils/isBrowser.ts b/src/utils/isBrowser.ts index 696ce25..e41d782 100644 --- a/src/utils/isBrowser.ts +++ b/src/utils/isBrowser.ts @@ -1,2 +1,6 @@ +/** + * Is it running in a browser? + * @returns + */ const isBrowser = () => typeof window !== 'undefined' export default isBrowser diff --git a/src/utils/overrideLocalStorage.ts b/src/utils/overrideLocalStorage.ts new file mode 100644 index 0000000..5c8e663 --- /dev/null +++ b/src/utils/overrideLocalStorage.ts @@ -0,0 +1,31 @@ +import sessionStorage from '../session-storage/sessionStorage' +import isBrowser from './isBrowser' + +/** + * Overrides the window.localStorage with the bridge's sessionStorage (Widget's Storage) + */ +export const overrideLocalStorage = () => { + class BridgeLocalStorage { + setItem(key: string, value: any) { + sessionStorage.setItem(key, value) + } + + getItem(key: string) { + return sessionStorage.getItem(key) + } + + removeItem(key: string) { + sessionStorage.removeItem(key) + } + } + + const myLocalStorage = new BridgeLocalStorage() + + // Assign the newly created instance to localStorage + if (isBrowser()) { + Object.defineProperty(window, 'localStorage', { + value: myLocalStorage, + writable: true, + }) + } +} diff --git a/tsconfig.json b/tsconfig.json index f3c7ea5..8fc4e78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,9 +20,6 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "@lib/*": ["./*"] - } + "baseUrl": "." } }