@arcblock/ux 2.12.14 → 2.12.16

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.
@@ -98,8 +98,8 @@ const DidAddress = /*#__PURE__*/forwardRef((props, ref) => {
98
98
  /* title prop 直接加在 icon 上不生效 */
99
99
  _jsx(CopyIcon, {
100
100
  className: "did-address-copy",
101
- width: 16,
102
- height: 16,
101
+ width: "1em",
102
+ height: "1em",
103
103
  onClick: onCopy
104
104
  })
105
105
  });
@@ -115,37 +115,37 @@ export default function LocaleSelector(props) {
115
115
  children: languages.map(({
116
116
  code,
117
117
  name
118
- }) => /*#__PURE__*/_jsxs(MenuItem, {
119
- className: "locale-item",
120
- onClick: () => onSelect(code),
121
- sx: {
122
- fontSize: 16,
123
- fontStyle: 'normal',
124
- fontStretch: 'normal',
125
- lineHeight: 'normal',
126
- letterSpacing: '2px',
127
- textAlign: 'center',
128
- color: getColor({
129
- ...popperProps,
130
- theme
131
- }),
132
- cursor: 'pointer',
133
- display: 'flex',
134
- padding: '16px',
135
- alignItems: 'center'
136
- },
137
- children: [/*#__PURE__*/_jsx(Box, {
138
- component: IconifyIcon,
139
- icon: CheckIcon,
140
- className: code === locale ? 'check-icon check-icon-visible' : 'check-icon',
141
- sx: [code === locale ? {
142
- visibility: 'visible'
143
- } : {}, {
144
- visibility: 'hidden',
145
- marginRight: 0.5
146
- }]
147
- }), name]
148
- }, code))
118
+ }) => {
119
+ return /*#__PURE__*/_jsxs(MenuItem, {
120
+ className: "locale-item",
121
+ onClick: () => onSelect(code),
122
+ sx: {
123
+ fontSize: 16,
124
+ fontStyle: 'normal',
125
+ fontStretch: 'normal',
126
+ lineHeight: 'normal',
127
+ letterSpacing: '2px',
128
+ textAlign: 'center',
129
+ color: getColor({
130
+ ...popperProps,
131
+ theme
132
+ }),
133
+ cursor: 'pointer',
134
+ display: 'flex',
135
+ padding: '16px',
136
+ alignItems: 'center'
137
+ },
138
+ children: [/*#__PURE__*/_jsx(Box, {
139
+ component: IconifyIcon,
140
+ icon: CheckIcon,
141
+ className: code === locale ? 'check-icon check-icon-visible' : 'check-icon',
142
+ sx: {
143
+ marginRight: 0.5,
144
+ visibility: code === locale ? 'visible' : 'hidden'
145
+ }
146
+ }), name]
147
+ }, code);
148
+ })
149
149
  })
150
150
  })
151
151
  })
@@ -0,0 +1,13 @@
1
+ import { SelectProps } from '@mui/material';
2
+ import type { CountryIso2 } from 'react-international-phone';
3
+ export interface CountryDisplayOptions {
4
+ showFullName?: boolean;
5
+ }
6
+ export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
7
+ value: CountryIso2;
8
+ onChange?: (value: CountryIso2) => void;
9
+ selectCountryProps?: CountryDisplayOptions;
10
+ preview?: boolean;
11
+ }
12
+ declare const CountrySelect: import("react").ForwardRefExoticComponent<Omit<CountrySelectProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
13
+ export default CountrySelect;
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, forwardRef, useState, useCallback } from 'react';
3
+ import { Box, MenuItem, Select, Typography, TextField } from '@mui/material';
4
+ import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
5
+ const CountrySelect = /*#__PURE__*/forwardRef(({
6
+ value,
7
+ onChange,
8
+ sx = {},
9
+ selectCountryProps,
10
+ preview = false
11
+ }, ref) => {
12
+ const {
13
+ showFullName = false
14
+ } = selectCountryProps || {};
15
+ const [searchQuery, setSearchQuery] = useState('');
16
+ const countryDetail = useMemo(() => {
17
+ const item = defaultCountries.find(v => v[1] === value);
18
+ return value && item ? parseCountry(item) : {
19
+ name: '',
20
+ iso2: ''
21
+ };
22
+ }, [value]);
23
+ const filteredCountries = useMemo(() => {
24
+ if (!searchQuery) return defaultCountries;
25
+ const query = searchQuery.toLowerCase();
26
+ return defaultCountries.filter(country => {
27
+ const parsed = parseCountry(country);
28
+ return parsed.name.toLowerCase().includes(query) || parsed.iso2.toLowerCase().includes(query) || parsed.dialCode.includes(query);
29
+ });
30
+ }, [searchQuery]);
31
+ const onCountryChange = e => {
32
+ onChange?.(e.target.value);
33
+ };
34
+ const renderCountryContent = useCallback(code => /*#__PURE__*/_jsxs(Box, {
35
+ display: "flex",
36
+ alignItems: "center",
37
+ flexWrap: "nowrap",
38
+ gap: 0.5,
39
+ sx: {
40
+ cursor: preview ? 'default' : 'pointer'
41
+ },
42
+ children: [/*#__PURE__*/_jsx(FlagEmoji, {
43
+ iso2: code,
44
+ style: {
45
+ display: 'flex',
46
+ width: '24px',
47
+ color: 'inherit'
48
+ }
49
+ }), /*#__PURE__*/_jsx(Typography, {
50
+ children: showFullName ? countryDetail?.name : countryDetail?.iso2
51
+ })]
52
+ }), [preview, showFullName, countryDetail]);
53
+ if (preview) {
54
+ return renderCountryContent(value);
55
+ }
56
+ return /*#__PURE__*/_jsxs(Select, {
57
+ ref: ref,
58
+ MenuProps: {
59
+ style: {
60
+ height: '300px',
61
+ top: '10px'
62
+ },
63
+ anchorOrigin: {
64
+ vertical: 'bottom',
65
+ horizontal: 'left'
66
+ },
67
+ transformOrigin: {
68
+ vertical: 'top',
69
+ horizontal: 'left'
70
+ }
71
+ },
72
+ sx: {
73
+ width: '100%',
74
+ fieldset: {
75
+ display: 'none'
76
+ },
77
+ '&.Mui-focused:has(div[aria-expanded="false"])': {
78
+ fieldset: {
79
+ display: 'block'
80
+ }
81
+ },
82
+ '.MuiSelect-select': {
83
+ padding: '8px',
84
+ paddingRight: '24px !important'
85
+ },
86
+ svg: {
87
+ right: 0
88
+ },
89
+ '.MuiMenuItem-root': {
90
+ justifyContent: 'flex-start'
91
+ },
92
+ ...sx
93
+ },
94
+ value: value,
95
+ onChange: onCountryChange,
96
+ renderValue: renderCountryContent,
97
+ children: [/*#__PURE__*/_jsx(Box, {
98
+ sx: {
99
+ p: 1,
100
+ position: 'sticky',
101
+ top: 0,
102
+ bgcolor: 'background.paper',
103
+ zIndex: 1
104
+ },
105
+ children: /*#__PURE__*/_jsx(TextField, {
106
+ size: "small",
107
+ fullWidth: true,
108
+ placeholder: "Search country...",
109
+ value: searchQuery,
110
+ onChange: e => setSearchQuery(e.target.value),
111
+ onClick: e => e.stopPropagation(),
112
+ onKeyDown: e => e.stopPropagation(),
113
+ sx: {
114
+ '& .MuiOutlinedInput-root': {
115
+ '& fieldset': {
116
+ borderColor: 'divider'
117
+ }
118
+ }
119
+ }
120
+ })
121
+ }), filteredCountries.map(c => {
122
+ const parsed = parseCountry(c);
123
+ return /*#__PURE__*/_jsxs(MenuItem, {
124
+ value: parsed.iso2,
125
+ children: [/*#__PURE__*/_jsx(FlagEmoji, {
126
+ iso2: parsed.iso2,
127
+ style: {
128
+ marginRight: '8px',
129
+ width: '24px'
130
+ }
131
+ }), /*#__PURE__*/_jsx(Typography, {
132
+ marginRight: "8px",
133
+ children: parsed.name
134
+ }), /*#__PURE__*/_jsxs(Typography, {
135
+ color: "gray",
136
+ children: ["+", parsed.dialCode]
137
+ })]
138
+ }, parsed.iso2);
139
+ })]
140
+ });
141
+ });
142
+ export default CountrySelect;
@@ -0,0 +1,17 @@
1
+ import { TextFieldProps } from '@mui/material';
2
+ import { CountryIso2 } from 'react-international-phone';
3
+ import { CountryDisplayOptions } from './country-select';
4
+ export interface PhoneValue {
5
+ country: CountryIso2;
6
+ phone: string;
7
+ }
8
+ export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
9
+ value?: PhoneValue;
10
+ onChange?: (value: PhoneValue) => void;
11
+ countryDisplayOptions?: CountryDisplayOptions;
12
+ preview?: boolean;
13
+ allowDial?: boolean;
14
+ }
15
+ export declare function getCountryCallingCode(country: CountryIso2): string;
16
+ export declare function validatePhoneNumber(phone: string): boolean;
17
+ export default function PhoneInput({ value, onChange, placeholder, countryDisplayOptions, preview, allowDial, ...props }: PhoneInputProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,137 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, InputAdornment, TextField, Typography } from '@mui/material';
4
+ import { defaultCountries, usePhoneInput, parseCountry } from 'react-international-phone';
5
+ import isMobilePhone from 'validator/lib/isMobilePhone';
6
+ import CountrySelect from './country-select';
7
+ export function getCountryCallingCode(country) {
8
+ const countryData = defaultCountries.find(c => parseCountry(c).iso2 === country);
9
+ if (countryData) {
10
+ return parseCountry(countryData).dialCode.replace('+', '');
11
+ }
12
+ return '';
13
+ }
14
+ export function validatePhoneNumber(phone) {
15
+ return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', {
16
+ strictMode: true
17
+ });
18
+ }
19
+ export default function PhoneInput({
20
+ value = {
21
+ country: 'us',
22
+ phone: ''
23
+ },
24
+ onChange,
25
+ placeholder = 'Enter phone number',
26
+ countryDisplayOptions = {},
27
+ preview = false,
28
+ allowDial = true,
29
+ ...props
30
+ }) {
31
+ // 在使用 usePhoneInput 之前处理电话号码
32
+ const phoneWithPrefix = useMemo(() => {
33
+ if (!value.phone) return '';
34
+ // 如果电话号码已经包含 + 号,说明已经有区号了
35
+ if (value.phone.startsWith('+')) return value.phone;
36
+ // 获取国家区号并添加到号码前
37
+ const countryCode = getCountryCallingCode(value.country);
38
+ return `+${countryCode}${value.phone}`;
39
+ }, [value.phone, value.country]);
40
+ const {
41
+ phone,
42
+ handlePhoneValueChange,
43
+ country,
44
+ setCountry
45
+ } = usePhoneInput({
46
+ defaultCountry: value.country,
47
+ value: phoneWithPrefix,
48
+ countries: defaultCountries,
49
+ onChange: data => {
50
+ // 确保类型匹配 PhoneValue 接口
51
+ const phoneValue = {
52
+ country: data.country,
53
+ phone: data.phone
54
+ };
55
+ onChange?.(phoneValue);
56
+ }
57
+ });
58
+ const onCountryChange = v => {
59
+ if (!preview) {
60
+ setCountry(v);
61
+ }
62
+ };
63
+ if (preview) {
64
+ const isValid = phone && validatePhoneNumber(phone);
65
+ const canDial = allowDial && isValid;
66
+ return /*#__PURE__*/_jsxs(Box, {
67
+ display: "flex",
68
+ alignItems: "center",
69
+ component: canDial ? 'a' : 'div',
70
+ href: canDial ? `tel:${phone}` : undefined,
71
+ sx: {
72
+ textDecoration: 'none',
73
+ color: 'inherit',
74
+ cursor: canDial ? 'pointer' : 'default',
75
+ '&:hover': {
76
+ opacity: canDial ? 0.8 : 1
77
+ }
78
+ },
79
+ children: [/*#__PURE__*/_jsx(CountrySelect, {
80
+ value: country,
81
+ preview: preview
82
+ }), /*#__PURE__*/_jsx(Typography, {
83
+ sx: {
84
+ ml: 0.5
85
+ },
86
+ children: phone
87
+ })]
88
+ });
89
+ }
90
+ return /*#__PURE__*/_jsx(TextField, {
91
+ ...props,
92
+ value: phone,
93
+ onChange: preview ? undefined : handlePhoneValueChange,
94
+ placeholder: placeholder,
95
+ className: "phone-input",
96
+ disabled: preview,
97
+ sx: {
98
+ '&>.MuiInputBase-root': {
99
+ paddingLeft: '8px',
100
+ cursor: preview ? 'default' : undefined
101
+ },
102
+ ...(props.sx ?? {}),
103
+ ...(preview ? {
104
+ '& .Mui-disabled': {
105
+ '-webkit-text-fill-color': 'inherit',
106
+ color: 'inherit'
107
+ },
108
+ fieldset: {
109
+ display: 'none'
110
+ }
111
+ } : {})
112
+ },
113
+ InputProps: {
114
+ startAdornment: /*#__PURE__*/_jsx(InputAdornment, {
115
+ position: "start",
116
+ style: {
117
+ marginRight: '2px',
118
+ marginLeft: '-8px'
119
+ },
120
+ children: /*#__PURE__*/_jsx(CountrySelect, {
121
+ value: country,
122
+ onChange: onCountryChange,
123
+ preview: preview,
124
+ selectCountryProps: countryDisplayOptions,
125
+ sx: {
126
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
127
+ borderColor: 'transparent'
128
+ },
129
+ opacity: preview ? 1 : undefined,
130
+ cursor: preview ? 'default' : undefined
131
+ }
132
+ })
133
+ }),
134
+ ...(props.InputProps ?? {})
135
+ }
136
+ });
137
+ }
@@ -25,6 +25,7 @@ export default function UnLogin({
25
25
  const t = useMemoizedFn((key, data = {}) => {
26
26
  return translate(translations, key, locale, 'en', data);
27
27
  });
28
+ const searchParams = new URLSearchParams(window.location.search);
28
29
  const browser = useBrowser();
29
30
  const isFirstLoading = false;
30
31
  const userAnchorRef = useRef(null);
@@ -53,7 +54,7 @@ export default function UnLogin({
53
54
  let timer;
54
55
 
55
56
  // NOTICE: ArcSphere 中使用原生的快捷登录
56
- if (!browser.arcSphere) {
57
+ if (!browser.arcSphere && searchParams.get('showQuickConnect') !== 'false') {
57
58
  timer = setTimeout(async () => {
58
59
  currentState.userSessions = await session.getUserSessions();
59
60
  if (currentState.userSessions.length > 0) {
@@ -15,6 +15,7 @@ export declare const PASSPORT_STATUS: {
15
15
  REVOKED: string;
16
16
  };
17
17
  export declare const LOGIN_PROVIDER: {
18
+ EMAIL: string;
18
19
  AUTH0: string;
19
20
  APPLE: string;
20
21
  GITHUB: string;
@@ -15,7 +15,7 @@ export const PASSPORT_STATUS = {
15
15
  REVOKED: 'revoked'
16
16
  };
17
17
  export const LOGIN_PROVIDER = {
18
- // EMAIL: 'Email',
18
+ EMAIL: 'email',
19
19
  AUTH0: 'auth0',
20
20
  APPLE: 'apple',
21
21
  GITHUB: 'github',
@@ -25,8 +25,8 @@ export const LOGIN_PROVIDER = {
25
25
  PASSKEY: 'passkey'
26
26
  };
27
27
  export const LOGIN_PROVIDER_NAME = {
28
- // [LOGIN_PROVIDER.EMAIL]: 'Email',
29
- [LOGIN_PROVIDER.AUTH0]: 'Email',
28
+ [LOGIN_PROVIDER.EMAIL]: 'Email',
29
+ [LOGIN_PROVIDER.AUTH0]: 'Auth0',
30
30
  [LOGIN_PROVIDER.APPLE]: 'Apple',
31
31
  [LOGIN_PROVIDER.GITHUB]: 'Github',
32
32
  [LOGIN_PROVIDER.GOOGLE]: 'Google',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.12.14",
3
+ "version": "2.12.16",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -68,12 +68,12 @@
68
68
  "react": ">=18.2.0",
69
69
  "react-router-dom": ">=6.22.3"
70
70
  },
71
- "gitHead": "ac2b40cdc5ca3cf446e5a1ced41d5eada7860cbd",
71
+ "gitHead": "02b619f243bf6a0c6847349edf513e18b212bcd7",
72
72
  "dependencies": {
73
73
  "@arcblock/did-motif": "^1.1.13",
74
- "@arcblock/icons": "^2.12.14",
75
- "@arcblock/nft-display": "^2.12.14",
76
- "@arcblock/react-hooks": "^2.12.14",
74
+ "@arcblock/icons": "^2.12.16",
75
+ "@arcblock/nft-display": "^2.12.16",
76
+ "@arcblock/react-hooks": "^2.12.16",
77
77
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
78
78
  "@fontsource/inter": "^5.0.16",
79
79
  "@fontsource/ubuntu-mono": "^5.0.18",
@@ -110,6 +110,7 @@
110
110
  "react-error-boundary": "^3.1.4",
111
111
  "react-ga4": "^2.1.0",
112
112
  "react-helmet": "^6.1.0",
113
+ "react-international-phone": "^3.1.2",
113
114
  "react-intersection-observer": "^8.34.0",
114
115
  "react-lottie-player": "^1.4.3",
115
116
  "react-player": "^1.15.3",
@@ -119,6 +120,7 @@
119
120
  "rebound": "^0.1.0",
120
121
  "topojson-client": "^3.1.0",
121
122
  "type-fest": "^4.28.0",
123
+ "validator": "^13.9.0",
122
124
  "versor": "^0.0.4"
123
125
  }
124
126
  }
@@ -120,7 +120,7 @@ const DidAddress = forwardRef<HTMLDidAddressElement, IDidAddressProps>((props, r
120
120
  </Tooltip>
121
121
  ) : (
122
122
  /* title prop 直接加在 icon 上不生效 */
123
- <CopyIcon className="did-address-copy" width={16} height={16} onClick={onCopy} />
123
+ <CopyIcon className="did-address-copy" width="1em" height="1em" onClick={onCopy} />
124
124
  )}
125
125
  </span>
126
126
  );
@@ -115,39 +115,38 @@ export default function LocaleSelector(props: LocaleSelectorProps) {
115
115
  }}>
116
116
  <ClickAwayListener onClickAway={onClose}>
117
117
  <MenuList>
118
- {languages.map(({ code, name }) => (
119
- <MenuItem
120
- key={code}
121
- className="locale-item"
122
- onClick={() => onSelect(code)}
123
- sx={{
124
- fontSize: 16,
125
- fontStyle: 'normal',
126
- fontStretch: 'normal',
127
- lineHeight: 'normal',
128
- letterSpacing: '2px',
129
- textAlign: 'center',
130
- color: getColor({ ...popperProps, theme }),
131
- cursor: 'pointer',
132
- display: 'flex',
133
- padding: '16px',
134
- alignItems: 'center',
135
- }}>
136
- <Box
137
- component={IconifyIcon}
138
- icon={CheckIcon}
139
- className={code === locale ? 'check-icon check-icon-visible' : 'check-icon'}
140
- sx={[
141
- code === locale ? { visibility: 'visible' } : {},
142
- {
143
- visibility: 'hidden',
118
+ {languages.map(({ code, name }) => {
119
+ return (
120
+ <MenuItem
121
+ key={code}
122
+ className="locale-item"
123
+ onClick={() => onSelect(code)}
124
+ sx={{
125
+ fontSize: 16,
126
+ fontStyle: 'normal',
127
+ fontStretch: 'normal',
128
+ lineHeight: 'normal',
129
+ letterSpacing: '2px',
130
+ textAlign: 'center',
131
+ color: getColor({ ...popperProps, theme }),
132
+ cursor: 'pointer',
133
+ display: 'flex',
134
+ padding: '16px',
135
+ alignItems: 'center',
136
+ }}>
137
+ <Box
138
+ component={IconifyIcon}
139
+ icon={CheckIcon}
140
+ className={code === locale ? 'check-icon check-icon-visible' : 'check-icon'}
141
+ sx={{
144
142
  marginRight: 0.5,
145
- },
146
- ]}
147
- />
148
- {name}
149
- </MenuItem>
150
- ))}
143
+ visibility: code === locale ? 'visible' : 'hidden',
144
+ }}
145
+ />
146
+ {name}
147
+ </MenuItem>
148
+ );
149
+ })}
151
150
  </MenuList>
152
151
  </ClickAwayListener>
153
152
  </Box>
@@ -0,0 +1,138 @@
1
+ import { useMemo, forwardRef, useState, useCallback } from 'react';
2
+ import { Box, MenuItem, Select, Typography, SelectProps, TextField } from '@mui/material';
3
+ import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
4
+ import type { CountryIso2 } from 'react-international-phone';
5
+
6
+ export interface CountryDisplayOptions {
7
+ showFullName?: boolean;
8
+ }
9
+
10
+ export interface CountrySelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
11
+ value: CountryIso2;
12
+ onChange?: (value: CountryIso2) => void;
13
+ selectCountryProps?: CountryDisplayOptions;
14
+ preview?: boolean;
15
+ }
16
+
17
+ const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(
18
+ ({ value, onChange, sx = {}, selectCountryProps, preview = false }, ref) => {
19
+ const { showFullName = false } = selectCountryProps || {};
20
+ const [searchQuery, setSearchQuery] = useState('');
21
+
22
+ const countryDetail = useMemo(() => {
23
+ const item = defaultCountries.find((v) => v[1] === value);
24
+ return value && item ? parseCountry(item) : { name: '', iso2: '' };
25
+ }, [value]);
26
+
27
+ const filteredCountries = useMemo(() => {
28
+ if (!searchQuery) return defaultCountries;
29
+ const query = searchQuery.toLowerCase();
30
+ return defaultCountries.filter((country) => {
31
+ const parsed = parseCountry(country);
32
+ return (
33
+ parsed.name.toLowerCase().includes(query) ||
34
+ parsed.iso2.toLowerCase().includes(query) ||
35
+ parsed.dialCode.includes(query)
36
+ );
37
+ });
38
+ }, [searchQuery]);
39
+
40
+ const onCountryChange = (e: any) => {
41
+ onChange?.(e.target.value as CountryIso2);
42
+ };
43
+
44
+ const renderCountryContent = useCallback(
45
+ (code: CountryIso2) => (
46
+ <Box
47
+ display="flex"
48
+ alignItems="center"
49
+ flexWrap="nowrap"
50
+ gap={0.5}
51
+ sx={{ cursor: preview ? 'default' : 'pointer' }}>
52
+ <FlagEmoji iso2={code} style={{ display: 'flex', width: '24px', color: 'inherit' }} />
53
+ <Typography>{showFullName ? countryDetail?.name : countryDetail?.iso2}</Typography>
54
+ </Box>
55
+ ),
56
+ [preview, showFullName, countryDetail]
57
+ );
58
+
59
+ if (preview) {
60
+ return renderCountryContent(value);
61
+ }
62
+
63
+ return (
64
+ <Select
65
+ ref={ref}
66
+ MenuProps={{
67
+ style: {
68
+ height: '300px',
69
+ top: '10px',
70
+ },
71
+ anchorOrigin: {
72
+ vertical: 'bottom',
73
+ horizontal: 'left',
74
+ },
75
+ transformOrigin: {
76
+ vertical: 'top',
77
+ horizontal: 'left',
78
+ },
79
+ }}
80
+ sx={{
81
+ width: '100%',
82
+ fieldset: {
83
+ display: 'none',
84
+ },
85
+ '&.Mui-focused:has(div[aria-expanded="false"])': {
86
+ fieldset: {
87
+ display: 'block',
88
+ },
89
+ },
90
+ '.MuiSelect-select': {
91
+ padding: '8px',
92
+ paddingRight: '24px !important',
93
+ },
94
+ svg: {
95
+ right: 0,
96
+ },
97
+ '.MuiMenuItem-root': {
98
+ justifyContent: 'flex-start',
99
+ },
100
+ ...sx,
101
+ }}
102
+ value={value}
103
+ onChange={onCountryChange}
104
+ renderValue={renderCountryContent}>
105
+ <Box sx={{ p: 1, position: 'sticky', top: 0, bgcolor: 'background.paper', zIndex: 1 }}>
106
+ <TextField
107
+ size="small"
108
+ fullWidth
109
+ placeholder="Search country..."
110
+ value={searchQuery}
111
+ onChange={(e) => setSearchQuery(e.target.value)}
112
+ onClick={(e) => e.stopPropagation()}
113
+ onKeyDown={(e) => e.stopPropagation()}
114
+ sx={{
115
+ '& .MuiOutlinedInput-root': {
116
+ '& fieldset': {
117
+ borderColor: 'divider',
118
+ },
119
+ },
120
+ }}
121
+ />
122
+ </Box>
123
+ {filteredCountries.map((c: any) => {
124
+ const parsed = parseCountry(c);
125
+ return (
126
+ <MenuItem key={parsed.iso2} value={parsed.iso2}>
127
+ <FlagEmoji iso2={parsed.iso2} style={{ marginRight: '8px', width: '24px' }} />
128
+ <Typography marginRight="8px">{parsed.name}</Typography>
129
+ <Typography color="gray">+{parsed.dialCode}</Typography>
130
+ </MenuItem>
131
+ );
132
+ })}
133
+ </Select>
134
+ );
135
+ }
136
+ );
137
+
138
+ export default CountrySelect;
@@ -0,0 +1,142 @@
1
+ import { useMemo } from 'react';
2
+ import { Box, InputAdornment, TextField, TextFieldProps, Typography } from '@mui/material';
3
+ import { defaultCountries, CountryIso2, usePhoneInput, parseCountry } from 'react-international-phone';
4
+ import isMobilePhone from 'validator/lib/isMobilePhone';
5
+ import CountrySelect, { CountryDisplayOptions } from './country-select';
6
+
7
+ export interface PhoneValue {
8
+ country: CountryIso2;
9
+ phone: string;
10
+ }
11
+
12
+ export interface PhoneInputProps extends Omit<TextFieldProps, 'value' | 'onChange'> {
13
+ value?: PhoneValue;
14
+ onChange?: (value: PhoneValue) => void;
15
+ countryDisplayOptions?: CountryDisplayOptions;
16
+ preview?: boolean;
17
+ allowDial?: boolean; // 是否允许拨号, 只在 preview 为 true 时有效
18
+ }
19
+
20
+ export function getCountryCallingCode(country: CountryIso2): string {
21
+ const countryData = defaultCountries.find((c) => parseCountry(c).iso2 === country);
22
+ if (countryData) {
23
+ return parseCountry(countryData).dialCode.replace('+', '');
24
+ }
25
+ return '';
26
+ }
27
+
28
+ export function validatePhoneNumber(phone: string): boolean {
29
+ return isMobilePhone(phone.replace(/[\s\-()]+/g, ''), 'any', { strictMode: true });
30
+ }
31
+
32
+ export default function PhoneInput({
33
+ value = { country: 'us', phone: '' },
34
+ onChange,
35
+ placeholder = 'Enter phone number',
36
+ countryDisplayOptions = {},
37
+ preview = false,
38
+ allowDial = true,
39
+ ...props
40
+ }: PhoneInputProps) {
41
+ // 在使用 usePhoneInput 之前处理电话号码
42
+ const phoneWithPrefix = useMemo(() => {
43
+ if (!value.phone) return '';
44
+ // 如果电话号码已经包含 + 号,说明已经有区号了
45
+ if (value.phone.startsWith('+')) return value.phone;
46
+ // 获取国家区号并添加到号码前
47
+ const countryCode = getCountryCallingCode(value.country);
48
+ return `+${countryCode}${value.phone}`;
49
+ }, [value.phone, value.country]);
50
+
51
+ const { phone, handlePhoneValueChange, country, setCountry } = usePhoneInput({
52
+ defaultCountry: value.country,
53
+ value: phoneWithPrefix,
54
+ countries: defaultCountries,
55
+ onChange: (data) => {
56
+ // 确保类型匹配 PhoneValue 接口
57
+ const phoneValue: PhoneValue = {
58
+ country: data.country,
59
+ phone: data.phone,
60
+ };
61
+ onChange?.(phoneValue);
62
+ },
63
+ });
64
+
65
+ const onCountryChange = (v: CountryIso2) => {
66
+ if (!preview) {
67
+ setCountry(v);
68
+ }
69
+ };
70
+
71
+ if (preview) {
72
+ const isValid = phone && validatePhoneNumber(phone);
73
+ const canDial = allowDial && isValid;
74
+
75
+ return (
76
+ <Box
77
+ display="flex"
78
+ alignItems="center"
79
+ component={canDial ? 'a' : 'div'}
80
+ href={canDial ? `tel:${phone}` : undefined}
81
+ sx={{
82
+ textDecoration: 'none',
83
+ color: 'inherit',
84
+ cursor: canDial ? 'pointer' : 'default',
85
+ '&:hover': {
86
+ opacity: canDial ? 0.8 : 1,
87
+ },
88
+ }}>
89
+ <CountrySelect value={country} preview={preview} />
90
+ <Typography sx={{ ml: 0.5 }}>{phone}</Typography>
91
+ </Box>
92
+ );
93
+ }
94
+ return (
95
+ <TextField
96
+ {...props}
97
+ value={phone}
98
+ onChange={preview ? undefined : handlePhoneValueChange}
99
+ placeholder={placeholder}
100
+ className="phone-input"
101
+ disabled={preview}
102
+ sx={{
103
+ '&>.MuiInputBase-root': {
104
+ paddingLeft: '8px',
105
+ cursor: preview ? 'default' : undefined,
106
+ },
107
+ ...(props.sx ?? {}),
108
+ ...(preview
109
+ ? {
110
+ '& .Mui-disabled': {
111
+ '-webkit-text-fill-color': 'inherit',
112
+ color: 'inherit',
113
+ },
114
+ fieldset: {
115
+ display: 'none',
116
+ },
117
+ }
118
+ : {}),
119
+ }}
120
+ InputProps={{
121
+ startAdornment: (
122
+ <InputAdornment position="start" style={{ marginRight: '2px', marginLeft: '-8px' }}>
123
+ <CountrySelect
124
+ value={country}
125
+ onChange={onCountryChange}
126
+ preview={preview}
127
+ selectCountryProps={countryDisplayOptions}
128
+ sx={{
129
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
130
+ borderColor: 'transparent',
131
+ },
132
+ opacity: preview ? 1 : undefined,
133
+ cursor: preview ? 'default' : undefined,
134
+ }}
135
+ />
136
+ </InputAdornment>
137
+ ),
138
+ ...(props.InputProps ?? {}),
139
+ }}
140
+ />
141
+ );
142
+ }
@@ -40,6 +40,7 @@ export default function UnLogin({ session, onLogin = noop, size = 24, dark = fal
40
40
  const t = useMemoizedFn((key, data = {}) => {
41
41
  return translate(translations, key, locale, 'en', data);
42
42
  });
43
+ const searchParams = new URLSearchParams(window.location.search);
43
44
  const browser = useBrowser();
44
45
 
45
46
  const isFirstLoading = false;
@@ -74,7 +75,7 @@ export default function UnLogin({ session, onLogin = noop, size = 24, dark = fal
74
75
  let timer: NodeJS.Timeout;
75
76
 
76
77
  // NOTICE: ArcSphere 中使用原生的快捷登录
77
- if (!browser.arcSphere) {
78
+ if (!browser.arcSphere && searchParams.get('showQuickConnect') !== 'false') {
78
79
  timer = setTimeout(async () => {
79
80
  currentState.userSessions = await session.getUserSessions();
80
81
  if (currentState.userSessions.length > 0) {
@@ -20,7 +20,7 @@ export const PASSPORT_STATUS = {
20
20
  };
21
21
 
22
22
  export const LOGIN_PROVIDER = {
23
- // EMAIL: 'Email',
23
+ EMAIL: 'email',
24
24
  AUTH0: 'auth0',
25
25
  APPLE: 'apple',
26
26
  GITHUB: 'github',
@@ -31,8 +31,8 @@ export const LOGIN_PROVIDER = {
31
31
  };
32
32
 
33
33
  export const LOGIN_PROVIDER_NAME = {
34
- // [LOGIN_PROVIDER.EMAIL]: 'Email',
35
- [LOGIN_PROVIDER.AUTH0]: 'Email',
34
+ [LOGIN_PROVIDER.EMAIL]: 'Email',
35
+ [LOGIN_PROVIDER.AUTH0]: 'Auth0',
36
36
  [LOGIN_PROVIDER.APPLE]: 'Apple',
37
37
  [LOGIN_PROVIDER.GITHUB]: 'Github',
38
38
  [LOGIN_PROVIDER.GOOGLE]: 'Google',