diff --git a/wallet/configs/ts/tsconfig.common.json b/wallet/configs/ts/tsconfig.common.json index aec900655a6fa..3f99be4f10868 100644 --- a/wallet/configs/ts/tsconfig.common.json +++ b/wallet/configs/ts/tsconfig.common.json @@ -26,7 +26,9 @@ "_store": ["./src/ui/app/redux/store/"], "_store/*": ["./src/ui/app/redux/store/*"], "_hooks": ["./src/ui/app/hooks/"], - "_components/*": ["./src/ui/app/components/*"] + "_components/*": ["./src/ui/app/components/*"], + "_messaging/*": ["./src/shared/messaging/*"], + "_messages/*": ["./src/shared/messaging/messages/*"] } }, "include": ["../../src"], diff --git a/wallet/src/shared/messaging/messages/payloads/permissions/Permission.ts b/wallet/src/shared/messaging/messages/payloads/permissions/Permission.ts new file mode 100644 index 0000000000000..4c79b9855ad26 --- /dev/null +++ b/wallet/src/shared/messaging/messages/payloads/permissions/Permission.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import type { PermissionType } from './PermissionType'; +import type { SuiAddress } from '@mysten/sui.js'; + +export interface Permission { + id: string; + origin: string; + favIcon: string | undefined; + accounts: SuiAddress[]; + allowed: boolean | null; + permissions: PermissionType[]; + createdDate: string; + responseDate: string | null; +} diff --git a/wallet/src/shared/messaging/messages/payloads/permissions/PermissionType.ts b/wallet/src/shared/messaging/messages/payloads/permissions/PermissionType.ts new file mode 100644 index 0000000000000..74b87edcb82a8 --- /dev/null +++ b/wallet/src/shared/messaging/messages/payloads/permissions/PermissionType.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +export type PermissionType = 'viewAccount'; diff --git a/wallet/src/shared/messaging/messages/payloads/permissions/index.ts b/wallet/src/shared/messaging/messages/payloads/permissions/index.ts new file mode 100644 index 0000000000000..2df9625200813 --- /dev/null +++ b/wallet/src/shared/messaging/messages/payloads/permissions/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +export * from './PermissionType'; +export * from './Permission'; diff --git a/wallet/src/ui/app/background-client/index.ts b/wallet/src/ui/app/background-client/index.ts new file mode 100644 index 0000000000000..cd93aca595491 --- /dev/null +++ b/wallet/src/ui/app/background-client/index.ts @@ -0,0 +1,52 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { setPermissions } from '_redux/slices/permissions'; + +import type { SuiAddress } from '@mysten/sui.js'; +import type { AppDispatch } from '_store'; + +export class BackgroundClient { + private _dispatch: AppDispatch | null = null; + private _initialized = false; + + public async init(dispatch: AppDispatch) { + if (this._initialized) { + throw new Error('[BackgroundClient] already initialized'); + } + this._initialized = true; + this._dispatch = dispatch; + // TODO: implement + return this.sendGetPermissionRequests().then(() => undefined); + } + + public sendPermissionResponse( + id: string, + accounts: SuiAddress[], + allowed: boolean, + responseDate: string + ) { + // TODO: implement + } + + public async sendGetPermissionRequests() { + // TODO: remove mock and implement + const id = /connect\/(.+)/.exec(window.location.hash)?.[1]; + if (this._dispatch && id) { + this._dispatch( + setPermissions([ + { + id, + accounts: [], + allowed: null, + createdDate: new Date().toISOString(), + favIcon: 'https://www.google.com/favicon.ico', + origin: 'https://www.google.com', + permissions: ['viewAccount'], + responseDate: null, + }, + ]) + ); + } + } +} diff --git a/wallet/src/ui/app/components/account-address/index.tsx b/wallet/src/ui/app/components/account-address/index.tsx index e1ebba502b50e..401d165db8272 100644 --- a/wallet/src/ui/app/components/account-address/index.tsx +++ b/wallet/src/ui/app/components/account-address/index.tsx @@ -1,6 +1,8 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +import cl from 'classnames'; + import CopyToClipboard from '_components/copy-to-clipboard'; import ExplorerLink from '_components/explorer-link'; import { ExplorerLinkType } from '_components/explorer-link/ExplorerLinkType'; @@ -8,24 +10,31 @@ import { useAppSelector, useMiddleEllipsis } from '_hooks'; import st from './AccountAddress.module.scss'; -function AccountAddress() { +type AccountAddressProps = { + className?: string; + showLink?: boolean; +}; + +function AccountAddress({ className, showLink = true }: AccountAddressProps) { const address = useAppSelector( ({ account: { address } }) => address && `0x${address}` ); const shortenAddress = useMiddleEllipsis(address || '', 20); return address ? ( - + {shortenAddress} - + {showLink ? ( + + ) : null} ) : null; } diff --git a/wallet/src/ui/app/index.tsx b/wallet/src/ui/app/index.tsx index e68d69d55a66b..81620376c94a7 100644 --- a/wallet/src/ui/app/index.tsx +++ b/wallet/src/ui/app/index.tsx @@ -14,6 +14,7 @@ import BackupPage from './pages/initialize/backup'; import CreatePage from './pages/initialize/create'; import ImportPage from './pages/initialize/import'; import SelectPage from './pages/initialize/select'; +import SiteConnectPage from './pages/site-connect'; import TransactionDetailsPage from './pages/transaction-details'; import TransferCoinPage from './pages/transfer-coin'; import WelcomePage from './pages/welcome'; @@ -56,6 +57,7 @@ const App = () => { } /> } /> + } /> } diff --git a/wallet/src/ui/app/pages/site-connect/SiteConnectPage.module.scss b/wallet/src/ui/app/pages/site-connect/SiteConnectPage.module.scss new file mode 100644 index 0000000000000..6aff9e88fc55b --- /dev/null +++ b/wallet/src/ui/app/pages/site-connect/SiteConnectPage.module.scss @@ -0,0 +1,58 @@ +.container { + display: flex; + flex-flow: column nowrap; + padding: 15px; + align-items: center; + justify-content: center; +} + +.title { + margin-bottom: 30px; +} + +.origin-container { + display: flex; + flex-flow: column nowrap; + align-items: center; + padding: 8px; + border: 1px solid #b2bbc3; + border-radius: 4px; + align-self: stretch; +} + +.fav-icon { + width: 40px; + border-radius: 4px; + margin-bottom: 8px; +} + +.origin { + font-size: 14px; + font-weight: 500; +} + +.label { + align-self: flex-start; + font-weight: 700; + margin-top: 16px; + margin-bottom: 2px; + letter-spacing: 0.6px; +} + +.permission { + font-weight: 500; + font-style: italic; + padding: 3px 6px; + background-color: #c7c7c7; + border-radius: 4px; + display: inline-block; +} + +.actions { + display: flex; + flex-flow: row nowrap; + margin-top: 35px; + align-self: stretch; + align-items: center; + justify-content: space-between; +} diff --git a/wallet/src/ui/app/pages/site-connect/index.tsx b/wallet/src/ui/app/pages/site-connect/index.tsx new file mode 100644 index 0000000000000..7db6a0fb0c6f7 --- /dev/null +++ b/wallet/src/ui/app/pages/site-connect/index.tsx @@ -0,0 +1,122 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import AccountAddress from '_components/account-address'; +import Loading from '_components/loading'; +import { useAppDispatch, useAppSelector, useInitializedGuard } from '_hooks'; +import { + permissionsSelectors, + respondToPermissionRequest, +} from '_redux/slices/permissions'; + +import type { PermissionType } from '_messages/payloads/permissions'; +import type { RootState } from '_redux/RootReducer'; +import type { MouseEventHandler } from 'react'; + +import st from './SiteConnectPage.module.scss'; + +const permissionTypeToTxt: Record = { + viewAccount: 'View Account', +}; + +function SiteConnectPage() { + const { requestID } = useParams(); + const guardLoading = useInitializedGuard(true); + const permissionsInitialized = useAppSelector( + ({ permissions }) => permissions.initialized + ); + const loading = guardLoading || !permissionsInitialized; + const permissionSelector = useMemo( + () => (state: RootState) => + requestID + ? permissionsSelectors.selectById(state, requestID) + : null, + [requestID] + ); + const dispatch = useAppDispatch(); + const permissionRequest = useAppSelector(permissionSelector); + const activeAccount = useAppSelector(({ account }) => account.address); + const [submitting, setSubmitting] = useState(false); + const handleOnResponse = useCallback>( + (e) => { + const allowed = e.currentTarget.dataset.allow === 'true'; + if (requestID && activeAccount) { + setSubmitting(true); + dispatch( + respondToPermissionRequest({ + id: requestID, + accounts: allowed ? [activeAccount] : [], + allowed, + }) + ); + } + }, + [dispatch, requestID, activeAccount] + ); + useEffect(() => { + if ( + !loading && + (!permissionRequest || permissionRequest.responseDate) + ) { + window.close(); + } + }, [loading, permissionRequest]); + + return ( + + {permissionRequest ? ( +
+

Connect to Sui wallet

+ +
+ {permissionRequest.favIcon ? ( + Site favicon + ) : null} + + {permissionRequest.origin} + +
+ + + +
+ {permissionRequest.permissions.map((aPermission) => ( + + {permissionTypeToTxt[aPermission]} + + ))} +
+
+ + +
+
+ ) : null} +
+ ); +} + +export default SiteConnectPage; diff --git a/wallet/src/ui/app/redux/RootReducer.ts b/wallet/src/ui/app/redux/RootReducer.ts index bf762950600b0..db97ed4419a90 100644 --- a/wallet/src/ui/app/redux/RootReducer.ts +++ b/wallet/src/ui/app/redux/RootReducer.ts @@ -5,6 +5,7 @@ import { combineReducers } from '@reduxjs/toolkit'; import account from './slices/account'; import app from './slices/app'; +import permissions from './slices/permissions'; import suiObjects from './slices/sui-objects'; import transactions from './slices/transactions'; import txresults from './slices/txresults'; @@ -15,6 +16,7 @@ const rootReducer = combineReducers({ suiObjects, transactions, txresults, + permissions, }); export type RootState = ReturnType; diff --git a/wallet/src/ui/app/redux/slices/permissions/index.ts b/wallet/src/ui/app/redux/slices/permissions/index.ts new file mode 100644 index 0000000000000..45a7e5ff28b93 --- /dev/null +++ b/wallet/src/ui/app/redux/slices/permissions/index.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { + createAsyncThunk, + createEntityAdapter, + createSlice, +} from '@reduxjs/toolkit'; + +import type { SuiAddress } from '@mysten/sui.js'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { Permission } from '_messages/payloads/permissions'; +import type { RootState } from '_redux/RootReducer'; +import type { AppThunkConfig } from '_store/thunk-extras'; + +const permissionsAdapter = createEntityAdapter({ + sortComparer: (a, b) => { + const aDate = new Date(a.createdDate); + const bDate = new Date(b.createdDate); + return aDate.getTime() - bDate.getTime(); + }, +}); + +export const respondToPermissionRequest = createAsyncThunk< + { + id: string; + accounts: SuiAddress[]; + allowed: boolean; + responseDate: string; + }, + { id: string; accounts: SuiAddress[]; allowed: boolean }, + AppThunkConfig +>( + 'respond-to-permission-request', + ({ id, accounts, allowed }, { extra: { background } }) => { + const responseDate = new Date().toISOString(); + background.sendPermissionResponse(id, accounts, allowed, responseDate); + return { id, accounts, allowed, responseDate }; + } +); + +const slice = createSlice({ + name: 'permissions', + initialState: permissionsAdapter.getInitialState({ initialized: false }), + reducers: { + setPermissions: (state, { payload }: PayloadAction) => { + permissionsAdapter.setAll(state, payload); + state.initialized = true; + }, + }, + extraReducers: (build) => { + build.addCase( + respondToPermissionRequest.fulfilled, + (state, { payload }) => { + const { id, accounts, allowed, responseDate } = payload; + permissionsAdapter.updateOne(state, { + id, + changes: { + accounts, + allowed, + responseDate, + }, + }); + } + ); + }, +}); + +export default slice.reducer; + +export const { setPermissions } = slice.actions; + +export const permissionsSelectors = permissionsAdapter.getSelectors( + (state: RootState) => state.permissions +); diff --git a/wallet/src/ui/app/redux/store/thunk-extras.ts b/wallet/src/ui/app/redux/store/thunk-extras.ts index 5669f385f9400..ea6655103503d 100644 --- a/wallet/src/ui/app/redux/store/thunk-extras.ts +++ b/wallet/src/ui/app/redux/store/thunk-extras.ts @@ -3,6 +3,7 @@ import ApiProvider from '_app/ApiProvider'; import KeypairVault from '_app/KeypairVault'; +import { BackgroundClient } from '_app/background-client'; import type { RootState } from '_redux/RootReducer'; import type { AppDispatch } from '_store'; @@ -10,6 +11,7 @@ import type { AppDispatch } from '_store'; export const thunkExtras = { keypairVault: new KeypairVault(), api: new ApiProvider(), + background: new BackgroundClient(), }; type ThunkExtras = typeof thunkExtras; diff --git a/wallet/src/ui/index.tsx b/wallet/src/ui/index.tsx index 3c22c58043de1..3e23215f8c967 100644 --- a/wallet/src/ui/index.tsx +++ b/wallet/src/ui/index.tsx @@ -10,25 +10,37 @@ import App from './app'; import { initAppType } from '_redux/slices/app'; import { getFromLocationSearch } from '_redux/slices/app/AppType'; import store from '_store'; +import { thunkExtras } from '_store/thunk-extras'; import './styles/global.scss'; -// TODO only in dev -(window as unknown as Record)['store'] = store; +async function init() { + if (process.env.NODE_ENV === 'development') { + Object.defineProperty(window, 'store', { value: store }); + } -store.dispatch(initAppType(getFromLocationSearch(window.location.search))); + store.dispatch(initAppType(getFromLocationSearch(window.location.search))); + await thunkExtras.background.init(store.dispatch); +} -const rootDom = document.getElementById('root'); -if (!rootDom) { - throw new Error('Root element not found'); +function renderApp() { + const rootDom = document.getElementById('root'); + if (!rootDom) { + throw new Error('Root element not found'); + } + const root = createRoot(rootDom); + root.render( + + + + + + + + ); } -const root = createRoot(rootDom); -root.render( - - - - - - - -); + +(async () => { + await init(); + renderApp(); +})(); diff --git a/wallet/src/ui/styles/global.scss b/wallet/src/ui/styles/global.scss index 22e42a436e382..a95dab3a8bcb4 100644 --- a/wallet/src/ui/styles/global.scss +++ b/wallet/src/ui/styles/global.scss @@ -29,6 +29,10 @@ body { cursor: pointer; color: black; + &.link { + background-color: transparent; + } + &:visited, &:active { color: inherit;