@arcblock/ux 2.4.44 → 2.4.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Address/compact-text.js +145 -0
- package/lib/Address/did-address.js +218 -0
- package/lib/Address/index.js +54 -0
- package/lib/Address/responsive-did-address.js +106 -0
- package/lib/Avatar/did-motif.js +89 -0
- package/lib/Avatar/etherscan-blockies.js +108 -0
- package/lib/Avatar/index.js +166 -0
- package/lib/DidLogo/index.js +46 -0
- package/lib/SessionManager/index.js +408 -0
- package/lib/Util/wallet.js +49 -0
- package/lib/WebWalletSWKeeper/index.js +143 -0
- package/lib/index.js +48 -0
- package/package.json +6 -4
- package/src/Address/compact-text.js +77 -0
- package/src/Address/did-address.js +215 -0
- package/src/Address/index.js +22 -0
- package/src/Address/responsive-did-address.js +77 -0
- package/src/Avatar/did-motif.js +40 -0
- package/src/Avatar/etherscan-blockies.js +81 -0
- package/src/Avatar/index.js +141 -0
- package/src/DidLogo/index.js +30 -0
- package/src/SessionManager/index.js +366 -0
- package/src/Util/wallet.js +35 -0
- package/src/WebWalletSWKeeper/index.js +103 -0
- package/src/index.js +12 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
/* eslint-disable react/no-unused-prop-types */
|
2
|
+
import { useState, useMemo } from 'react';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
5
|
+
import { Shape } from '@arcblock/did-motif';
|
6
|
+
import Box from '@mui/material/Box';
|
7
|
+
import Img from '../Img';
|
8
|
+
import { mergeProps } from '../Util';
|
9
|
+
import { styled } from '../Theme';
|
10
|
+
import DIDMotif from './did-motif';
|
11
|
+
import blockies from './etherscan-blockies';
|
12
|
+
|
13
|
+
// 参考: asset-chain @arcblock/did
|
14
|
+
const isEthereumDid = (did) => {
|
15
|
+
const address = did.replace('did:abt:', '');
|
16
|
+
// check if it has the basic requirements of an address
|
17
|
+
if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
|
18
|
+
return false;
|
19
|
+
}
|
20
|
+
return true;
|
21
|
+
};
|
22
|
+
|
23
|
+
// 参考: https://github.com/blocklet/block-explorer/issues/478#issuecomment-1038954976
|
24
|
+
function Avatar(props) {
|
25
|
+
const [imgError, setImgError] = useState(false);
|
26
|
+
const newProps = mergeProps(props, Avatar, []);
|
27
|
+
const { did = '', size, src, variant, animation, shape, ...rest } = newProps;
|
28
|
+
|
29
|
+
// ethereum blockies
|
30
|
+
const blockyIcon = useMemo(() => {
|
31
|
+
if (isEthereumDid(did)) {
|
32
|
+
return blockies
|
33
|
+
.createIcon({
|
34
|
+
seed: did.replace('did:abt:', '').toLowerCase(),
|
35
|
+
size: 8,
|
36
|
+
scale: 16,
|
37
|
+
})
|
38
|
+
.toDataURL();
|
39
|
+
}
|
40
|
+
return null;
|
41
|
+
}, [did]);
|
42
|
+
|
43
|
+
// 如果显式传入 src 则直接使用 src
|
44
|
+
if (src && !imgError) {
|
45
|
+
return (
|
46
|
+
<StyledImg
|
47
|
+
width={size}
|
48
|
+
src={src}
|
49
|
+
alt={did}
|
50
|
+
onError={() => setImgError(true)}
|
51
|
+
{...rest}
|
52
|
+
className={`avatar-img--${variant} ${rest.className}`}
|
53
|
+
/>
|
54
|
+
);
|
55
|
+
}
|
56
|
+
// 对于 eth address, 渲染成 blocky icon, 形状与 account role type 的 did motif 相似都为矩形, 高宽比为 0.7
|
57
|
+
if (blockyIcon) {
|
58
|
+
// blocky icon 要与灰色卡片矩形留有空间
|
59
|
+
const padding = size > 24 ? 4 : 2;
|
60
|
+
return (
|
61
|
+
<BlockyIconContainer $size={size} {...rest}>
|
62
|
+
<div className="blocky-icon-inner">
|
63
|
+
<Img width={size * 0.7 - padding * 2} src={blockyIcon} alt={did} />
|
64
|
+
</div>
|
65
|
+
</BlockyIconContainer>
|
66
|
+
);
|
67
|
+
}
|
68
|
+
if (did) {
|
69
|
+
// 渲染 did motif
|
70
|
+
return (
|
71
|
+
<DIDMotif
|
72
|
+
did={did.replace('did:abt:', '')}
|
73
|
+
size={size}
|
74
|
+
animation={animation}
|
75
|
+
shape={Shape[(shape || '').toUpperCase()]}
|
76
|
+
responsive={newProps.responsive}
|
77
|
+
{...rest}
|
78
|
+
/>
|
79
|
+
);
|
80
|
+
}
|
81
|
+
throw new Error(`Invalid DID: ${did}`);
|
82
|
+
}
|
83
|
+
|
84
|
+
Avatar.propTypes = {
|
85
|
+
did: PropTypes.string.isRequired,
|
86
|
+
size: PropTypes.number,
|
87
|
+
variant: PropTypes.oneOf(['circle', 'rounded', 'default']),
|
88
|
+
// animation 仅对 did motif 有效
|
89
|
+
animation: PropTypes.bool,
|
90
|
+
// shape 仅对 did motif 有效, 明确指定 motif shape, 而非由 did role type 自动推断 shape
|
91
|
+
shape: PropTypes.oneOf(['', 'rectangle', 'square', 'hexagon', 'circle']),
|
92
|
+
};
|
93
|
+
|
94
|
+
Avatar.defaultProps = {
|
95
|
+
size: 36,
|
96
|
+
variant: 'default',
|
97
|
+
animation: false,
|
98
|
+
shape: '',
|
99
|
+
};
|
100
|
+
|
101
|
+
const BlockyIconContainer = styled('div')`
|
102
|
+
display: flex;
|
103
|
+
align-items: center;
|
104
|
+
width: ${(props) => props.$size}px;
|
105
|
+
height: ${(props) => props.$size}px;
|
106
|
+
.blocky-icon-inner {
|
107
|
+
box-sizing: border-box;
|
108
|
+
display: flex;
|
109
|
+
justify-content: center;
|
110
|
+
align-items: center;
|
111
|
+
width: ${(props) => props.$size}px;
|
112
|
+
height: ${(props) => props.$size * 0.7}px;
|
113
|
+
border-radius: ${(props) => Math.min(10, Math.floor(0.1 * props.$size + 2))}px;
|
114
|
+
background: #ddd;
|
115
|
+
}
|
116
|
+
`;
|
117
|
+
|
118
|
+
const StyledImg = styled(Img)`
|
119
|
+
&.avatar-img--rounded {
|
120
|
+
border-radius: 4px;
|
121
|
+
overflow: hidden;
|
122
|
+
}
|
123
|
+
&.avatar-img--circle {
|
124
|
+
border-radius: 100%;
|
125
|
+
overflow: hidden;
|
126
|
+
}
|
127
|
+
`;
|
128
|
+
|
129
|
+
export default function AvatarWithErrorBoundary(props) {
|
130
|
+
const size = props.size || 36;
|
131
|
+
const borderRadius = { rounded: '4px', circle: '100%' }[props.variant] || 0;
|
132
|
+
return (
|
133
|
+
<ErrorBoundary
|
134
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
135
|
+
fallbackRender={() => <Box width={size} height={size} bgcolor="grey.300" borderRadius={borderRadius} />}>
|
136
|
+
<Avatar {...props} />
|
137
|
+
</ErrorBoundary>
|
138
|
+
);
|
139
|
+
}
|
140
|
+
|
141
|
+
AvatarWithErrorBoundary.propTypes = Avatar.propTypes;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import PropTypes from 'prop-types';
|
2
|
+
import DidLogoIcon from '@arcblock/icons/lib/DidLogo';
|
3
|
+
|
4
|
+
const defaultStyle = {
|
5
|
+
width: 'auto',
|
6
|
+
height: '1em',
|
7
|
+
fill: 'currentColor',
|
8
|
+
};
|
9
|
+
|
10
|
+
export default function DidLogo({ style, size, className }) {
|
11
|
+
const height = Number(size) > 0 ? `${Number(size)}px` : size;
|
12
|
+
return (
|
13
|
+
<DidLogoIcon
|
14
|
+
className={`${className}`.trim()}
|
15
|
+
style={Object.assign({}, defaultStyle, style, height ? { height } : {})}
|
16
|
+
/>
|
17
|
+
);
|
18
|
+
}
|
19
|
+
|
20
|
+
DidLogo.propTypes = {
|
21
|
+
style: PropTypes.object,
|
22
|
+
size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
23
|
+
className: PropTypes.string,
|
24
|
+
};
|
25
|
+
|
26
|
+
DidLogo.defaultProps = {
|
27
|
+
style: defaultStyle,
|
28
|
+
size: 0,
|
29
|
+
className: '',
|
30
|
+
};
|
@@ -0,0 +1,366 @@
|
|
1
|
+
/* eslint-disable react/no-array-index-key */
|
2
|
+
/* eslint-disable react/jsx-no-bind */
|
3
|
+
import { useMemo, useRef, useState } from 'react';
|
4
|
+
import PropTypes from 'prop-types';
|
5
|
+
import { IconButton, ClickAwayListener, MenuList, MenuItem, Paper, Popper, SvgIcon, Button, Chip } from '@mui/material';
|
6
|
+
import AccountIcon from '@arcblock/icons/lib/Account';
|
7
|
+
import ShieldCheck from 'mdi-material-ui/ShieldCheck';
|
8
|
+
import OpenInIcon from '@arcblock/icons/lib/OpenIn';
|
9
|
+
import DisconnectIcon from '@arcblock/icons/lib/Disconnect';
|
10
|
+
import SwitchDidIcon from '@arcblock/icons/lib/Switch';
|
11
|
+
import SwitchProfileIcon from '@mui/icons-material/PersonOutline';
|
12
|
+
import SwitchPassportIcon from '@mui/icons-material/VpnKeyOutlined';
|
13
|
+
import useBrowser from '@arcblock/react-hooks/lib/useBrowser';
|
14
|
+
import { styled } from '../Theme';
|
15
|
+
|
16
|
+
import DidAvatar from '../Avatar';
|
17
|
+
import DidAddress from '../Address';
|
18
|
+
|
19
|
+
const messages = {
|
20
|
+
zh: {
|
21
|
+
switchDid: '切换账户',
|
22
|
+
switchProfile: '切换用户信息',
|
23
|
+
switchPassport: '切换通行证',
|
24
|
+
disconnect: '退出',
|
25
|
+
connect: '登录',
|
26
|
+
openInWallet: '打开 DID 钱包',
|
27
|
+
},
|
28
|
+
en: {
|
29
|
+
switchDid: 'Switch DID',
|
30
|
+
switchProfile: 'Switch Profile',
|
31
|
+
switchPassport: 'Switch Passport',
|
32
|
+
disconnect: 'Disconnect',
|
33
|
+
connect: 'Connect',
|
34
|
+
openInWallet: 'Open DID Wallet',
|
35
|
+
},
|
36
|
+
};
|
37
|
+
|
38
|
+
function SessionManager({
|
39
|
+
session,
|
40
|
+
locale,
|
41
|
+
showText,
|
42
|
+
showRole,
|
43
|
+
switchDid,
|
44
|
+
switchProfile,
|
45
|
+
switchPassport,
|
46
|
+
disableLogout,
|
47
|
+
onLogin,
|
48
|
+
onLogout,
|
49
|
+
onSwitchDid,
|
50
|
+
onSwitchProfile,
|
51
|
+
onSwitchPassport,
|
52
|
+
menu,
|
53
|
+
menuRender,
|
54
|
+
dark,
|
55
|
+
size,
|
56
|
+
...rest
|
57
|
+
}) {
|
58
|
+
const userAnchorRef = useRef(null);
|
59
|
+
const [userOpen, setUserOpen] = useState(false);
|
60
|
+
|
61
|
+
// base64 img maybe have some blank char, need encodeURIComponent to transform it
|
62
|
+
const avatar = session.user?.avatar?.replace(/\s/g, encodeURIComponent(' '));
|
63
|
+
const currentRole = useMemo(
|
64
|
+
() => session.user?.passports?.find((item) => item.name === session.user.role),
|
65
|
+
[session.user]
|
66
|
+
);
|
67
|
+
const browser = useBrowser();
|
68
|
+
|
69
|
+
if (!session.user) {
|
70
|
+
return showText ? (
|
71
|
+
<Button
|
72
|
+
sx={[{ borderRadius: '100vw' }, dark && { color: '#fff', borderColor: '#fff' }]}
|
73
|
+
variant="outlined"
|
74
|
+
onClick={_onLogin}
|
75
|
+
aria-label="login button"
|
76
|
+
{...rest}
|
77
|
+
data-cy="sessionManager-login">
|
78
|
+
<AccountIcon />
|
79
|
+
<span style={{ lineHeight: '25px' }}>{messages[locale].connect}</span>
|
80
|
+
</Button>
|
81
|
+
) : (
|
82
|
+
<IconButton {...rest} onClick={_onLogin} data-cy="sessionManager-login" size="medium">
|
83
|
+
<AccountIcon style={{ width: size, height: size, color: dark ? '#fff' : '' }} />
|
84
|
+
</IconButton>
|
85
|
+
);
|
86
|
+
}
|
87
|
+
|
88
|
+
function onToggleUser() {
|
89
|
+
setUserOpen((prevOpen) => !prevOpen);
|
90
|
+
}
|
91
|
+
|
92
|
+
function onCloseUser(e) {
|
93
|
+
if (userAnchorRef.current && userAnchorRef.current.contains(e.target)) {
|
94
|
+
return;
|
95
|
+
}
|
96
|
+
setUserOpen(false);
|
97
|
+
}
|
98
|
+
|
99
|
+
function _onLogin() {
|
100
|
+
session.login(onLogin);
|
101
|
+
}
|
102
|
+
function _onLogout() {
|
103
|
+
session.logout((...args) => {
|
104
|
+
setUserOpen(false);
|
105
|
+
onLogout(...args);
|
106
|
+
});
|
107
|
+
}
|
108
|
+
function _onSwitchDid() {
|
109
|
+
session.switchDid((...args) => {
|
110
|
+
setUserOpen(false);
|
111
|
+
onSwitchDid(...args);
|
112
|
+
});
|
113
|
+
}
|
114
|
+
function _onSwitchProfile() {
|
115
|
+
session.switchProfile((...args) => {
|
116
|
+
setUserOpen(false);
|
117
|
+
onSwitchProfile(...args);
|
118
|
+
});
|
119
|
+
}
|
120
|
+
function _onSwitchPassport() {
|
121
|
+
setUserOpen(false);
|
122
|
+
session.switchPassport((...args) => {
|
123
|
+
setUserOpen(false);
|
124
|
+
onSwitchPassport(...args);
|
125
|
+
});
|
126
|
+
}
|
127
|
+
|
128
|
+
const message = messages[locale] || messages.en;
|
129
|
+
|
130
|
+
return (
|
131
|
+
<>
|
132
|
+
<IconButton
|
133
|
+
ref={userAnchorRef}
|
134
|
+
onClick={onToggleUser}
|
135
|
+
{...rest}
|
136
|
+
data-cy="sessionManager-logout-popup"
|
137
|
+
size="medium"
|
138
|
+
style={{ lineHeight: 1 }}>
|
139
|
+
<DidAvatar variant="circle" did={session.user.did} src={avatar} size={size} shape="circle" />
|
140
|
+
</IconButton>
|
141
|
+
{userAnchorRef.current && (
|
142
|
+
<StyledPopper
|
143
|
+
open={userOpen}
|
144
|
+
disablePortal
|
145
|
+
anchorEl={userAnchorRef.current}
|
146
|
+
placement="bottom-end"
|
147
|
+
$dark={dark}>
|
148
|
+
<Paper
|
149
|
+
sx={[
|
150
|
+
(theme) => ({
|
151
|
+
borderColor: '#F0F0F0',
|
152
|
+
boxShadow: '0px 8px 12px rgba(92, 92, 92, 0.04)',
|
153
|
+
borderRadius: theme.spacing(2),
|
154
|
+
overflow: 'hidden',
|
155
|
+
maxWidth: 'calc(100vw - 10px)',
|
156
|
+
'& .MuiChip-root .MuiChip-icon': {
|
157
|
+
color: theme.palette.success.main,
|
158
|
+
},
|
159
|
+
}),
|
160
|
+
dark && {
|
161
|
+
backgroundColor: '#27282c',
|
162
|
+
color: '#fff',
|
163
|
+
border: 0,
|
164
|
+
'& .MuiChip-root': {
|
165
|
+
borderColor: '#aaa',
|
166
|
+
},
|
167
|
+
'& .MuiListItem-root, & .MuiChip-label': {
|
168
|
+
color: '#aaa',
|
169
|
+
},
|
170
|
+
'& .MuiListItem-root:hover': {
|
171
|
+
backgroundColor: '#363434',
|
172
|
+
},
|
173
|
+
},
|
174
|
+
]}
|
175
|
+
variant="outlined">
|
176
|
+
<ClickAwayListener onClickAway={onCloseUser}>
|
177
|
+
<MenuList sx={{ p: 0 }}>
|
178
|
+
<div className="session-manager-user">
|
179
|
+
<div className="session-manager-user-name">
|
180
|
+
<span>{session.user.fullName}</span>
|
181
|
+
{!!showRole && (currentRole?.title || session.user?.role.toUpperCase()) && (
|
182
|
+
<Chip
|
183
|
+
label={currentRole?.title || session.user?.role.toUpperCase()}
|
184
|
+
size="small"
|
185
|
+
variant="outlined"
|
186
|
+
sx={{ height: 'auto', marginRight: 0 }}
|
187
|
+
icon={<SvgIcon component={ShieldCheck} size="small" />}
|
188
|
+
/>
|
189
|
+
)}
|
190
|
+
</div>
|
191
|
+
<DidAddress responsive={false}>{session.user.did}</DidAddress>
|
192
|
+
</div>
|
193
|
+
{Array.isArray(menu) &&
|
194
|
+
menu.map((menuItem, index) => {
|
195
|
+
const { svgIcon, ...menuProps } = menuItem;
|
196
|
+
return (
|
197
|
+
<MenuItem
|
198
|
+
key={index}
|
199
|
+
className="session-manager-menu-item"
|
200
|
+
{...{
|
201
|
+
...menuProps,
|
202
|
+
icon: undefined,
|
203
|
+
label: undefined,
|
204
|
+
}}>
|
205
|
+
{svgIcon
|
206
|
+
? svgIcon && <SvgIcon component={svgIcon} className="session-manager-menu-icon" />
|
207
|
+
: menuItem.icon}
|
208
|
+
{menuItem.label}
|
209
|
+
</MenuItem>
|
210
|
+
);
|
211
|
+
})}
|
212
|
+
{menuRender({
|
213
|
+
classes: {
|
214
|
+
menuItem: 'session-manager-menu-item',
|
215
|
+
menuIcon: 'session-manager-menu-icon',
|
216
|
+
},
|
217
|
+
})}
|
218
|
+
{!browser.wallet && (
|
219
|
+
<MenuItem
|
220
|
+
component="a"
|
221
|
+
className="session-manager-menu-item"
|
222
|
+
data-cy="sessionManager-openInWallet"
|
223
|
+
href="https://www.abtwallet.io/"
|
224
|
+
target="_blank">
|
225
|
+
<SvgIcon component={OpenInIcon} className="session-manager-menu-icon" />
|
226
|
+
{message.openInWallet}
|
227
|
+
</MenuItem>
|
228
|
+
)}
|
229
|
+
{!!switchDid && (
|
230
|
+
<MenuItem
|
231
|
+
className="session-manager-menu-item"
|
232
|
+
onClick={_onSwitchDid}
|
233
|
+
data-cy="sessionManager-switch-trigger">
|
234
|
+
<SvgIcon component={SwitchDidIcon} className="session-manager-menu-icon" />
|
235
|
+
{message.switchDid}
|
236
|
+
</MenuItem>
|
237
|
+
)}
|
238
|
+
{!!switchProfile && (
|
239
|
+
<MenuItem
|
240
|
+
className="session-manager-menu-item"
|
241
|
+
onClick={_onSwitchProfile}
|
242
|
+
data-cy="sessionManager-switch-profile-trigger">
|
243
|
+
<SvgIcon component={SwitchProfileIcon} className="session-manager-menu-icon" />
|
244
|
+
{message.switchProfile}
|
245
|
+
</MenuItem>
|
246
|
+
)}
|
247
|
+
{!!switchPassport && (
|
248
|
+
<MenuItem
|
249
|
+
className="session-manager-menu-item"
|
250
|
+
onClick={_onSwitchPassport}
|
251
|
+
data-cy="sessionManager-switch-passport-trigger">
|
252
|
+
<SvgIcon component={SwitchPassportIcon} className="session-manager-menu-icon" />
|
253
|
+
{message.switchPassport}
|
254
|
+
</MenuItem>
|
255
|
+
)}
|
256
|
+
<MenuItem
|
257
|
+
className="session-manager-menu-item"
|
258
|
+
onClick={_onLogout}
|
259
|
+
disabled={disableLogout}
|
260
|
+
data-cy="sessionManager-logout-trigger">
|
261
|
+
<SvgIcon component={DisconnectIcon} className="session-manager-menu-icon" />
|
262
|
+
{message.disconnect}
|
263
|
+
</MenuItem>
|
264
|
+
</MenuList>
|
265
|
+
</ClickAwayListener>
|
266
|
+
</Paper>
|
267
|
+
</StyledPopper>
|
268
|
+
)}
|
269
|
+
</>
|
270
|
+
);
|
271
|
+
}
|
272
|
+
|
273
|
+
SessionManager.propTypes = {
|
274
|
+
session: PropTypes.shape({
|
275
|
+
user: PropTypes.shape({
|
276
|
+
did: PropTypes.string.isRequired,
|
277
|
+
role: PropTypes.string.isRequired,
|
278
|
+
fullName: PropTypes.string,
|
279
|
+
avatar: PropTypes.string,
|
280
|
+
passports: PropTypes.arrayOf(
|
281
|
+
PropTypes.shape({
|
282
|
+
name: PropTypes.string.isRequired,
|
283
|
+
title: PropTypes.string.isRequired,
|
284
|
+
})
|
285
|
+
),
|
286
|
+
}),
|
287
|
+
login: PropTypes.func.isRequired,
|
288
|
+
logout: PropTypes.func.isRequired,
|
289
|
+
switchDid: PropTypes.func.isRequired,
|
290
|
+
switchProfile: PropTypes.func.isRequired,
|
291
|
+
switchPassport: PropTypes.func.isRequired,
|
292
|
+
}).isRequired,
|
293
|
+
locale: PropTypes.string,
|
294
|
+
showText: PropTypes.bool,
|
295
|
+
showRole: PropTypes.bool,
|
296
|
+
switchDid: PropTypes.bool,
|
297
|
+
switchProfile: PropTypes.bool,
|
298
|
+
switchPassport: PropTypes.bool,
|
299
|
+
disableLogout: PropTypes.bool,
|
300
|
+
onLogin: PropTypes.func,
|
301
|
+
onLogout: PropTypes.func,
|
302
|
+
onSwitchDid: PropTypes.func,
|
303
|
+
onSwitchProfile: PropTypes.func,
|
304
|
+
onSwitchPassport: PropTypes.func,
|
305
|
+
menu: PropTypes.array,
|
306
|
+
menuRender: PropTypes.func,
|
307
|
+
dark: PropTypes.bool,
|
308
|
+
size: PropTypes.number,
|
309
|
+
};
|
310
|
+
|
311
|
+
const noop = () => {};
|
312
|
+
|
313
|
+
SessionManager.defaultProps = {
|
314
|
+
locale: 'en',
|
315
|
+
showText: false,
|
316
|
+
showRole: false,
|
317
|
+
switchDid: true,
|
318
|
+
switchProfile: true,
|
319
|
+
switchPassport: true,
|
320
|
+
disableLogout: false,
|
321
|
+
menu: [],
|
322
|
+
menuRender: noop,
|
323
|
+
onLogin: noop,
|
324
|
+
onLogout: noop,
|
325
|
+
onSwitchDid: noop,
|
326
|
+
onSwitchProfile: noop,
|
327
|
+
onSwitchPassport: noop,
|
328
|
+
dark: false,
|
329
|
+
size: 24,
|
330
|
+
};
|
331
|
+
|
332
|
+
const StyledPopper = styled(Popper)`
|
333
|
+
z-index: ${({ theme }) => theme.zIndex.tooltip};
|
334
|
+
.MuiList-root {
|
335
|
+
width: 280px;
|
336
|
+
}
|
337
|
+
.session-manager-user {
|
338
|
+
font-size: 12px;
|
339
|
+
flex-direction: column;
|
340
|
+
align-items: flex-start;
|
341
|
+
padding: 24px 24px 10px;
|
342
|
+
}
|
343
|
+
.session-manager-user-name {
|
344
|
+
font-size: 20px;
|
345
|
+
color: ${({ $dark }) => ($dark ? '#aaa' : '#222')};
|
346
|
+
font-weight: bold;
|
347
|
+
margin-bottom: 10px;
|
348
|
+
display: flex;
|
349
|
+
align-items: center;
|
350
|
+
justify-content: space-between;
|
351
|
+
}
|
352
|
+
.session-manager-menu-item {
|
353
|
+
padding: 18.5px 24px;
|
354
|
+
color: #777;
|
355
|
+
font-size: 16px;
|
356
|
+
&:hover {
|
357
|
+
background-color: #fbfbfb;
|
358
|
+
}
|
359
|
+
}
|
360
|
+
.session-manager-menu-icon {
|
361
|
+
color: #999;
|
362
|
+
margin-right: 16px;
|
363
|
+
}
|
364
|
+
`;
|
365
|
+
|
366
|
+
export default SessionManager;
|
@@ -0,0 +1,35 @@
|
|
1
|
+
/* eslint-disable import/prefer-default-export */
|
2
|
+
export const providerName = 'wallet_url';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* 获取 web wallet url, 常用于为 did connect 组件传递 webWalletUrl 参数,
|
6
|
+
* 更明确的说, 这里获取的应该是 **default web wallet url**,
|
7
|
+
* 如果用户使用自定义的 web wallet url, 不应该使用该方法, 应该显式的将自定义的 web wallet url 传递给 did connect 组件
|
8
|
+
* (参考: https://github.com/blocklet/ocap-playground/issues/98)
|
9
|
+
*
|
10
|
+
* 获取优先级:
|
11
|
+
* - localStorage 使用 provider 注册
|
12
|
+
* - window.env.webWalletUrl
|
13
|
+
* - window.blocklet.webWalletUrl
|
14
|
+
* - production web wallet url
|
15
|
+
*/
|
16
|
+
export const getWebWalletUrl = () => {
|
17
|
+
return (
|
18
|
+
window.localStorage.getItem(providerName) ||
|
19
|
+
window.env?.webWalletUrl ||
|
20
|
+
window.blocklet?.webWalletUrl ||
|
21
|
+
'https://web.abtwallet.io/'
|
22
|
+
);
|
23
|
+
};
|
24
|
+
|
25
|
+
// 检查 wallet url protocol 和当前页面地址的 protocol 是否一致
|
26
|
+
export const checkSameProtocol = (webWalletUrl) => {
|
27
|
+
const { protocol } = window.location;
|
28
|
+
let walletProtocol = '';
|
29
|
+
try {
|
30
|
+
walletProtocol = new URL(webWalletUrl).protocol;
|
31
|
+
} catch (error) {
|
32
|
+
walletProtocol = '';
|
33
|
+
}
|
34
|
+
return protocol === walletProtocol;
|
35
|
+
};
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import { useEffect } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import useIdle from 'react-use/lib/useIdle';
|
4
|
+
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
5
|
+
import useBrowser from '@arcblock/react-hooks/lib/useBrowser';
|
6
|
+
import { getWebWalletUrl, checkSameProtocol } from '../Util/wallet';
|
7
|
+
|
8
|
+
// 默认最大空闲时间: 30min
|
9
|
+
const DEFAULT_MAX_IDLE_TIME = 1000 * 60 * 30;
|
10
|
+
// 可使用 localStorage.setItem('wallet_sw_keeper_disabled', 1) 来禁用嵌入 wallet iframe
|
11
|
+
const STORAGE_KEY_DISABLED = 'wallet_sw_keeper_disabled';
|
12
|
+
// iframe id, 如果存在多个 WebWalletSWKeeper 组件实例, 共享此 id, 保证只有一个 iframe
|
13
|
+
let id;
|
14
|
+
|
15
|
+
const injectIframe = (webWalletUrl) => {
|
16
|
+
const iframe = document.createElement('iframe');
|
17
|
+
iframe.title = 'abt wallet';
|
18
|
+
iframe.id = id;
|
19
|
+
iframe.style.width = 0;
|
20
|
+
iframe.style.height = 0;
|
21
|
+
iframe.style.border = 0;
|
22
|
+
// https://stackoverflow.com/questions/27858989/iframe-with-0-height-creates-a-gap
|
23
|
+
iframe.style.display = 'block';
|
24
|
+
// fix: 页面自动滚动到底部问题 (https://github.com/blocklet/abt-wallet/issues/1160)
|
25
|
+
// top: 0 可能不是必须的, 但测试中发现, 如果不设置, 在某些特殊情况下似乎也会导致页面自动滚动到底部
|
26
|
+
iframe.style.position = 'absolute';
|
27
|
+
iframe.style.top = 0;
|
28
|
+
iframe.src = `${webWalletUrl}?action=iframe`;
|
29
|
+
document.body.appendChild(iframe);
|
30
|
+
};
|
31
|
+
|
32
|
+
const removeIframe = () => {
|
33
|
+
const iframe = document.getElementById(id);
|
34
|
+
if (iframe) {
|
35
|
+
document.body.removeChild(iframe);
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
const cleanup = () => {
|
40
|
+
removeIframe();
|
41
|
+
id = null;
|
42
|
+
};
|
43
|
+
|
44
|
+
const enable = (webWalletUrl) => {
|
45
|
+
if (!id) {
|
46
|
+
id = `web_wallet_sw_keeper_${Date.now()}`;
|
47
|
+
injectIframe(webWalletUrl);
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
// 该组件通过嵌入一个 web wallet iframe 帮助 web wallet service worker 延最大空闲时间
|
52
|
+
function WebWalletSWKeeper({ webWalletUrl, maxIdleTime }) {
|
53
|
+
const isIdle = useIdle(maxIdleTime);
|
54
|
+
// 用户操作空闲时间超过 maxIdleTime 时禁用, 活跃时启用
|
55
|
+
useEffect(() => {
|
56
|
+
if (isIdle) {
|
57
|
+
cleanup();
|
58
|
+
} else {
|
59
|
+
enable(webWalletUrl);
|
60
|
+
}
|
61
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
62
|
+
}, [isIdle]);
|
63
|
+
// 组件销毁时自动清理
|
64
|
+
useEffect(() => () => cleanup(), []);
|
65
|
+
return null;
|
66
|
+
}
|
67
|
+
|
68
|
+
WebWalletSWKeeper.propTypes = {
|
69
|
+
webWalletUrl: PropTypes.string.isRequired,
|
70
|
+
maxIdleTime: PropTypes.number,
|
71
|
+
};
|
72
|
+
|
73
|
+
WebWalletSWKeeper.defaultProps = {
|
74
|
+
maxIdleTime: DEFAULT_MAX_IDLE_TIME,
|
75
|
+
};
|
76
|
+
|
77
|
+
export default WebWalletSWKeeper;
|
78
|
+
|
79
|
+
export const withWebWalletSWKeeper = (Component) => {
|
80
|
+
// eslint-disable-next-line react/prop-types
|
81
|
+
return function WithWebWalletSWKeeperComponent({ webWalletUrl, maxIdleTime, ...rest }) {
|
82
|
+
const browser = useBrowser();
|
83
|
+
// eslint-disable-next-line no-param-reassign
|
84
|
+
webWalletUrl = webWalletUrl || getWebWalletUrl();
|
85
|
+
const [disabled] = useLocalStorage(STORAGE_KEY_DISABLED);
|
86
|
+
const webWalletExtension = window.ABT_DEV || window.ABT;
|
87
|
+
const isSameProtocol = checkSameProtocol(webWalletUrl);
|
88
|
+
const isWalletWebview = browser.wallet;
|
89
|
+
// 以下几种情况不会嵌入 wallet iframe :
|
90
|
+
// - 通过设置 localStorage#wallet_sw_keeper_disabled = 1 明确禁止 (开发调试过程中可以使用, 避免控制台打印一堆日志影响调试)
|
91
|
+
// - 检查到 wallet 浏览器插件
|
92
|
+
// - webWalletUrl 与当前页面 url 的 protocol 不同
|
93
|
+
// - wallet webview
|
94
|
+
return (
|
95
|
+
<>
|
96
|
+
{!disabled && !webWalletExtension && isSameProtocol && !isWalletWebview && (
|
97
|
+
<WebWalletSWKeeper webWalletUrl={webWalletUrl} maxIdleTime={maxIdleTime} />
|
98
|
+
)}
|
99
|
+
<Component webWalletUrl={webWalletUrl} {...rest} />
|
100
|
+
</>
|
101
|
+
);
|
102
|
+
};
|
103
|
+
};
|