@arcblock/ux 2.13.13 → 2.13.14

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 (55) hide show
  1. package/lib/Address/responsive-did-address.js +3 -1
  2. package/lib/DIDConnect/app-icon.d.ts +8 -0
  3. package/lib/DIDConnect/app-icon.js +31 -0
  4. package/lib/DIDConnect/app-info-item.d.ts +7 -0
  5. package/lib/DIDConnect/app-info-item.js +73 -0
  6. package/lib/DIDConnect/did-connect-footer.d.ts +4 -0
  7. package/lib/DIDConnect/did-connect-footer.js +54 -0
  8. package/lib/DIDConnect/did-connect-logo.d.ts +1 -0
  9. package/lib/DIDConnect/did-connect-logo.js +11 -0
  10. package/lib/DIDConnect/index.d.ts +7 -0
  11. package/lib/DIDConnect/index.js +7 -0
  12. package/lib/DIDConnect/powered-by.d.ts +3 -0
  13. package/lib/DIDConnect/powered-by.js +46 -0
  14. package/lib/DIDConnect/with-container.d.ts +11 -0
  15. package/lib/DIDConnect/with-container.js +273 -0
  16. package/lib/DIDConnect/with-ux-theme.d.ts +1 -0
  17. package/lib/DIDConnect/with-ux-theme.js +23 -0
  18. package/lib/Dialog/confirm.d.ts +6 -1
  19. package/lib/Dialog/confirm.js +7 -3
  20. package/lib/Dialog/use-confirm.js +6 -0
  21. package/lib/Locale/util.d.ts +3 -3
  22. package/lib/Locale/util.js +6 -1
  23. package/lib/LoginButton/index.d.ts +12 -0
  24. package/lib/LoginButton/index.js +74 -0
  25. package/lib/SessionUser/components/un-login.js +42 -31
  26. package/lib/SharedBridge/index.d.ts +16 -0
  27. package/lib/SharedBridge/index.js +109 -0
  28. package/lib/SharedBridge/need-storage-access-api-dialog.d.ts +7 -0
  29. package/lib/SharedBridge/need-storage-access-api-dialog.js +212 -0
  30. package/lib/Theme/index.d.ts +2 -2
  31. package/lib/Theme/index.js +1 -1
  32. package/lib/Util/iframe.d.ts +5 -0
  33. package/lib/Util/iframe.js +24 -0
  34. package/lib/Util/index.d.ts +10 -1
  35. package/lib/Util/index.js +67 -4
  36. package/package.json +7 -6
  37. package/src/Address/responsive-did-address.tsx +11 -1
  38. package/src/DIDConnect/app-icon.tsx +36 -0
  39. package/src/DIDConnect/app-info-item.tsx +82 -0
  40. package/src/DIDConnect/did-connect-footer.tsx +51 -0
  41. package/src/DIDConnect/did-connect-logo.tsx +8 -0
  42. package/src/DIDConnect/index.ts +7 -0
  43. package/src/DIDConnect/powered-by.tsx +48 -0
  44. package/src/DIDConnect/with-container.tsx +307 -0
  45. package/src/DIDConnect/with-ux-theme.tsx +22 -0
  46. package/src/Dialog/confirm.jsx +31 -23
  47. package/src/Dialog/use-confirm.jsx +6 -0
  48. package/src/Locale/util.ts +7 -2
  49. package/src/LoginButton/index.tsx +73 -0
  50. package/src/SessionUser/components/un-login.tsx +34 -27
  51. package/src/SharedBridge/index.tsx +123 -0
  52. package/src/SharedBridge/need-storage-access-api-dialog.tsx +171 -0
  53. package/src/Theme/index.ts +2 -2
  54. package/src/Util/iframe.ts +19 -0
  55. package/src/Util/index.ts +77 -4
@@ -0,0 +1,24 @@
1
+ export function getCallbackAction(id, action) {
2
+ return `callback_${action}_${id}`;
3
+ }
4
+
5
+ // eslint-disable-next-line require-await
6
+ export async function callIframe(iframe, action) {
7
+ const callbackAction = getCallbackAction(iframe.dataset.id, action);
8
+ const promise = new Promise(resolve => {
9
+ const handleMessage = ({
10
+ data
11
+ }) => {
12
+ if (data.action === callbackAction) {
13
+ window.removeEventListener('message', handleMessage);
14
+ resolve(data);
15
+ }
16
+ };
17
+ window.addEventListener('message', handleMessage);
18
+ });
19
+ iframe?.contentWindow?.postMessage({
20
+ action,
21
+ callback: callbackAction
22
+ }, '*');
23
+ return promise;
24
+ }
@@ -1,3 +1,4 @@
1
+ import Cookies from 'js-cookie';
1
2
  import { type DeepmergeOptions } from '@mui/utils/deepmerge';
2
3
  import type { $TSFixMe, Locale } from '../type';
3
4
  declare let dateTool: $TSFixMe | null;
@@ -14,7 +15,7 @@ export declare function getCookieOptions(expireInDays?: number | {
14
15
  returnDomain?: boolean;
15
16
  sameSite?: Cookies.CookieAttributes['sameSite'];
16
17
  secure?: boolean;
17
- }): import("js-cookie").CookieAttributes;
18
+ }): Cookies.CookieAttributes;
18
19
  export declare const getColor: (props: $TSFixMe) => any;
19
20
  export declare const getBackground: (props: $TSFixMe) => any;
20
21
  /**
@@ -76,6 +77,7 @@ export declare const sleep: (time?: number) => Promise<void>;
76
77
  export declare const isUrl: (str: string) => boolean;
77
78
  export declare const getVisitorId: () => string | null;
78
79
  export declare const setVisitorId: (value: string | null) => void;
80
+ export declare const ensureVisitorId: () => void;
79
81
  export declare const getDIDColor: (did: string) => any;
80
82
  type NestedTranslation = {
81
83
  [key: string]: string | NestedTranslation;
@@ -93,6 +95,13 @@ export declare const getTranslation: (translations: TranslationsObject, locale:
93
95
  }) => string;
94
96
  export declare const lazyRetry: (fn: () => Promise<any>) => import("react").LazyExoticComponent<import("react").ComponentType<any>>;
95
97
  export declare const cleanedObj: (obj: object) => import("lodash").Dictionary<any>;
98
+ /**
99
+ * 将十六进制颜色转换为 RGBA
100
+ * @param hex 十六进制颜色字符串 (例如: "#FF0000" 或 "FF0000")
101
+ * @param alpha 透明度值 (0-1 之间,默认为 1)
102
+ * @returns RGBA 颜色字符串 (例如: "rgba(255, 0, 0, 1)")
103
+ */
104
+ export declare function hexToRgba(hex: string, alpha?: number): string;
96
105
  /**
97
106
  * 依次对数组中的对象进行深度合并
98
107
  * @param objects - 需要合并的对象数组
package/lib/Util/index.js CHANGED
@@ -5,8 +5,11 @@ import { getDIDMotifInfo, colors } from '@arcblock/did-motif';
5
5
  import isNil from 'lodash/isNil';
6
6
  import omitBy from 'lodash/omitBy';
7
7
  import pRetry from 'p-retry';
8
+ import Cookies from 'js-cookie';
9
+ import colorConvert from 'color-convert';
8
10
  import deepmerge from '@mui/utils/deepmerge';
9
11
  import { DID_PREFIX, BLOCKLET_SERVICE_PATH_PREFIX } from './constant';
12
+ import { getFederatedEnabled } from './federated';
10
13
  let dateTool = null;
11
14
  const IP_V4_REGEX = /^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/;
12
15
  // 常见顶级域名
@@ -360,15 +363,63 @@ export const sleep = (time = 0) => {
360
363
  export const isUrl = str => {
361
364
  return /^https?:\/\//.test(str);
362
365
  };
363
- const visitorIdKey = '__visitor_id';
366
+ const visitorIdKey = 'vid';
367
+ const visitorIdKeyLegacy = '__visitor_id';
364
368
  export const getVisitorId = () => {
365
- return localStorage.getItem(visitorIdKey);
369
+ // FIXME: @zhanghan 短期内做一个兼容,确保在 migrate 前的请求能够携带正确的 vid
370
+ return Cookies.get(visitorIdKey) || localStorage.getItem(visitorIdKeyLegacy);
366
371
  };
367
372
  export const setVisitorId = value => {
368
373
  if (value === null) {
369
- localStorage.removeItem(visitorIdKey);
374
+ Cookies.remove(visitorIdKey, {
375
+ sameSite: 'None',
376
+ secure: true
377
+ });
370
378
  } else {
371
- localStorage.setItem(visitorIdKey, value);
379
+ Cookies.set(visitorIdKey, value, {
380
+ sameSite: 'None',
381
+ secure: true,
382
+ expires: 365
383
+ });
384
+ }
385
+ };
386
+ export const ensureVisitorId = () => {
387
+ let visitorId = localStorage.getItem(visitorIdKeyLegacy);
388
+ if (visitorId) {
389
+ localStorage.removeItem(visitorIdKeyLegacy);
390
+ setVisitorId(visitorId);
391
+ }
392
+ if (getVisitorId()) {
393
+ return;
394
+ }
395
+ if (!getFederatedEnabled()) {
396
+ try {
397
+ // 在支持 crypto.randomUUID 的环境中使用
398
+ if (window.crypto && typeof window.crypto.randomUUID === 'function') {
399
+ visitorId = window.crypto.randomUUID();
400
+ } else {
401
+ // 在不支持 crypto.randomUUID 的环境中生成随机 ID
402
+ const randomValues = new Uint8Array(16);
403
+ if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
404
+ window.crypto.getRandomValues(randomValues);
405
+ } else {
406
+ // 降级方案:使用 Math.random 生成
407
+ for (let i = 0; i < 16; i++) {
408
+ randomValues[i] = Math.floor(Math.random() * 256);
409
+ }
410
+ }
411
+
412
+ // 转换为 UUID 格式
413
+ const hexArray = Array.from(randomValues).map(b => b.toString(16).padStart(2, '0'));
414
+ visitorId = [hexArray.slice(0, 4).join(''), hexArray.slice(4, 6).join(''), hexArray.slice(6, 8).join(''), hexArray.slice(8, 10).join(''), hexArray.slice(10, 16).join('')].join('-');
415
+ }
416
+ } catch (error) {
417
+ // 如果上述方法都失败,使用时间戳和随机数生成
418
+ visitorId = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
419
+ }
420
+ }
421
+ if (visitorId) {
422
+ setVisitorId(visitorId);
372
423
  }
373
424
  };
374
425
  export const getDIDColor = did => {
@@ -429,6 +480,18 @@ export const cleanedObj = obj => {
429
480
  return omitBy(obj, isNil);
430
481
  };
431
482
 
483
+ /**
484
+ * 将十六进制颜色转换为 RGBA
485
+ * @param hex 十六进制颜色字符串 (例如: "#FF0000" 或 "FF0000")
486
+ * @param alpha 透明度值 (0-1 之间,默认为 1)
487
+ * @returns RGBA 颜色字符串 (例如: "rgba(255, 0, 0, 1)")
488
+ */
489
+ export function hexToRgba(hex, alpha = 1) {
490
+ const [r, g, b] = colorConvert.hex.rgb(hex);
491
+ // 返回 RGBA 格式
492
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
493
+ }
494
+
432
495
  /**
433
496
  * 依次对数组中的对象进行深度合并
434
497
  * @param objects - 需要合并的对象数组
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.13.13",
3
+ "version": "2.13.14",
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": "b098f3a884e8b34a1926479507b674d40ea58298",
74
+ "gitHead": "b9f48199169b641a2d3277806501471a56dd496d",
75
75
  "dependencies": {
76
76
  "@arcblock/did-motif": "^1.1.13",
77
- "@arcblock/icons": "^2.13.13",
78
- "@arcblock/nft-display": "^2.13.13",
79
- "@arcblock/react-hooks": "^2.13.13",
77
+ "@arcblock/icons": "^2.13.14",
78
+ "@arcblock/nft-display": "^2.13.14",
79
+ "@arcblock/react-hooks": "^2.13.14",
80
80
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
81
- "@blocklet/theme": "^2.13.13",
81
+ "@blocklet/theme": "^2.13.14",
82
82
  "@fontsource/roboto": "~5.1.1",
83
83
  "@fontsource/ubuntu-mono": "^5.0.18",
84
84
  "@iconify-icons/logos": "^1.2.36",
@@ -94,6 +94,7 @@
94
94
  "awesome-phonenumber": "^7.4.0",
95
95
  "axios": "^1.7.5",
96
96
  "base64-url": "^2.3.3",
97
+ "color-convert": "^3.0.1",
97
98
  "copy-to-clipboard": "^3.3.2",
98
99
  "core-js": "^3.25.5",
99
100
  "d3-geo": "^1.12.1",
@@ -80,7 +80,17 @@ const ResponsiveDidAddress = forwardRef<HTMLDidAddressElement, IResponsiveDidAdd
80
80
  }, [containerWidth, addressFullWidth]);
81
81
 
82
82
  return (
83
- <Root as={component} ref={containerRef} style={style} className={className} size={size} inline={rest.inline}>
83
+ <Root
84
+ as={component}
85
+ ref={containerRef}
86
+ style={style}
87
+ className={className}
88
+ size={size}
89
+ {...(component === 'span'
90
+ ? {}
91
+ : {
92
+ inline: rest.inline,
93
+ })}>
84
94
  <StyledDidAddress
85
95
  style={{
86
96
  position: loading ? 'absolute' : 'static',
@@ -0,0 +1,36 @@
1
+ import { useState } from 'react';
2
+ import { joinURL } from 'ufo';
3
+ import { SxProps } from '@mui/material';
4
+
5
+ import Img from '../Img';
6
+ import { isUrl } from '../Util';
7
+ import DidAvatar from '../Avatar';
8
+
9
+ type AppIconProps = {
10
+ appInfo: any;
11
+ size?: number;
12
+ sx?: SxProps;
13
+ };
14
+
15
+ export default function AppIcon({ appInfo, size = 32, ...rest }: AppIconProps) {
16
+ const [error, setError] = useState(false);
17
+ if (error || !(appInfo.appUrl || appInfo.appLogo)) {
18
+ return <DidAvatar did={appInfo.appPid} size={size} />;
19
+ }
20
+
21
+ let logoUrl = appInfo.appLogo || '';
22
+ if (!isUrl(logoUrl)) {
23
+ logoUrl = joinURL(appInfo.appUrl || '', logoUrl);
24
+ }
25
+
26
+ return (
27
+ <Img
28
+ src={logoUrl}
29
+ alt={appInfo.appName || 'Blocklet Icon'}
30
+ width={size}
31
+ height={size}
32
+ {...rest}
33
+ onError={() => setError(true)}
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,82 @@
1
+ import { Box, IconButton, SxProps, Tooltip, useMediaQuery } from '@mui/material';
2
+ import CheckIcon from '@iconify-icons/material-symbols/check';
3
+ import { Icon } from '@iconify/react';
4
+
5
+ import DID from '../DID';
6
+ import { mergeSx } from '../Util/style';
7
+ import AppIcon from './app-icon';
8
+
9
+ export default function AppInfoItem({
10
+ appInfo,
11
+ active = false,
12
+ appLogo = null,
13
+ sx,
14
+ }: {
15
+ appInfo: any;
16
+ active?: boolean;
17
+ appLogo?: React.ReactNode;
18
+ sx?: SxProps;
19
+ }) {
20
+ const isTinyView = useMediaQuery('(max-width:400px)');
21
+
22
+ return (
23
+ <Box
24
+ sx={mergeSx(
25
+ {
26
+ display: 'flex',
27
+ alignItems: 'center',
28
+ fontWeight: 600,
29
+ color: 'text.primary',
30
+
31
+ '& .app-info-content': {
32
+ paddingLeft: '8px',
33
+ overflow: 'hidden',
34
+ },
35
+
36
+ '& .app-info-name': {
37
+ maxWidth: '100%',
38
+ lineHeight: 'normal',
39
+ whiteSpace: 'nowrap',
40
+ overflow: 'hidden',
41
+ textOverflow: 'ellipsis',
42
+ fontSize: '12px',
43
+ color: 'text.primary',
44
+ },
45
+ },
46
+ sx
47
+ )}>
48
+ {appLogo || (
49
+ <Box
50
+ sx={{
51
+ borderRadius: 1,
52
+ overflow: 'hidden',
53
+ fontSize: 0,
54
+ }}>
55
+ <AppIcon appInfo={appInfo} />
56
+ </Box>
57
+ )}
58
+
59
+ <Box className="app-info-content">
60
+ <Tooltip title={appInfo.appName} placement="top">
61
+ <Box className="app-info-name">{appInfo.appName}</Box>
62
+ </Tooltip>
63
+ {appInfo.appPid && (
64
+ <DID
65
+ className="app-info-did"
66
+ did={appInfo.appPid}
67
+ sx={{ fontSize: '10px !important' }}
68
+ copyable={false}
69
+ startChars={isTinyView ? 6 : 8}
70
+ endChars={isTinyView ? 6 : 8}
71
+ size={12}
72
+ />
73
+ )}
74
+ </Box>
75
+ {active ? (
76
+ <IconButton size="small" color="success">
77
+ <Icon icon={CheckIcon} color="success" />
78
+ </IconButton>
79
+ ) : null}
80
+ </Box>
81
+ );
82
+ }
@@ -0,0 +1,51 @@
1
+ import { Box, useMediaQuery } from '@mui/material';
2
+
3
+ import PoweredBy from './powered-by';
4
+ import AppInfoItem from './app-info-item';
5
+ import AppIcon from './app-icon';
6
+ import { getDIDColor, hexToRgba } from '../Util';
7
+
8
+ export default function DIDConnectFooter({
9
+ currentAppInfo = globalThis.blocklet,
10
+ currentAppColor = globalThis.blocklet?.appPid ? getDIDColor(globalThis.blocklet?.appPid) : '#fff',
11
+ }: {
12
+ currentAppInfo?: any;
13
+ currentAppColor?: string;
14
+ }) {
15
+ const isSmallView = useMediaQuery('(max-width:640px)');
16
+
17
+ return (
18
+ <Box
19
+ sx={{
20
+ display: 'flex',
21
+ justifyContent: 'space-between',
22
+ alignItems: 'center',
23
+ gap: 1,
24
+ fontSize: 12,
25
+ backgroundColor: hexToRgba(currentAppColor, 0.08),
26
+ // 需要保持跟 .did-connect__root 的规则一样
27
+ mx: isSmallView ? -2 : -3,
28
+ px: isSmallView ? 2 : 3,
29
+ py: 1.5,
30
+ // HACK: 极限条件下,footer 会溢出,使用隐藏的滚动条来处理这种情况(屏幕宽度小于 360 时会出现)
31
+ overflow: 'auto',
32
+ '&::-webkit-scrollbar': {
33
+ display: 'none', // 隐藏滚动条 (Webkit 浏览器)
34
+ },
35
+ '-ms-overflow-style': 'none', // 隐藏滚动条 (IE 浏览器)
36
+ 'scrollbar-width': 'none', // 隐藏滚动条 (Firefox)
37
+ }}
38
+ className="did-connect__footer">
39
+ <AppInfoItem
40
+ appInfo={currentAppInfo}
41
+ appLogo={<AppIcon appInfo={currentAppInfo} size={24} sx={{ flexShrink: 0 }} />}
42
+ sx={{
43
+ flex: 1,
44
+ overflow: 'hidden',
45
+ }}
46
+ />
47
+
48
+ <PoweredBy sx={{ maxWidth: '100%', justifyContent: 'end' }} />
49
+ </Box>
50
+ );
51
+ }
@@ -0,0 +1,8 @@
1
+ import DidBrandConnect from '@arcblock/icons/lib/DidBrandConnect';
2
+ import { useTheme } from '@mui/material';
3
+
4
+ export default function DidConnectLogo() {
5
+ const theme = useTheme();
6
+
7
+ return <DidBrandConnect style={{ filter: theme.palette.mode === 'dark' ? 'invert(1)' : 'none' }} />;
8
+ }
@@ -0,0 +1,7 @@
1
+ export { default as DIDConnectFooter } from './did-connect-footer';
2
+ export { default as AppInfoItem } from './app-info-item';
3
+ export { default as AppIcon } from './app-icon';
4
+ export { default as PoweredBy } from './powered-by';
5
+ export { default as withContainer } from './with-container';
6
+ export { default as withUxTheme } from './with-ux-theme';
7
+ export { default as DIDConnectLogo } from './did-connect-logo';
@@ -0,0 +1,48 @@
1
+ import { Box, useTheme } from '@mui/material';
2
+ import { Icon } from '@iconify/react';
3
+ import shieldCheckIcon from '@iconify-icons/mdi/shield-check';
4
+
5
+ import DidConnectLogo from './did-connect-logo';
6
+ import { mergeSx } from '../Util/style';
7
+
8
+ export default function PoweredBy({ ...rest }) {
9
+ const { palette } = useTheme();
10
+
11
+ return (
12
+ <Box
13
+ {...rest}
14
+ sx={mergeSx(
15
+ {
16
+ display: 'flex',
17
+ alignItems: 'center',
18
+ justifyContent: 'center',
19
+ color: 'text.secondary',
20
+ gap: 0.5,
21
+ fontSize: 12,
22
+ fontFamily: 'Lexend',
23
+ whiteSpace: 'nowrap',
24
+ },
25
+ rest?.sx
26
+ )}>
27
+ <Icon icon={shieldCheckIcon} color={palette.success.main} />
28
+ Secured by
29
+ <Box
30
+ component="a"
31
+ href="https://www.didconnect.io/"
32
+ target="_blank"
33
+ rel="noopener"
34
+ sx={{
35
+ color: 'initial',
36
+ display: 'flex',
37
+ alignItems: 'center',
38
+ gap: 0.5,
39
+ textDecoration: 'none',
40
+ '&:hover': {
41
+ textDecoration: 'underline',
42
+ },
43
+ }}>
44
+ <DidConnectLogo />
45
+ </Box>
46
+ </Box>
47
+ );
48
+ }