@arcblock/ux 2.4.43 → 2.4.45

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.
@@ -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;
@@ -75,9 +75,11 @@ const LightBox = styled('div')`
75
75
  text-decoration: underline;
76
76
  }
77
77
 
78
- .copy-button {
79
- > .default-text {
80
- color: #222;
78
+ pre {
79
+ .copy-button {
80
+ > .default-text {
81
+ color: #222;
82
+ }
81
83
  }
82
84
  }
83
85
  `;
@@ -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
+ };