diff --git a/src/components/Home/Home.tsx b/src/components/Home/Home.tsx index 805d1b2f..4d5f3ab0 100644 --- a/src/components/Home/Home.tsx +++ b/src/components/Home/Home.tsx @@ -1,11 +1,13 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import styled from '@emotion/styled'; +import { ipcRenderer } from 'electron'; +import { dispatch } from 'store/rematch'; import { selectModalName, selectView } from 'store/selectors'; import links from 'util/links'; -import { TOPBAR_HEIGHT_PX, ViewType } from 'vars/defines'; +import { DEEP_LINK_IPC_ID, TOPBAR_HEIGHT_PX, ViewType } from 'vars/defines'; import InfoNote from 'components/_General/InfoNote'; import CreateToken from 'components/CreateToken/CreateToken'; @@ -68,6 +70,19 @@ const Home = () => { const currentView = useSelector(selectView); const modalProps = modals[useSelector(selectModalName)]; + useEffect(() => { + const listener = (_, { view, params }) => { + dispatch.environment.SET_VIEW(view || ViewType.DASHBOARD); + if (params) dispatch.environment.SET_DEEP_LINK_PARAMS(params); + }; + + ipcRenderer.on(DEEP_LINK_IPC_ID, listener); + + return () => { + ipcRenderer.removeListener(DEEP_LINK_IPC_ID, listener); + }; + }, []); + return ( diff --git a/src/components/Marketplace/Marketplace.tsx b/src/components/Marketplace/Marketplace.tsx index 62b82d45..075d3da4 100644 --- a/src/components/Marketplace/Marketplace.tsx +++ b/src/components/Marketplace/Marketplace.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; @@ -9,6 +10,8 @@ import fillOrderIcon from 'assets/fillOrder.svg'; import inboxIcon from 'assets/inbox.svg'; import listIcon from 'assets/list.svg'; import trendUpIcon from 'assets/trendUp.svg'; +import { dispatch } from 'store/rematch'; +import { selectDeepLinkParams } from 'store/selectors'; import { V } from 'util/theming'; import { Layout, SubTitle, Title } from 'components/_General/_UIElements/common'; @@ -68,9 +71,28 @@ const WelcomeMessageWrapper = styled.div` `; const Marketplace: React.FC = () => { + const deepLinkParams = useSelector(selectDeepLinkParams); const [currentView, setCurrentView] = useState(null); const [currentOrderId, setCurrentOrderId] = useState(); + const handleViewChange = (view: MARKETPLACE_VIEWS | null) => { + setCurrentView(view); + setCurrentOrderId(undefined); + }; + + useEffect(() => { + if (deepLinkParams?.length) { + const params = new URLSearchParams(deepLinkParams); + const action = + params.get('action') === 'bid' ? MARKETPLACE_VIEWS.BID : MARKETPLACE_VIEWS.FILL; + + setCurrentView(action); + setCurrentOrderId(params.get('orderid') || params.get('tokenid')); + + dispatch.environment.SET_DEEP_LINK_PARAMS(''); + } + }, [deepLinkParams, currentView]); + const CurrentTab = () => { switch (currentView) { case MARKETPLACE_VIEWS.FILL: @@ -123,7 +145,7 @@ const Marketplace: React.FC = () => { {menuData.map(menuItem => ( setCurrentView(menuItem.type)} + onClick={() => handleViewChange(menuItem.type)} name={menuItem.name} icon={menuItem.icon} selected={menuItem.type === currentView} diff --git a/src/components/Marketplace/widgets/MarketOrder.tsx b/src/components/Marketplace/widgets/MarketOrder.tsx index 93760e5c..2bf8b435 100644 --- a/src/components/Marketplace/widgets/MarketOrder.tsx +++ b/src/components/Marketplace/widgets/MarketOrder.tsx @@ -46,8 +46,7 @@ const MarketOrderWidget: React.FC = ({ type }) => { const tokenDetails = useSelector(selectTokenDetails); const myTokens = useMyTokens(); const fulfillOrderSchema = useFulfillOrderSchema(type); - const { currentOrderId: prefillOrderId, setCurrentOrderId: setPrefillOrderId } = - useContext(ViewContext); + const { currentOrderId: prefillOrderId } = useContext(ViewContext); const handleMarketOrder = (values: MarketOrder, { setSubmitting }) => { setSubmitting(false); @@ -101,13 +100,11 @@ const MarketOrderWidget: React.FC = ({ type }) => { : 'Review order'; useEffect(() => { - if (prefillOrderId) formikBag.setFieldValue('orderId', prefillOrderId); + if (prefillOrderId?.length) + formikBag.setFieldValue(type === 'fill' ? 'orderId' : 'assetId', prefillOrderId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [prefillOrderId, formikBag.setFieldValue]); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => () => setPrefillOrderId(undefined), []); - useEffect(() => { if (currentOrderDetails) { const quantity = parseBigNumObject(currentOrderDetails.bnAmount).toNumber(); @@ -190,7 +187,6 @@ const MarketOrderWidget: React.FC = ({ type }) => { formikBag.setFieldValue('order', {}); formikBag.setFieldValue('quantity', 0); formikBag.setFieldValue('price', 0); - formikBag.setFieldValue('assetId', ''); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [formikBag.setFieldValue, formikBag.values.orderId]); diff --git a/src/components/_General/TokenMediaDisplay.tsx b/src/components/_General/TokenMediaDisplay.tsx index 252c3925..a7911230 100644 --- a/src/components/_General/TokenMediaDisplay.tsx +++ b/src/components/_General/TokenMediaDisplay.tsx @@ -5,7 +5,15 @@ import { ipcRenderer } from 'electron'; import { Responsive, extractIPFSHash } from 'util/helpers'; import { V } from 'util/theming'; -import { DEFAULT_IPFS_FALLBACK_GATEWAY, IPFS_IPC_ID, IpfsAction } from 'vars/defines'; +import { + DEFAULT_IPFS_FALLBACK_GATEWAY, + IPFS_IPC_ID, + IpfsAction, + TOKEN_WHITE_LIST_LOCATION, +} from 'vars/defines'; + +import { ButtonSmall } from 'components/_General/buttons'; +import FriendlyWarning from 'components/_General/WarningFriendly'; const MediaContent = styled.div` overflow-y: auto; @@ -29,6 +37,19 @@ const TokenMediaIframe = styled.iframe` border: 0; `; +const ButtonWrapper = styled.div` + width: 100%; + display: flex; + justify-content: space-around; + margin-top: 25px; +`; + +const DisclaimerText = styled.div` + text-align: justify; + text-align-last: center; + font-size: 0.9rem; +`; + interface TokenMediaDisplayProps { url?: string; } @@ -38,31 +59,44 @@ const TokenMediaDisplay: React.FC = ({ url }) => { const [iframeLoaded, setIframeLoaded] = useState(false); const [iframeHeight, setIframeHeight] = useState('unset'); const [mediaUrl, setMediaUrl] = useState(null); + const [mediaShouldLoad, setMediaShouldLoad] = useState(false); + const [readMore, setReadMore] = useState(false); const ipfsId = useMemo(() => extractIPFSHash(url), [url]); + const tokenAddress = ipfsId || url; + const tokenInWhiteList = !!localStorage.getItem(`${TOKEN_WHITE_LIST_LOCATION}/${tokenAddress}`); useEffect(() => { setIframeHeight('unset'); setMediaUrl(null); + setIframeLoaded(false); + setMediaShouldLoad(false); + setReadMore(false); iframeRef.current?.contentWindow.postMessage({ mediaUrl: '', width: 0 }); }, [url]); - // Request IPFS file if it's an IPFS link. Set link meanwhile anyway useEffect(() => { - if (ipfsId) { - ipcRenderer.send(IPFS_IPC_ID, { - type: IpfsAction.GET, - payload: { - ipfsId, - }, - }); + if (tokenInWhiteList && !iframeLoaded) setMediaShouldLoad(true); + }, [tokenInWhiteList, iframeLoaded]); - // Set fallback in case IPFS data never streams to our node - setMediaUrl(`${DEFAULT_IPFS_FALLBACK_GATEWAY}/${ipfsId}`); - } else { - setMediaUrl(url); + // Request IPFS file if it's an IPFS link. Set link meanwhile anyway + useEffect(() => { + if (mediaShouldLoad) { + if (ipfsId) { + ipcRenderer.send(IPFS_IPC_ID, { + type: IpfsAction.GET, + payload: { + ipfsId, + }, + }); + + // Set fallback in case IPFS data never streams to our node + setMediaUrl(`${DEFAULT_IPFS_FALLBACK_GATEWAY}/${ipfsId}`); + } else { + setMediaUrl(url); + } } - }, [url, ipfsId]); + }, [url, ipfsId, mediaShouldLoad]); // Listen for IPFS files useEffect(() => { @@ -105,21 +139,99 @@ const TokenMediaDisplay: React.FC = ({ url }) => { return () => { if (observer) observer.disconnect(); }; - }, [iframeRef, iframeLoaded]); + }, [iframeRef]); - if (!mediaUrl) return null; + function addTokenToWhiteList() { + localStorage.setItem(`${TOKEN_WHITE_LIST_LOCATION}/${tokenAddress}`, `${tokenAddress}`); + setMediaShouldLoad(true); + } + + function removeTokenFromWhiteList() { + localStorage.removeItem(`${TOKEN_WHITE_LIST_LOCATION}/${tokenAddress}`); + setMediaShouldLoad(false); + setIframeLoaded(false); + } return ( - - - setIframeLoaded(true)} - /> - - + <> + {mediaShouldLoad && ( + <> + + + { + setIframeLoaded(true); + setMediaShouldLoad(true); + }} + /> + + + + removeTokenFromWhiteList()} + > + Hide Image + + + + )} + {!mediaShouldLoad && ( +
+ + +
+ + {!readMore && ( + <> + + The Tokel team does not own, endorse, host or content moderate anything that is + shown in the dApp. By it's nature, the dApp merely reads... + +
+ setReadMore(true)}> + Read More + + + )} + + {readMore && ( + <> + + The Tokel team does not own, endorse, host or content moderate anything that is + shown in the dApp. By it's nature, the dApp merely reads the media URL's + that are linked within the meta data of tokens that are created on the Tokel public + blockchain. Content moderation issues should be addressed with the token creator, + owner, or through the web host that stores the media itself. + +
+ + By accepting this disclaimer, you are accepting that you have personally verified + the source of the image and are happy for it to be displayed, knowing that there are + no content moderators and you're taking all responsibility for viewing the + media and any risks associated with that. You are accepting that anybody that + participates in creating and/or shipping this open source software holds no + liability for what is shown, and that the decision to proceed is completely + voluntary and at your own risk. + + + + setMediaShouldLoad(true)}> + View once + + addTokenToWhiteList()}> + View and never ask again + + + + )} +
+ )} + ); }; diff --git a/src/electron/main.ts b/src/electron/main.ts index 1f472a36..9af0e381 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -23,7 +23,14 @@ import log from 'electron-log'; import { autoUpdater } from 'electron-updater'; import { BitgoAction, checkData } from '../util/bitgoHelper'; -import { BITGO_IPC_ID, IPFS_IPC_ID, VERSIONS_MSG, WindowControl } from '../vars/defines'; +import { + BITGO_IPC_ID, + DEEP_LINK_IPC_ID, + DEEP_LINK_PROTOCOL, + IPFS_IPC_ID, + VERSIONS_MSG, + WindowControl, +} from '../vars/defines'; import MenuBuilder from './menu'; import packagejson from './package.json'; @@ -104,6 +111,15 @@ ipcMain.on('window-controls', async (_, arg) => { } }); +// handle deep links +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient('tokel', process.execPath, [path.resolve(process.argv[1])]); + } +} else { + app.setAsDefaultProtocolClient('tokel'); +} + const createWindow = async () => { if (isDev || process.env.DEBUG_PROD === 'true') { await installExtensions(); @@ -217,6 +233,17 @@ autoUpdater.on('update-downloaded', data => { mainWindow?.webContents.send('update-downloaded', data); }); +app.on('open-url', (_, url) => { + if (url.startsWith(DEEP_LINK_PROTOCOL)) { + console.log('RECEIVED DEEP LINK:', url); + const urlObj = new URL(url); + mainWindow.webContents.send(DEEP_LINK_IPC_ID, { + view: urlObj.hostname, + params: urlObj.search, + }); + } +}); + app.on('window-all-closed', () => { // Respect the OSX convention of having the application in memory even // after all windows have been closed diff --git a/src/electron/package.json b/src/electron/package.json index 84f72ade..18808a2e 100644 --- a/src/electron/package.json +++ b/src/electron/package.json @@ -1,7 +1,7 @@ { "name": "tokel_app", "productName": "tokelPlatform", - "version": "1.3.2", + "version": "1.3.3", "description": "Komodo ecosystem’s Token Platform", "main": "./main.js", "author": { @@ -14,7 +14,7 @@ }, "license": "MIT", "dependencies": { - "@tokel/nspv-js": "0.1.8", + "@tokel/nspv-js": "0.1.9", "axios": "0.21.4", "ipfs-core": "0.13.0", "satoshi-bitcoin": "1.0.5" diff --git a/src/electron/yarn.lock b/src/electron/yarn.lock index 1560729e..01aa8f3d 100644 --- a/src/electron/yarn.lock +++ b/src/electron/yarn.lock @@ -274,10 +274,10 @@ resolved "https://registry.yarnpkg.com/@tokel/cryptoconditions-js/-/cryptoconditions-js-0.1.3.tgz#bc0f0252427394066a13a09196c02ce535681538" integrity sha512-R2GYxRjXhoh6cJTeROVpRWWxTg5WDFn3c8QUjeyhTcUlXAXU4OuQ0t1jazGr6kvyADaZQgbGZAm5vtEj/ecwsQ== -"@tokel/nspv-js@0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@tokel/nspv-js/-/nspv-js-0.1.8.tgz#a7ec7e3aaa44c343d1cb64107ef925b7b8d0071d" - integrity sha512-XhWfWFy/+zVv+l6P/yVbbbivbgFmdXie2CtooQxgFGk56jVrSXPipS4o09WZELldPZ/7Gilzzu5SXCvAgWAk5Q== +"@tokel/nspv-js@0.1.9": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@tokel/nspv-js/-/nspv-js-0.1.9.tgz#2c2cc9a8ce82208042218dd66c9f8d04a880615d" + integrity sha512-n2CGAZSM15DfP1nUHKvGYtEcrAQ8dEoUqy/TnCEytSqMerzsTWGV7+knMzErqAJH48ijWtNogUXYrlsH+05vkg== dependencies: "@tokel/cryptoconditions-js" "0.1.3" bech32 "0.0.3" diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts index 9031ae6e..c9840513 100644 --- a/src/hooks/usePrevious.ts +++ b/src/hooks/usePrevious.ts @@ -1,11 +1,11 @@ -import { useRef, useEffect } from 'react'; +import { useEffect, useRef } from 'react'; const usePrevious = (value: T): T | undefined => { const ref = useRef(); useEffect(() => { ref.current = value; - }); + }, [value]); return ref.current; }; diff --git a/src/store/models/environment.ts b/src/store/models/environment.ts index d0d86428..68ae3d5b 100644 --- a/src/store/models/environment.ts +++ b/src/store/models/environment.ts @@ -12,6 +12,7 @@ import type { RootModel } from './models'; export type EnvironmentState = { theme?: ThemeName; view?: string; + deepLinkParams?: string; modal: Modal; tokenDetails: Record; tokelPriceUSD?: number; @@ -41,6 +42,7 @@ export default createModel()({ state: { theme: themeNames[0], view: ViewType.DASHBOARD, + deepLinkParams: '', modal: DEFAULT_NULL_MODAL, tokenDetails: {}, tokelPriceUSD: null, @@ -56,6 +58,7 @@ export default createModel()({ reducers: { SET_THEME: (state, theme: string) => ({ ...state, theme }), SET_VIEW: (state, view: string) => ({ ...state, view }), + SET_DEEP_LINK_PARAMS: (state, deepLinkParams: string) => ({ ...state, deepLinkParams }), SET_MODAL: (state, modal: Modal) => ({ ...state, modal }), SET_MODAL_NAME: (state, modalName: ModalName) => dp.set(state, `modal.name`, modalName), SET_TOKEN_DETAIL: (state, detail: TokenDetail) => { diff --git a/src/store/selectors.ts b/src/store/selectors.ts index d8388f09..089ed182 100644 --- a/src/store/selectors.ts +++ b/src/store/selectors.ts @@ -7,6 +7,7 @@ import { RootState } from './rematch'; export const selectTheme = (state: RootState) => state.environment.theme; export const selectView = (state: RootState) => state.environment.view; +export const selectDeepLinkParams = (state: RootState) => state.environment.deepLinkParams; export const selectModal = (state: RootState) => state.environment.modal; export const selectModalName = (state: RootState) => state.environment.modal.name; export const selectModalOptions = (state: RootState) => state.environment.modal.options; diff --git a/src/vars/defines.ts b/src/vars/defines.ts index f205aa85..3710f91f 100644 --- a/src/vars/defines.ts +++ b/src/vars/defines.ts @@ -5,7 +5,9 @@ export const TICKER = 'TKL'; export const RPC_PORT = '29405'; export const BITGO_IPC_ID = 'bitgo'; export const IPFS_IPC_ID = 'ipfs'; +export const DEEP_LINK_IPC_ID = 'link'; export const VERSIONS_MSG = 'version'; +export const DEEP_LINK_PROTOCOL = 'tokel://'; export enum IpfsAction { GET = 'get', } @@ -26,6 +28,8 @@ export const IS_DEV = process.env.NODE_ENV === 'development' || process.env.NODE export const IS_PROD = process.env.NODE_ENV === 'production'; export const SATOSHIS = 100000000; +export const TOKEN_WHITE_LIST_LOCATION = 'token_white_list'; + export enum NetworkType { TOKEL = 'TOKEL', TKLTEST = 'TKLTEST2',