@arcblock/ux 2.13.27 → 2.13.29

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.
Files changed (33) hide show
  1. package/lib/Config/config-provider.d.ts +1 -0
  2. package/lib/DIDConnect/did-connect-container.d.ts +2 -1
  3. package/lib/DIDConnect/did-connect-container.js +4 -2
  4. package/lib/DIDConnect/index.d.ts +1 -0
  5. package/lib/DIDConnect/index.js +2 -1
  6. package/lib/DIDConnect/request-storage-access-api-dialog.d.ts +12 -0
  7. package/lib/DIDConnect/request-storage-access-api-dialog.js +325 -0
  8. package/lib/RelativeTime/index.d.ts +2 -1
  9. package/lib/RelativeTime/index.js +11 -5
  10. package/lib/SessionUser/components/un-login.js +29 -37
  11. package/lib/SharedBridge/index.d.ts +5 -6
  12. package/lib/SharedBridge/index.js +34 -48
  13. package/lib/Tabs/index.js +2 -2
  14. package/lib/Theme/theme-provider.d.ts +5 -2
  15. package/lib/Theme/theme-provider.js +28 -9
  16. package/lib/Theme/theme.d.ts +6 -4
  17. package/lib/Theme/theme.js +15 -9
  18. package/package.json +6 -6
  19. package/src/DIDConnect/did-connect-container.tsx +4 -1
  20. package/src/DIDConnect/index.ts +1 -0
  21. package/src/DIDConnect/request-storage-access-api-dialog.tsx +280 -0
  22. package/src/RelativeTime/index.tsx +25 -4
  23. package/src/SessionUser/components/un-login.tsx +21 -29
  24. package/src/SharedBridge/index.tsx +91 -97
  25. package/src/Tabs/index.tsx +2 -2
  26. package/src/Theme/theme-provider.tsx +36 -10
  27. package/src/Theme/theme.ts +21 -15
  28. package/lib/LoginButton/index.d.ts +0 -12
  29. package/lib/LoginButton/index.js +0 -74
  30. package/lib/SharedBridge/need-storage-access-api-dialog.d.ts +0 -6
  31. package/lib/SharedBridge/need-storage-access-api-dialog.js +0 -191
  32. package/src/LoginButton/index.tsx +0 -73
  33. package/src/SharedBridge/need-storage-access-api-dialog.tsx +0 -149
@@ -1,28 +1,27 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box } from '@mui/material';
3
- import React, { memo, useEffect, useId, useRef } from 'react';
3
+ import { forwardRef, memo, useEffect, useId, useImperativeHandle, useRef } from 'react';
4
4
  import { withQuery } from 'ufo';
5
5
  import { useMemoizedFn, useReactive } from 'ahooks';
6
+ import noop from 'lodash/noop';
6
7
  import { mergeSx } from '../Util/style';
7
8
  import { callIframe, getCallbackAction } from '../Util/iframe';
8
- import NeedStorageAccessApiDialog from './need-storage-access-api-dialog';
9
- import { DIDConnectContainer } from '../DIDConnect';
10
- const SharedBridge = /*#__PURE__*/memo(function SharedBridge({
9
+ const SharedBridge = /*#__PURE__*/forwardRef(({
11
10
  src,
12
11
  onClick,
13
- onLoad,
12
+ onLoad = noop,
14
13
  sx,
15
- iframeRef,
16
14
  locale = 'en',
17
15
  ...rest
18
- }) {
19
- const _iframeRef = useRef(null);
16
+ }, ref) => {
17
+ const targetIframeRef = useRef(null);
20
18
  const refId = useId();
21
19
  const dataId = `shared-bridge_${refId}`;
22
20
  const currentState = useReactive({
23
21
  hasInited: undefined,
24
22
  open: false,
25
23
  hasStorageAccess: false,
24
+ containerEl: null,
26
25
  get origin() {
27
26
  try {
28
27
  return new URL(src).origin;
@@ -38,7 +37,6 @@ const SharedBridge = /*#__PURE__*/memo(function SharedBridge({
38
37
  }
39
38
  }
40
39
  });
41
- const targetIframeRef = iframeRef ?? _iframeRef;
42
40
  useEffect(() => {
43
41
  async function handleMessage(event) {
44
42
  const {
@@ -74,6 +72,7 @@ const SharedBridge = /*#__PURE__*/memo(function SharedBridge({
74
72
  currentState.hasStorageAccess = value;
75
73
  currentState.hasInited = true;
76
74
  });
75
+ // HACK: 如果目标 bridge 1s 内没有初始化,则认为目标 bridge 不兼容,不进行后续内容的加载
77
76
  setTimeout(() => {
78
77
  if (currentState.hasInited === undefined) {
79
78
  currentState.hasInited = false;
@@ -81,43 +80,30 @@ const SharedBridge = /*#__PURE__*/memo(function SharedBridge({
81
80
  }, 1000);
82
81
  onLoad();
83
82
  });
84
- if (currentState.hasInited === false) {
85
- return null;
86
- }
87
- if (currentState.hasStorageAccess) {
88
- return null;
89
- }
90
- return /*#__PURE__*/_jsxs(_Fragment, {
91
- children: [/*#__PURE__*/_jsx(DIDConnectContainer, {
92
- popup: true,
93
- hideCloseButton: true,
94
- open: currentState.open,
95
- children: /*#__PURE__*/_jsx(NeedStorageAccessApiDialog, {
96
- locale: locale,
97
- origin: currentState.origin,
98
- host: currentState.host
99
- })
100
- }), /*#__PURE__*/_jsx(Box, {
101
- ...rest,
102
- component: "iframe",
103
- ref: targetIframeRef,
104
- onLoad: handleLoad,
105
- title: "shared-bridge",
106
- "data-id": dataId,
107
- src: withQuery(src, {
108
- id: dataId
109
- }),
110
- sx: mergeSx({
111
- border: 0,
112
- position: 'absolute',
113
- top: 0,
114
- left: 0,
115
- width: '100%',
116
- height: '100%',
117
- cursor: 'pointer',
118
- opacity: 0
119
- }, sx)
120
- })]
83
+ useImperativeHandle(ref, () => ({
84
+ callIframe(action) {
85
+ return callIframe(targetIframeRef.current, action);
86
+ }
87
+ }));
88
+ return /*#__PURE__*/_jsx(Box, {
89
+ ...rest,
90
+ component: "iframe",
91
+ ref: targetIframeRef,
92
+ onLoad: handleLoad,
93
+ "data-id": dataId,
94
+ src: withQuery(src, {
95
+ id: dataId
96
+ }),
97
+ sx: mergeSx({
98
+ border: 0,
99
+ position: 'absolute',
100
+ top: 0,
101
+ left: 0,
102
+ width: '100%',
103
+ height: '100%',
104
+ cursor: 'pointer',
105
+ backgroundColor: 'transparent'
106
+ }, sx)
121
107
  });
122
108
  });
123
- export default SharedBridge;
109
+ export default /*#__PURE__*/memo(SharedBridge);
package/lib/Tabs/index.js CHANGED
@@ -41,7 +41,7 @@ function CardTabs({
41
41
  },
42
42
  '.MuiTabs-flexContainer': {
43
43
  borderRadius: '100vw',
44
- backgroundColor: 'grey.200',
44
+ backgroundColor: 'grey.100',
45
45
  padding: 0.5,
46
46
  display: 'inline-flex',
47
47
  columnGap: 0.25,
@@ -60,7 +60,7 @@ function CardTabs({
60
60
  textTransform: 'capitalize',
61
61
  transition: 'background-color 0.2s ease',
62
62
  '&.Mui-selected, &:hover': {
63
- backgroundColor: 'white',
63
+ backgroundColor: 'action.selected',
64
64
  borderColor: 'grey.100',
65
65
  color: 'grey.A700'
66
66
  }
@@ -1,15 +1,18 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import { PaletteMode } from '@mui/material';
3
- import { Theme } from '@mui/material/styles';
3
+ import { Theme, ThemeOptions } from '@mui/material/styles';
4
4
  /** 颜色模式上下文类型 */
5
5
  export interface ColorSchemeContextType {
6
6
  mode: PaletteMode;
7
7
  toggleMode: () => void;
8
+ changeMode: (mode: PaletteMode) => void;
8
9
  prefer?: Prefer;
9
10
  }
10
11
  export declare const ColorSchemeContext: import("react").Context<ColorSchemeContextType>;
11
12
  export declare function useColorScheme(): ColorSchemeContextType;
12
- export type UxTheme = Partial<Theme> | ((outerTheme: Partial<Theme>) => Theme);
13
+ export type UxTheme = ThemeOptions | ((parentTheme: Theme, context?: {
14
+ mode: PaletteMode;
15
+ }) => Theme);
13
16
  export type Prefer = 'light' | 'dark' | 'system';
14
17
  interface BaseThemeProviderProps {
15
18
  children?: React.ReactNode;
@@ -7,7 +7,7 @@ import StyledEngineProvider from '@mui/material/StyledEngineProvider';
7
7
  import CssBaseline from '@mui/material/CssBaseline';
8
8
  import set from 'lodash/set';
9
9
  import { BLOCKLET_THEME_PREFER_KEY } from '@blocklet/theme';
10
- import { createTheme, getDefaultThemePrefer, isTheme, lazyThemeConfig } from './theme';
10
+ import { createTheme, getDefaultThemePrefer, isTheme, isUxTheme, lazyCreateDefaultTheme } from './theme';
11
11
  const defaultTheme = createTheme();
12
12
 
13
13
  /** 颜色模式上下文类型 */
@@ -83,7 +83,7 @@ function DarkSchemeStyles({
83
83
  }
84
84
  return null;
85
85
  }
86
- /** 基础的 theme provider, 可以为 webapp/blocklet 快捷的配置好 mui theme provider */
86
+ /** 快速配置 MUI 主题 */
87
87
  function BaseThemeProvider({
88
88
  children,
89
89
  theme = defaultTheme,
@@ -110,7 +110,7 @@ function BaseThemeProvider({
110
110
  })
111
111
  );
112
112
  }
113
- /** 带颜色模式切换功能的 theme provider */
113
+ /** 配置带颜色模式切换功能的 MUI 主题 */
114
114
  function ColorSchemeProvider({
115
115
  children,
116
116
  theme: themeInput,
@@ -119,14 +119,26 @@ function ColorSchemeProvider({
119
119
  ...rest
120
120
  }) {
121
121
  const [mode, setMode] = useState(() => resolveMode(prefer));
122
+ const parentTheme = useTheme();
122
123
  const _themeInput = useMemo(() => {
123
124
  let result = {};
124
- const getThemeConfig = lazyThemeConfig(mode);
125
+ const createBaseTheme = lazyCreateDefaultTheme(mode);
125
126
  if (themeInput) {
126
127
  if (typeof themeInput === 'function') {
127
- result = {
128
- ...themeInput(getThemeConfig())
129
- };
128
+ const baseTheme = createBaseTheme();
129
+ if (isUxTheme(parentTheme)) {
130
+ result = {
131
+ ...themeInput(parentTheme, {
132
+ mode
133
+ })
134
+ };
135
+ } else {
136
+ result = {
137
+ ...themeInput(baseTheme, {
138
+ mode
139
+ })
140
+ };
141
+ }
130
142
  } else {
131
143
  result = {
132
144
  ...themeInput
@@ -136,7 +148,7 @@ function ColorSchemeProvider({
136
148
  set(result, 'palette.mode', mode);
137
149
  set(result, 'mode', mode);
138
150
  return result;
139
- }, [mode, themeInput]);
151
+ }, [mode, themeInput, parentTheme]);
140
152
  const theme = useMemo(() => {
141
153
  return createTheme({
142
154
  ..._themeInput,
@@ -150,11 +162,18 @@ function ColorSchemeProvider({
150
162
  setMode(newMode);
151
163
  localStorage.setItem(BLOCKLET_THEME_PREFER_KEY, newMode);
152
164
  }, [mode, setMode]);
165
+ const changeMode = useCallback(newMode => {
166
+ if (mode !== newMode) {
167
+ setMode(newMode);
168
+ localStorage.setItem(BLOCKLET_THEME_PREFER_KEY, newMode);
169
+ }
170
+ }, [mode, setMode]);
153
171
  const colorSchemeValue = useMemo(() => ({
154
172
  mode,
155
173
  toggleMode,
174
+ changeMode,
156
175
  prefer
157
- }), [mode, prefer, toggleMode]);
176
+ }), [mode, prefer, toggleMode, changeMode]);
158
177
  useEffect(() => {
159
178
  if (prefer) {
160
179
  setMode(resolveMode(prefer));
@@ -9,6 +9,8 @@ import '@fontsource/roboto/latin-ext-500.css';
9
9
  import '@fontsource/roboto/latin-ext-700.css';
10
10
  /** 是否是 MUI Theme 对象 */
11
11
  export declare function isTheme(obj: any): obj is Theme;
12
+ /** 是否是 UX Theme 对象 */
13
+ export declare function isUxTheme(obj: any): obj is Theme;
12
14
  export declare function collectFontFamilies(obj?: {
13
15
  fontFamily?: string;
14
16
  }, fontSet?: Set<string>): Set<string>;
@@ -19,10 +21,10 @@ export declare function getDefaultThemePrefer(meta?: {
19
21
  };
20
22
  }): PaletteMode;
21
23
  export declare function createDefaultThemeOptions(mode?: PaletteMode): ThemeOptions;
22
- export interface UserThemeOptions extends ThemeOptions {
24
+ export interface UxThemeOptions extends ThemeOptions {
23
25
  disableBlockletTheme?: boolean;
24
26
  }
25
- export declare function lazyThemeConfig(mode: PaletteMode): () => Partial<Theme>;
26
- export declare const create: (...args: Array<UserThemeOptions | ((config: Partial<Theme>) => UserThemeOptions)>) => Theme;
27
- export declare const createTheme: (...args: Array<UserThemeOptions | ((config: Partial<Theme>) => UserThemeOptions)>) => Theme;
27
+ export declare function lazyCreateDefaultTheme(mode: PaletteMode): () => Theme;
28
+ export declare const create: (...args: Array<UxThemeOptions | ((baseTheme: Theme) => UxThemeOptions)>) => Theme;
29
+ export declare const createTheme: (...args: Array<UxThemeOptions | ((baseTheme: Theme) => UxThemeOptions)>) => Theme;
28
30
  export { deepmerge };
@@ -20,6 +20,11 @@ export function isTheme(obj) {
20
20
  return obj && typeof obj === 'object' && obj.palette && typeof obj.palette.getContrastText === 'function';
21
21
  }
22
22
 
23
+ /** 是否是 UX Theme 对象 */
24
+ export function isUxTheme(obj) {
25
+ return isTheme(obj) && obj.__isUxTheme__ === true;
26
+ }
27
+
23
28
  // 收集字体配置
24
29
  export function collectFontFamilies(obj, fontSet = new Set()) {
25
30
  if (!obj || typeof obj !== 'object') return fontSet;
@@ -94,13 +99,13 @@ export function createDefaultThemeOptions(mode = 'light') {
94
99
  }
95
100
  return BLOCKLET_THEME_LIGHT;
96
101
  }
97
- // 用于获取 Blocklet Theme 配置,便于用户创建自定义主题
98
- export function lazyThemeConfig(mode) {
99
- let config = null;
102
+ export function lazyCreateDefaultTheme(mode) {
103
+ let theme = null;
100
104
  return () => {
101
- if (config) return config;
102
- config = deepmerge(createDefaultThemeOptions(mode), window.blocklet?.theme?.[mode] ?? {});
103
- return config;
105
+ if (theme) return theme;
106
+ const options = deepmerge(createDefaultThemeOptions(mode), window.blocklet?.theme?.[mode] ?? {});
107
+ theme = _createTheme(options);
108
+ return theme;
104
109
  };
105
110
  }
106
111
 
@@ -121,7 +126,7 @@ const normalizeUserThemeOptions = ({
121
126
  };
122
127
  return result;
123
128
  };
124
- const defaultUserThemeOptions = {
129
+ const defaultUxThemeOptions = {
125
130
  themeName: 'ArcBlock',
126
131
  pageWidth: 'md',
127
132
  disableBlockletTheme: false,
@@ -151,8 +156,8 @@ const defaultUserThemeOptions = {
151
156
  // https://material-ui.com/customization/default-theme/
152
157
  export const create = (...args) => {
153
158
  const defaultPrefer = getDefaultThemePrefer();
154
- const getThemeConfig = lazyThemeConfig(defaultPrefer);
155
- const userThemeOptions = args.reduce((acc, curr) => deepmerge(acc, normalizeUserThemeOptions(typeof curr === 'function' ? curr(getThemeConfig()) : curr)), normalizeUserThemeOptions(defaultUserThemeOptions));
159
+ const createBaseTheme = lazyCreateDefaultTheme(defaultPrefer);
160
+ const userThemeOptions = args.reduce((acc, curr) => deepmerge(acc, normalizeUserThemeOptions(typeof curr === 'function' ? curr(createBaseTheme()) : curr)), normalizeUserThemeOptions(defaultUxThemeOptions));
156
161
  const prefer = userThemeOptions.mode || userThemeOptions.palette?.mode || defaultPrefer;
157
162
  const blockletThemeOptions = window.blocklet?.theme?.[prefer] ?? {};
158
163
  const defaultThemeOptions = createDefaultThemeOptions(prefer);
@@ -172,6 +177,7 @@ export const create = (...args) => {
172
177
 
173
178
  // 创建主题
174
179
  const theme = _createTheme(mergedThemeOptions);
180
+ theme.__isUxTheme__ = true;
175
181
 
176
182
  // 异步加载字体
177
183
  const fonts = collectFontFamilies(theme.typography);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.13.27",
3
+ "version": "2.13.29",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -71,14 +71,14 @@
71
71
  "react": ">=18.2.0",
72
72
  "react-router-dom": ">=6.22.3"
73
73
  },
74
- "gitHead": "06abd7d8f9068ebca145a411b4a9b5b7f329535a",
74
+ "gitHead": "98e08de2a4ec51ae6181b57e2c6cf175eabd92ff",
75
75
  "dependencies": {
76
76
  "@arcblock/did-motif": "^1.1.13",
77
- "@arcblock/icons": "^2.13.27",
78
- "@arcblock/nft-display": "^2.13.27",
79
- "@arcblock/react-hooks": "^2.13.27",
77
+ "@arcblock/icons": "^2.13.29",
78
+ "@arcblock/nft-display": "^2.13.29",
79
+ "@arcblock/react-hooks": "^2.13.29",
80
80
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
81
- "@blocklet/theme": "^2.13.27",
81
+ "@blocklet/theme": "^2.13.29",
82
82
  "@fontsource/roboto": "~5.1.1",
83
83
  "@fontsource/ubuntu-mono": "^5.0.18",
84
84
  "@iconify-icons/logos": "^1.2.36",
@@ -37,6 +37,7 @@ export default function DIDConnectContainer({
37
37
  appPid,
38
38
  slotProps,
39
39
  onClose = noop,
40
+ keepMounted = false,
40
41
  }: {
41
42
  // 是否弹出显示, true 表示在 Dialog 中渲染, 并可以通过 open/onClose 控制 dialog 的显示/隐藏, false 表示直接渲染原内容
42
43
  popup?: boolean;
@@ -59,6 +60,7 @@ export default function DIDConnectContainer({
59
60
  sx?: SxProps;
60
61
  };
61
62
  };
63
+ keepMounted?: boolean;
62
64
  }) {
63
65
  const color = useCreation(() => {
64
66
  const did = appPid || window.blocklet.appPid;
@@ -96,7 +98,7 @@ export default function DIDConnectContainer({
96
98
  onClose();
97
99
  };
98
100
 
99
- const showModal = debouncedOpen || open;
101
+ const showModal = debouncedOpen || open || keepMounted;
100
102
 
101
103
  const DrawerComponent = hideCloseButton ? Drawer : SwipeableDrawer;
102
104
 
@@ -286,6 +288,7 @@ export default function DIDConnectContainer({
286
288
  slots={{
287
289
  backdrop: BackdropWrap,
288
290
  }}
291
+ keepMounted={keepMounted}
289
292
  className="did-connect__container-dialog"
290
293
  onClose={handleOnClose}
291
294
  PaperProps={{
@@ -6,3 +6,4 @@ export { default as withContainer } from './with-container';
6
6
  export { default as withUxTheme } from './with-ux-theme';
7
7
  export { default as DIDConnectLogo } from './did-connect-logo';
8
8
  export { default as DIDConnectContainer } from './did-connect-container';
9
+ export { default as RequestStorageAccessApiDialog } from './request-storage-access-api-dialog';
@@ -0,0 +1,280 @@
1
+ import { Box, Button, Typography, Chip, List, ListItem } from '@mui/material';
2
+ import { Icon } from '@iconify/react';
3
+ import externalLinkIcon from '@iconify-icons/tabler/external-link';
4
+ import lockOutlineIcon from '@iconify-icons/material-symbols/lock-outline';
5
+ import checkCircleIcon from '@iconify-icons/material-symbols/check-circle';
6
+ import rocketLaunchRoundedIcon from '@iconify-icons/material-symbols/rocket-launch-rounded';
7
+ import { useMemoizedFn, useReactive } from 'ahooks';
8
+ import { forwardRef, memo, useImperativeHandle, useRef } from 'react';
9
+ import noop from 'lodash/noop';
10
+
11
+ import { Locale } from '../type';
12
+ import { translate } from '../Locale/util';
13
+ import DIDConnectContainer from './did-connect-container';
14
+ import CloseButton from '../CloseButton';
15
+ import SharedBridge from '../SharedBridge';
16
+ import { setVisitorId } from '../Util';
17
+
18
+ type StorageAccessState = 'prompt' | 'granted' | 'denied';
19
+
20
+ const translations: Record<
21
+ Locale,
22
+ {
23
+ allow: string;
24
+ dataUsage: string;
25
+ title: string;
26
+ clickAllow: ({ allowButton }: { allowButton: React.ReactNode }) => React.ReactNode;
27
+ reason: ({ site }: { site: React.ReactNode }) => React.ReactNode;
28
+ afterAllow: {
29
+ title: string;
30
+ list1: string;
31
+ list2: string;
32
+ };
33
+ }
34
+ > = {
35
+ en: {
36
+ allow: 'Allow',
37
+ dataUsage:
38
+ 'Your data is only used for identity authentication, and will not be collected or used for any other purpose.',
39
+ title: 'Authorization Request',
40
+ clickAllow: ({ allowButton }: { allowButton: React.ReactNode }) => {
41
+ return <>You only need to click the {allowButton} button below, and you will not see this request again.</>;
42
+ },
43
+ reason: ({ site }) => {
44
+ return <>For a better login experience, we need to apply for the storage permission of the {site} site.</>;
45
+ },
46
+ afterAllow: {
47
+ title: 'After authorization, you will enjoy:',
48
+ list1: 'More convenient login experience',
49
+ list2: 'Faster access speed',
50
+ },
51
+ },
52
+ zh: {
53
+ allow: '允许',
54
+ dataUsage: '您的数据仅用于身份认证,不会被收集或用于其他用途。',
55
+ title: '授权请求',
56
+ clickAllow: ({ allowButton }) => {
57
+ return <>您只需要点击下方的 {allowButton} 按钮,后续将不会再看到这个请求。</>;
58
+ },
59
+ reason: ({ site }) => {
60
+ return <>为了让您获得更好的登录体验,我们需要申请 {site} 站点存储权限。</>;
61
+ },
62
+ afterAllow: {
63
+ title: '授权后,您将享受:',
64
+ list1: '更便捷的登录体验',
65
+ list2: '更快的访问速度',
66
+ },
67
+ },
68
+ };
69
+
70
+ const RequestStorageAccessApiDialog = forwardRef(
71
+ (
72
+ {
73
+ locale = 'en',
74
+ src,
75
+ storageAccessState,
76
+ onHandle = noop,
77
+ }: {
78
+ locale?: Locale;
79
+ src: string;
80
+ storageAccessState: StorageAccessState;
81
+ onHandle: (data: { value: boolean; error?: Error }) => void;
82
+ },
83
+ ref
84
+ ) => {
85
+ const sharedBridgeRef = useRef<{ callIframe: (method: string) => Promise<any> }>(null);
86
+ const currentState = useReactive({
87
+ callback: noop,
88
+ open: false,
89
+ get origin() {
90
+ try {
91
+ return new URL(src).origin;
92
+ } catch (error) {
93
+ return src;
94
+ }
95
+ },
96
+ get host() {
97
+ try {
98
+ return new URL(src).host;
99
+ } catch (error) {
100
+ return src;
101
+ }
102
+ },
103
+ });
104
+ const t = useMemoizedFn((key, data = {}) => {
105
+ return translate(translations, key, locale, 'en', data);
106
+ });
107
+
108
+ const reset = useMemoizedFn(() => {
109
+ currentState.open = false;
110
+ currentState.callback = noop;
111
+ });
112
+
113
+ const open = useMemoizedFn(() => {
114
+ currentState.open = true;
115
+ });
116
+ const close = useMemoizedFn(() => {
117
+ currentState.open = false;
118
+ });
119
+
120
+ const _requestStorageAccess = useMemoizedFn(
121
+ async (callback: (result: boolean, origin?: 'broswer' | 'system') => void) => {
122
+ const result = await sharedBridgeRef.current?.callIframe('requestStorageAccess');
123
+ if (result.value) {
124
+ callback(true, 'broswer');
125
+ } else if (storageAccessState === undefined) {
126
+ // 用户未在当前网页交互过,此时必然需要进行一次弹窗
127
+ callback(false, 'system');
128
+ } else if (storageAccessState === 'denied') {
129
+ // 用户已经明确拒绝过跨站授权,直接弹窗
130
+ callback(false);
131
+ } else {
132
+ // NOTICE: 暂时保留这部分逻辑,后续可能会用上
133
+ // else if (
134
+ // result.error?.name === 'NotAllowedError' &&
135
+ // result.error?.message === 'requestStorageAccess not allowed'
136
+ // ) {
137
+ // // 操作被浏览器禁止,需要用户手动与 iframe 交互一次才可以
138
+ // currentState.open = true;
139
+ // currentState.callback = callback;
140
+ // }
141
+ currentState.open = true;
142
+ currentState.callback = callback;
143
+ }
144
+ }
145
+ );
146
+ const requestStorageAccess = useMemoizedFn(() => {
147
+ return new Promise((resolve) => {
148
+ _requestStorageAccess((result, origin) => {
149
+ reset();
150
+ resolve({ value: result, origin });
151
+ });
152
+ });
153
+ });
154
+
155
+ const handleAllow = useMemoizedFn(() => {
156
+ currentState.open = false;
157
+ });
158
+ const handleClose = useMemoizedFn(() => {
159
+ currentState.open = false;
160
+ currentState.callback(false, 'system');
161
+ });
162
+
163
+ useImperativeHandle(ref, () => ({
164
+ open,
165
+ close,
166
+ requestStorageAccess,
167
+ }));
168
+
169
+ return (
170
+ <DIDConnectContainer popup open={currentState.open} onClose={handleClose} keepMounted>
171
+ <Box
172
+ sx={{
173
+ backgroundColor: 'background.default',
174
+ display: 'flex',
175
+ flexDirection: 'column',
176
+ height: '100%',
177
+ position: 'relative',
178
+ maxWidth: '100%',
179
+ transition: 'width 0.2s ease-in-out',
180
+ margin: 'auto',
181
+ p: 3,
182
+ gap: 2,
183
+ }}>
184
+ <CloseButton
185
+ onClose={handleClose}
186
+ sx={{
187
+ position: 'absolute',
188
+ right: 14,
189
+ top: 14,
190
+ }}
191
+ />
192
+ <Typography
193
+ component="div"
194
+ variant="h4"
195
+ sx={{
196
+ fontWeight: 700,
197
+ fontFamily: 'Lexend',
198
+ display: 'flex',
199
+ alignItems: 'center',
200
+ gap: 1,
201
+ }}>
202
+ <Box component={Icon} icon={lockOutlineIcon} fontSize={28} sx={{ color: 'warning.main' }} />
203
+ {t('title')}
204
+ </Typography>
205
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
206
+ {/* 不需要随意更改以下内容的格式化,否则会影响到 UI 的展示 */}
207
+ <Typography>
208
+ {t('reason', {
209
+ site: (
210
+ <Chip
211
+ clickable
212
+ component="a"
213
+ href={currentState.origin}
214
+ label={currentState.host}
215
+ size="small"
216
+ deleteIcon={<Icon icon={externalLinkIcon} />}
217
+ onDelete={() => {}}
218
+ target="_blank"
219
+ />
220
+ ),
221
+ })}
222
+ </Typography>
223
+ <Typography component="div">
224
+ {t('clickAllow', {
225
+ allowButton: <Chip label={t('allow')} size="small" color="success" />,
226
+ })}
227
+ </Typography>
228
+ <Box sx={{ mt: 2 }}>
229
+ <Typography sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
230
+ <Box component={Icon} icon={checkCircleIcon} fontSize={24} sx={{ color: 'success.main' }} />
231
+ {t('afterAllow.title')}
232
+ </Typography>
233
+ <List dense sx={{ py: 0, pl: 2 }}>
234
+ <ListItem sx={{ display: 'flex', alignItems: 'center', gap: 0.8 }}>
235
+ <Box component={Icon} icon={rocketLaunchRoundedIcon} fontSize={20} sx={{ color: 'success.main' }} />
236
+ {t('afterAllow.list1')}
237
+ </ListItem>
238
+ <ListItem sx={{ display: 'flex', alignItems: 'center', gap: 0.8 }}>
239
+ <Box component={Icon} icon={rocketLaunchRoundedIcon} fontSize={20} sx={{ color: 'success.main' }} />
240
+ {t('afterAllow.list2')}
241
+ </ListItem>
242
+ </List>
243
+ </Box>
244
+
245
+ <Typography component="div" variant="body2" color="grey.700">
246
+ {t('dataUsage')}
247
+ </Typography>
248
+ </Box>
249
+
250
+ <Box sx={{ display: 'flex', justifyContent: 'center' }}>
251
+ <Box sx={{ position: 'relative', display: 'inline-block', cursor: 'pointer' }}>
252
+ <Button
253
+ variant="contained"
254
+ color="success"
255
+ onClick={handleAllow}
256
+ sx={{ minWidth: 100, letterSpacing: 1 }}>
257
+ {t('allow')}
258
+ </Button>
259
+ <SharedBridge
260
+ ref={sharedBridgeRef}
261
+ onClick={({ value, visitorId }) => {
262
+ if (visitorId) setVisitorId(visitorId);
263
+
264
+ currentState.callback(value, 'broswer');
265
+ }}
266
+ locale={locale}
267
+ src={src}
268
+ sx={{
269
+ zIndex: 1,
270
+ }}
271
+ />
272
+ </Box>
273
+ </Box>
274
+ </Box>
275
+ </DIDConnectContainer>
276
+ );
277
+ }
278
+ );
279
+
280
+ export default memo(RequestStorageAccessApiDialog);