@arcblock/did-connect-react 3.1.0

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 (183) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +134 -0
  3. package/lib/Address/index.js +4 -0
  4. package/lib/Avatar/index.js +4 -0
  5. package/lib/Button/index.js +17 -0
  6. package/lib/Connect/assets/locale.js +143 -0
  7. package/lib/Connect/assets/login-bg.png +0 -0
  8. package/lib/Connect/assets/login-slogan.js +9 -0
  9. package/lib/Connect/components/action-button.js +26 -0
  10. package/lib/Connect/components/app-tips.js +132 -0
  11. package/lib/Connect/components/auto-height.js +31 -0
  12. package/lib/Connect/components/back-button.js +24 -0
  13. package/lib/Connect/components/connect-status.js +263 -0
  14. package/lib/Connect/components/did-connect-title.js +126 -0
  15. package/lib/Connect/components/download-tips.js +52 -0
  16. package/lib/Connect/components/loading.js +26 -0
  17. package/lib/Connect/components/login-item/connect-choose-list.js +249 -0
  18. package/lib/Connect/components/login-item/login-method-item.js +129 -0
  19. package/lib/Connect/components/login-item/mobile-login-item.js +114 -0
  20. package/lib/Connect/components/login-item/passkey-login-item.js +44 -0
  21. package/lib/Connect/components/login-item/web-login-item.js +97 -0
  22. package/lib/Connect/components/mask-overlay.js +34 -0
  23. package/lib/Connect/components/refresh-overlay.js +57 -0
  24. package/lib/Connect/components/switch-app.js +70 -0
  25. package/lib/Connect/contexts/state.js +142 -0
  26. package/lib/Connect/fullpage.js +5 -0
  27. package/lib/Connect/hooks/auth-url.js +23 -0
  28. package/lib/Connect/hooks/method-list.js +46 -0
  29. package/lib/Connect/hooks/page-show.js +17 -0
  30. package/lib/Connect/hooks/security.js +27 -0
  31. package/lib/Connect/hooks/token.js +305 -0
  32. package/lib/Connect/hooks/use-apps.js +19 -0
  33. package/lib/Connect/hooks/use-quick-connect.js +97 -0
  34. package/lib/Connect/index.js +498 -0
  35. package/lib/Connect/landing-page.js +5 -0
  36. package/lib/Connect/plugins/email/index.js +62 -0
  37. package/lib/Connect/plugins/email/list-item.js +28 -0
  38. package/lib/Connect/plugins/email/placeholder.js +283 -0
  39. package/lib/Connect/plugins/index.js +4 -0
  40. package/lib/Connect/use-connect.js +164 -0
  41. package/lib/Connect/with-blocklet.js +15 -0
  42. package/lib/Connect/with-bridge-call.js +108 -0
  43. package/lib/Federated/context.js +61 -0
  44. package/lib/Federated/index.js +7 -0
  45. package/lib/Logo/index.js +4 -0
  46. package/lib/OAuth/context.js +234 -0
  47. package/lib/OAuth/guest.svg.js +5 -0
  48. package/lib/OAuth/index.js +7 -0
  49. package/lib/OAuth/passport-switcher.js +114 -0
  50. package/lib/Passkey/actions.js +165 -0
  51. package/lib/Passkey/constants.js +4 -0
  52. package/lib/Passkey/context.js +266 -0
  53. package/lib/Passkey/dialog.js +277 -0
  54. package/lib/Passkey/icon.js +13 -0
  55. package/lib/Passkey/index.js +9 -0
  56. package/lib/Service/index.js +62 -0
  57. package/lib/Session/assets/did-spaces-guide-cover.svg.js +135 -0
  58. package/lib/Session/assets/did-spaces-guide-icon.svg.js +9 -0
  59. package/lib/Session/context.js +5 -0
  60. package/lib/Session/did-spaces-guide.js +136 -0
  61. package/lib/Session/hooks/use-federated.js +64 -0
  62. package/lib/Session/hooks/use-mobile.js +8 -0
  63. package/lib/Session/hooks/use-protected-routes.js +11 -0
  64. package/lib/Session/hooks/use-session-token.js +169 -0
  65. package/lib/Session/hooks/use-verify.js +45 -0
  66. package/lib/Session/index.js +896 -0
  67. package/lib/Session/libs/constants.js +15 -0
  68. package/lib/Session/libs/did-spaces.js +10 -0
  69. package/lib/Session/libs/federated.js +42 -0
  70. package/lib/Session/libs/index.js +15 -0
  71. package/lib/Session/libs/locales.js +161 -0
  72. package/lib/Session/libs/login-mobile.js +55 -0
  73. package/lib/Session/window-focus-aware.js +17 -0
  74. package/lib/SessionManager/index.js +4 -0
  75. package/lib/Storage/engine/cookie.js +21 -0
  76. package/lib/Storage/engine/local-storage.js +36 -0
  77. package/lib/Storage/index.js +23 -0
  78. package/lib/User/index.js +6 -0
  79. package/lib/User/use-did.js +59 -0
  80. package/lib/User/wrap-did.js +13 -0
  81. package/lib/WebWalletSWKeeper/index.js +5 -0
  82. package/lib/constant.js +22 -0
  83. package/lib/error.js +8 -0
  84. package/lib/hooks/use-locale.js +7 -0
  85. package/lib/index.js +33 -0
  86. package/lib/locales/en.js +17 -0
  87. package/lib/locales/index.js +10 -0
  88. package/lib/locales/zh.js +17 -0
  89. package/lib/package.json.js +7 -0
  90. package/lib/types.d.ts +355 -0
  91. package/lib/utils.js +214 -0
  92. package/package.json +84 -0
  93. package/src/Address/index.jsx +2 -0
  94. package/src/Avatar/index.jsx +2 -0
  95. package/src/Button/Button.stories.jsx +7 -0
  96. package/src/Button/index.jsx +21 -0
  97. package/src/Connect/Connect.stories.jsx +34 -0
  98. package/src/Connect/assets/locale.js +145 -0
  99. package/src/Connect/assets/login-bg.png +0 -0
  100. package/src/Connect/assets/login-slogan.js +7 -0
  101. package/src/Connect/components/action-button.jsx +22 -0
  102. package/src/Connect/components/app-tips.jsx +156 -0
  103. package/src/Connect/components/auto-height.jsx +38 -0
  104. package/src/Connect/components/back-button.jsx +23 -0
  105. package/src/Connect/components/connect-status.jsx +259 -0
  106. package/src/Connect/components/did-connect-title.jsx +106 -0
  107. package/src/Connect/components/download-tips.jsx +55 -0
  108. package/src/Connect/components/loading.jsx +25 -0
  109. package/src/Connect/components/login-item/connect-choose-list.jsx +304 -0
  110. package/src/Connect/components/login-item/login-method-item.jsx +118 -0
  111. package/src/Connect/components/login-item/mobile-login-item.jsx +179 -0
  112. package/src/Connect/components/login-item/passkey-login-item.jsx +52 -0
  113. package/src/Connect/components/login-item/web-login-item.jsx +149 -0
  114. package/src/Connect/components/mask-overlay.jsx +32 -0
  115. package/src/Connect/components/refresh-overlay.jsx +52 -0
  116. package/src/Connect/components/switch-app.jsx +69 -0
  117. package/src/Connect/contexts/state.jsx +219 -0
  118. package/src/Connect/fullpage.jsx +3 -0
  119. package/src/Connect/hooks/auth-url.js +31 -0
  120. package/src/Connect/hooks/method-list.js +121 -0
  121. package/src/Connect/hooks/page-show.js +24 -0
  122. package/src/Connect/hooks/security.js +40 -0
  123. package/src/Connect/hooks/token.js +639 -0
  124. package/src/Connect/hooks/use-apps.js +69 -0
  125. package/src/Connect/hooks/use-quick-connect.js +130 -0
  126. package/src/Connect/index.jsx +600 -0
  127. package/src/Connect/landing-page.jsx +3 -0
  128. package/src/Connect/plugins/email/index.jsx +82 -0
  129. package/src/Connect/plugins/email/list-item.jsx +31 -0
  130. package/src/Connect/plugins/email/placeholder.jsx +365 -0
  131. package/src/Connect/plugins/index.js +2 -0
  132. package/src/Connect/use-connect.jsx +321 -0
  133. package/src/Connect/with-blocklet.jsx +26 -0
  134. package/src/Connect/with-bridge-call.jsx +138 -0
  135. package/src/Federated/context.jsx +93 -0
  136. package/src/Federated/index.jsx +1 -0
  137. package/src/Logo/index.jsx +2 -0
  138. package/src/OAuth/context.jsx +346 -0
  139. package/src/OAuth/guest.svg +20 -0
  140. package/src/OAuth/index.jsx +1 -0
  141. package/src/OAuth/passport-switcher.jsx +133 -0
  142. package/src/Passkey/actions.jsx +212 -0
  143. package/src/Passkey/constants.js +2 -0
  144. package/src/Passkey/context.jsx +381 -0
  145. package/src/Passkey/dialog.jsx +391 -0
  146. package/src/Passkey/icon.jsx +10 -0
  147. package/src/Passkey/index.jsx +2 -0
  148. package/src/Service/index.jsx +96 -0
  149. package/src/Session/assets/did-spaces-guide-cover.svg +128 -0
  150. package/src/Session/assets/did-spaces-guide-icon.svg +7 -0
  151. package/src/Session/context.jsx +7 -0
  152. package/src/Session/did-spaces-guide.jsx +173 -0
  153. package/src/Session/hooks/use-federated.js +88 -0
  154. package/src/Session/hooks/use-mobile.jsx +6 -0
  155. package/src/Session/hooks/use-protected-routes.js +16 -0
  156. package/src/Session/hooks/use-session-token.js +365 -0
  157. package/src/Session/hooks/use-verify.jsx +76 -0
  158. package/src/Session/index.jsx +1687 -0
  159. package/src/Session/libs/constants.js +14 -0
  160. package/src/Session/libs/did-spaces.js +38 -0
  161. package/src/Session/libs/federated.js +79 -0
  162. package/src/Session/libs/index.js +5 -0
  163. package/src/Session/libs/locales.js +160 -0
  164. package/src/Session/libs/login-mobile.js +80 -0
  165. package/src/Session/window-focus-aware.jsx +28 -0
  166. package/src/SessionManager/index.jsx +2 -0
  167. package/src/Storage/engine/cookie.js +23 -0
  168. package/src/Storage/engine/local-storage.js +55 -0
  169. package/src/Storage/index.js +25 -0
  170. package/src/User/index.js +4 -0
  171. package/src/User/use-did.js +80 -0
  172. package/src/User/wrap-did.jsx +18 -0
  173. package/src/WebWalletSWKeeper/index.jsx +3 -0
  174. package/src/constant.js +26 -0
  175. package/src/error.js +6 -0
  176. package/src/hooks/use-locale.jsx +6 -0
  177. package/src/index.js +43 -0
  178. package/src/locales/en.jsx +15 -0
  179. package/src/locales/index.jsx +13 -0
  180. package/src/locales/zh.jsx +15 -0
  181. package/src/types.d.ts +355 -0
  182. package/src/utils.js +395 -0
  183. package/vite.config.mjs +29 -0
@@ -0,0 +1,304 @@
1
+ import PropTypes from 'prop-types';
2
+ import { Box } from '@mui/material';
3
+ import { LOGIN_PROVIDER, LOGIN_PROVIDER_NAME } from '@arcblock/ux/lib/Util/constant';
4
+ import { getWebWalletUrl } from '@arcblock/ux/lib/Util/wallet';
5
+ import noop from 'lodash/noop';
6
+ import { useMemoizedFn } from 'ahooks';
7
+ import Cookie from 'js-cookie';
8
+ import { getCookieOptions } from '@arcblock/ux/lib/Util';
9
+ import { useEffect, useRef } from 'react';
10
+ import { mergeSx } from '@arcblock/ux/lib/Util/style';
11
+ import ProviderIcon from '@arcblock/ux/lib/DIDConnect/provider-icon';
12
+
13
+ import MobileLoginItem from './mobile-login-item';
14
+ import WebLoginItem from './web-login-item';
15
+ import LoginMethodItem from './login-method-item';
16
+ import { useOAuth } from '../../../OAuth';
17
+ import { useStateContext } from '../../contexts/state';
18
+ import { getApiErrorMessage, getAppId, logger } from '../../../utils';
19
+ import PasskeyLoginItem from './passkey-login-item';
20
+ import { usePasskey } from '../../../Passkey';
21
+ import { useEmailPlugin } from '../../plugins';
22
+
23
+ export default function ConnectChooseList({
24
+ onSuccess = noop,
25
+ onError = noop,
26
+ size = 'small',
27
+ tokenState,
28
+ webWalletUrl = getWebWalletUrl(),
29
+ tokenKey,
30
+ passkeyBehavior = 'none',
31
+ onRest = noop,
32
+ showMobileLogin = true,
33
+ showOAuthLogin = true,
34
+ showPasskeyLogin = true,
35
+ showWebLogin = true,
36
+ showEmailLogin = true,
37
+ oauthProviderList = [],
38
+ slotProps = {},
39
+ disableSwitchApp = false,
40
+ forceUpdate = noop,
41
+ magicToken = undefined,
42
+ baseUrl = '/',
43
+ customItems = [],
44
+ }) {
45
+ const walletLoginRef = useRef(null);
46
+ const webLoginRef = useRef(null);
47
+ const passkeyLoginRef = useRef(null);
48
+ const { loginOAuth, logoutOAuth, t, oauthState } = useOAuth();
49
+ const { passkeyState } = usePasskey();
50
+ const { extraParams, locale, connectState, plugins, setPlugins, setSelectedPlugin, getPlugin } = useStateContext();
51
+
52
+ const handleLoginOAuth = useMemoizedFn(async (item) => {
53
+ tokenState.reset();
54
+ oauthState.reset({
55
+ status: 'scanned',
56
+ });
57
+ passkeyState.reset();
58
+ connectState.chooseMethod = item.provider;
59
+
60
+ const sourceAppPid = extraParams?.sourceAppPid;
61
+ try {
62
+ oauthState.loading = true;
63
+ oauthState.status = 'scanned';
64
+ const loginResult = await loginOAuth(item, {
65
+ action: tokenState.action,
66
+ ...extraParams,
67
+ });
68
+ const cookieOptions = getCookieOptions({ returnDomain: false });
69
+ Cookie.remove('connected_did', cookieOptions);
70
+ Cookie.remove('connected_pk', cookieOptions);
71
+ Cookie.remove('connected_wallet_os', cookieOptions);
72
+
73
+ if (loginResult?.sessionToken) {
74
+ await onSuccess(
75
+ {
76
+ ...loginResult,
77
+ encrypted: false,
78
+ },
79
+ (val) => val,
80
+ {
81
+ sourceAppPid,
82
+ connected_app: getAppId(tokenState.appInfo, tokenState.memberAppInfo),
83
+ }
84
+ );
85
+ oauthState.loading = false;
86
+ oauthState.status = 'succeed';
87
+ }
88
+ } catch (e) {
89
+ logger.error(`Failed login OAuth: ${item.provider}`, e);
90
+ const errorMessage = getApiErrorMessage(e, t('loginOAuthFailed'));
91
+ oauthState.loading = false;
92
+ oauthState.error = errorMessage;
93
+ oauthState.status = 'error';
94
+ await logoutOAuth({ provider: item.provider });
95
+ onError(new Error(errorMessage));
96
+ }
97
+ });
98
+
99
+ const defaultRetryConnect = useMemoizedFn(async () => {
100
+ tokenState.reset();
101
+ await onRest();
102
+ tokenState.status = 'created';
103
+ connectState.chooseMethod = 'wallet';
104
+ });
105
+
106
+ const emailPlugin = useEmailPlugin({ baseUrl });
107
+
108
+ const setupMagicToken = useMemoizedFn(() => {
109
+ if (magicToken && showEmailLogin && plugins.some((plugin) => plugin.name === LOGIN_PROVIDER.EMAIL)) {
110
+ const plugin = getPlugin(LOGIN_PROVIDER.EMAIL);
111
+ if (plugin.state.status === 'idle') {
112
+ plugin.state.reset();
113
+ plugin.state.magicToken = magicToken;
114
+ connectState.chooseMethod = LOGIN_PROVIDER.EMAIL;
115
+ setSelectedPlugin(plugin);
116
+ forceUpdate();
117
+ }
118
+ }
119
+ });
120
+
121
+ // 考虑到切换账号的情况,此时 showEmailLogin 会根据当前上下文的 blocklet 发生变化,所以需要监听 showEmailLogin 的变化
122
+ useEffect(() => {
123
+ const finalList = [];
124
+ if (showEmailLogin) {
125
+ // 尽可能确保在 baseUrl 不变的情况下,不更改 email 插件的数据,以确保页面的展示不会出现闪烁
126
+ const prevEmailPlugin = getPlugin(LOGIN_PROVIDER.EMAIL);
127
+ if (prevEmailPlugin && prevEmailPlugin.baseUrl === emailPlugin.baseUrl) {
128
+ finalList.push(prevEmailPlugin);
129
+ } else {
130
+ finalList.push(emailPlugin);
131
+ }
132
+ }
133
+ setPlugins(finalList);
134
+ connectState.retryConnect = defaultRetryConnect;
135
+ // HACK: 必须要设置延迟,不然拿不到最新的 plugins 的值
136
+ setTimeout(() => {
137
+ setupMagicToken();
138
+ }, 100);
139
+ // eslint-disable-next-line react-hooks/exhaustive-deps
140
+ }, [showEmailLogin]);
141
+
142
+ return (
143
+ <Box className="did-connect__choose" sx={mergeSx({ flex: 1 }, slotProps?.root?.sx)}>
144
+ <Box
145
+ sx={{
146
+ display: 'flex',
147
+ flexDirection: 'column',
148
+ gap: 2,
149
+ }}>
150
+ <Box
151
+ sx={[
152
+ {
153
+ display: 'flex',
154
+ flexDirection: 'column',
155
+ gap: 1.5,
156
+ },
157
+ ]}>
158
+ {showMobileLogin && size !== 'small' && (
159
+ <MobileLoginItem
160
+ ref={walletLoginRef}
161
+ tokenState={tokenState}
162
+ sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
163
+ locale={locale}
164
+ tokenKey={tokenKey}
165
+ disableSwitchApp={disableSwitchApp}
166
+ onClick={async () => {
167
+ tokenState.reset();
168
+ await onRest();
169
+ tokenState.status = 'created';
170
+ connectState.chooseMethod = 'wallet';
171
+ const connectFn = walletLoginRef.current?.connect;
172
+ connectState.retryConnect = () => {
173
+ connectFn(defaultRetryConnect);
174
+ };
175
+ }}
176
+ />
177
+ )}
178
+ {showWebLogin && (
179
+ <WebLoginItem
180
+ ref={webLoginRef}
181
+ tokenState={tokenState}
182
+ webWalletUrl={webWalletUrl}
183
+ sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
184
+ disableSwitchApp={disableSwitchApp}
185
+ onClick={() => {
186
+ // HACK: 在点击插件登录时,直接将状态置为扫码中,避免插件登录时的状态切换
187
+ tokenState.status = 'scanned';
188
+ connectState.chooseMethod = 'wallet-web';
189
+ const connectFn = webLoginRef.current.connect;
190
+ connectState.retryConnect = async () => {
191
+ await onRest();
192
+ tokenState.error = '';
193
+ tokenState.status = 'scanned';
194
+ connectFn();
195
+ };
196
+ }}
197
+ />
198
+ )}
199
+
200
+ {showOAuthLogin || showPasskeyLogin || showEmailLogin ? (
201
+ <Box
202
+ sx={[
203
+ {
204
+ display: 'flex',
205
+ flexDirection: 'column',
206
+ gap: 1.5,
207
+ },
208
+ ]}>
209
+ {/* TODO: @zhanghan 需要将所有 method 转换为 plugin 的形式注入 */}
210
+ {plugins.map((plugin) =>
211
+ plugin.renderListItem({
212
+ key: plugin.name,
213
+ sx: [size === 'small' ? { p: 1 } : { p: 2 }],
214
+ // forceUpdate,
215
+ })
216
+ )}
217
+ {showOAuthLogin
218
+ ? oauthProviderList.map((item) => (
219
+ <LoginMethodItem
220
+ key={item.provider}
221
+ title={LOGIN_PROVIDER_NAME[item.provider]}
222
+ icon={
223
+ <ProviderIcon
224
+ provider={item.provider}
225
+ sx={{
226
+ transform: 'scale(0.95)',
227
+ width: 24,
228
+ height: 24,
229
+ color: 'text.primary',
230
+ }}
231
+ />
232
+ }
233
+ onClick={() => {
234
+ handleLoginOAuth(item);
235
+ connectState.retryConnect = () => {
236
+ handleLoginOAuth(item);
237
+ };
238
+ }}
239
+ sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
240
+ />
241
+ ))
242
+ : null}
243
+ {showPasskeyLogin ? (
244
+ <PasskeyLoginItem
245
+ ref={passkeyLoginRef}
246
+ onSuccess={onSuccess}
247
+ onError={onError}
248
+ tokenState={tokenState}
249
+ behavior={passkeyBehavior}
250
+ sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
251
+ onClick={() => {
252
+ const connectFn = passkeyLoginRef.current.connect;
253
+ connectState.chooseMethod = 'passkey';
254
+ connectState.retryConnect = () => {
255
+ passkeyState.verifying = true;
256
+ connectState.chooseMethod = 'passkey';
257
+ connectFn();
258
+ };
259
+ }}
260
+ slotProps={{
261
+ icon: {
262
+ sx: {
263
+ fontSize: 24,
264
+ '& svg': {
265
+ fontSize: 24,
266
+ width: '1em',
267
+ height: '1em',
268
+ },
269
+ },
270
+ },
271
+ }}
272
+ />
273
+ ) : null}
274
+ </Box>
275
+ ) : null}
276
+ {customItems.map((item) => (!item ? null : item))}
277
+ </Box>
278
+ </Box>
279
+ </Box>
280
+ );
281
+ }
282
+
283
+ ConnectChooseList.propTypes = {
284
+ onSuccess: PropTypes.func,
285
+ onError: PropTypes.func,
286
+ size: PropTypes.oneOf(['small', 'normal', 'large']),
287
+ tokenState: PropTypes.object.isRequired,
288
+ webWalletUrl: PropTypes.string,
289
+ tokenKey: PropTypes.string.isRequired,
290
+ passkeyBehavior: PropTypes.oneOf(['none', 'both', 'only-existing', 'only-new']),
291
+ onRest: PropTypes.func,
292
+ showMobileLogin: PropTypes.bool,
293
+ showOAuthLogin: PropTypes.bool,
294
+ showPasskeyLogin: PropTypes.bool,
295
+ showWebLogin: PropTypes.bool,
296
+ showEmailLogin: PropTypes.bool,
297
+ oauthProviderList: PropTypes.array,
298
+ slotProps: PropTypes.object,
299
+ disableSwitchApp: PropTypes.bool,
300
+ forceUpdate: PropTypes.func,
301
+ magicToken: PropTypes.string,
302
+ baseUrl: PropTypes.string,
303
+ customItems: PropTypes.arrayOf(PropTypes.node),
304
+ };
@@ -0,0 +1,118 @@
1
+ import { Box, Typography, useTheme } from '@mui/material';
2
+ import { Icon } from '@iconify/react';
3
+ import { isValidElement } from 'react';
4
+ import PropTypes from 'prop-types';
5
+ import { mergeSx } from '@arcblock/ux/lib/Util/style';
6
+ import ArrowRightAltRoundedIcon from '@iconify-icons/material-symbols/arrow-right-alt-rounded';
7
+
8
+ export default function LoginMethodItem({
9
+ title,
10
+ description = null,
11
+ icon,
12
+ iconScale = 0.95,
13
+ slotProps = {},
14
+ mode = 'normal',
15
+ ...rest
16
+ }) {
17
+ const theme = useTheme();
18
+
19
+ return (
20
+ <Box
21
+ {...rest}
22
+ sx={mergeSx(
23
+ {
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ gap: 1,
27
+ cursor: 'pointer',
28
+ p: 1,
29
+ borderRadius: 1,
30
+ backgroundColor: 'grey.50',
31
+ textDecoration: 'none',
32
+ transition: 'background-color 0.5s',
33
+ '&:hover, &:active, &.did-connect__choose-item__active': {
34
+ backgroundColor: 'grey.100',
35
+ },
36
+ '& .other-item-icon': {
37
+ opacity: '0',
38
+ transform: 'translateX(-100%)',
39
+ transition: 'transform 0.2s ease, opacity 0.1s ease',
40
+ },
41
+ '&:hover': {
42
+ '& .other-item-icon': {
43
+ display: 'inline-block',
44
+ transform: 'translateX(0)',
45
+ opacity: '1',
46
+ },
47
+ },
48
+ },
49
+ rest?.sx
50
+ )}>
51
+ <Box
52
+ className="arc-login-item__icon"
53
+ sx={mergeSx(
54
+ {
55
+ display: 'flex',
56
+ justifyContent: 'center',
57
+ alignItems: 'center',
58
+ color: 'text.primary',
59
+ },
60
+ slotProps?.icon?.sx
61
+ )}>
62
+ {isValidElement(icon) ? (
63
+ icon
64
+ ) : (
65
+ <Box
66
+ component={Icon}
67
+ icon={icon}
68
+ sx={{
69
+ transform: `scale(${iconScale})`,
70
+ width: 24,
71
+ height: 24,
72
+ }}
73
+ />
74
+ )}
75
+ </Box>
76
+ {mode === 'normal' ? (
77
+ <>
78
+ <Box sx={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
79
+ <Typography
80
+ sx={{
81
+ fontSize: 14,
82
+ fontWeight: '500',
83
+ color: 'text.primary',
84
+ whiteSpace: 'nowrap',
85
+ }}>
86
+ {title}
87
+ </Typography>
88
+ {description ? (
89
+ <Typography
90
+ sx={{
91
+ color: 'text.secondary',
92
+ fontSize: 12,
93
+ lineHeight: 1,
94
+ }}>
95
+ {description}
96
+ </Typography>
97
+ ) : null}
98
+ </Box>
99
+ <Icon
100
+ className="other-item-icon"
101
+ icon={ArrowRightAltRoundedIcon}
102
+ fontSize="1.3rem"
103
+ color={theme.palette.primary.main}
104
+ />
105
+ </>
106
+ ) : null}
107
+ </Box>
108
+ );
109
+ }
110
+
111
+ LoginMethodItem.propTypes = {
112
+ title: PropTypes.string.isRequired,
113
+ description: PropTypes.string,
114
+ icon: PropTypes.any.isRequired,
115
+ iconScale: PropTypes.number,
116
+ slotProps: PropTypes.object,
117
+ mode: PropTypes.oneOf(['simple', 'normal']),
118
+ };
@@ -0,0 +1,179 @@
1
+ import PropTypes from 'prop-types';
2
+ import { CircularProgress } from '@mui/material';
3
+ import { useCreation, useMemoizedFn, useReactive } from 'ahooks';
4
+ import bridge from '@arcblock/bridge';
5
+ import { useBrowser } from '@arcblock/react-hooks';
6
+ import noop from 'lodash/noop';
7
+ import { useImperativeHandle } from 'react';
8
+ import { mergeSx } from '@arcblock/ux/lib/Util/style';
9
+ import ProviderIcon from '@arcblock/ux/lib/DIDConnect/provider-icon';
10
+ import { LOGIN_PROVIDER } from '@arcblock/ux/lib/Util/constant';
11
+
12
+ import { useStateContext } from '../../contexts/state';
13
+ import LoginMethodItem from './login-method-item';
14
+ import { logger } from '../../../utils';
15
+ import useAuthUrl from '../../hooks/auth-url';
16
+
17
+ export default function MobileLoginItem({
18
+ ref = null,
19
+ tokenState,
20
+ locale,
21
+ tokenKey,
22
+ onClick = noop,
23
+ disableSwitchApp = false,
24
+ ...rest
25
+ }) {
26
+ const currentState = useReactive({
27
+ loading: false,
28
+ });
29
+ const { isWalletWebview } = useStateContext();
30
+ const title = 'DID Wallet';
31
+ const browser = useBrowser();
32
+
33
+ const callbackUrl = new URL(window.location.href);
34
+ const urlWithParams = useAuthUrl({ disableSwitchApp, tokenState });
35
+
36
+ const deepLink = useCreation(() => {
37
+ if (!tokenState.url) {
38
+ return '';
39
+ }
40
+
41
+ callbackUrl.searchParams.set('tokenKey', tokenState.token);
42
+
43
+ const link = new URL(urlWithParams);
44
+ if (!browser.wallet) {
45
+ link.searchParams.set('callback', encodeURIComponent(callbackUrl));
46
+ link.searchParams.set('callback_delay', 1500);
47
+ }
48
+ link.searchParams.set('locale', locale);
49
+ const encoder = new TextEncoder();
50
+ const currentUrlLength = link.href.length;
51
+
52
+ let appInfo = '';
53
+ let memberAppInfo = '';
54
+ try {
55
+ if (tokenState.appInfo) {
56
+ const encoded = encoder.encode(JSON.stringify(tokenState.appInfo));
57
+ appInfo = btoa(String.fromCharCode(...encoded));
58
+ }
59
+ if (tokenState.memberAppInfo) {
60
+ const encoded = encoder.encode(JSON.stringify(tokenState.memberAppInfo));
61
+ memberAppInfo = btoa(String.fromCharCode(...encoded));
62
+ }
63
+ // NOTICE: 当长度过长时,不再添加 appInfo & memberAppInfo
64
+ if (
65
+ currentUrlLength + 'appInfo='.length + appInfo.length + 'memberAppInfo='.length + memberAppInfo.length >
66
+ 2000
67
+ ) {
68
+ logger.warn('URL too long, drop appInfo & memberAppInfo', { appInfo, memberAppInfo });
69
+ throw new Error('URL too long');
70
+ }
71
+
72
+ if (memberAppInfo) {
73
+ if (appInfo) {
74
+ // 在有 memberAppInfo 的情况下,两个值必须一起写入
75
+ link.searchParams.set('appInfo', appInfo);
76
+ link.searchParams.set('memberAppInfo', memberAppInfo);
77
+ }
78
+ } else if (appInfo) {
79
+ link.searchParams.set('appInfo', appInfo);
80
+ }
81
+ } catch (error) {
82
+ logger.warn('Failed to convert appInfo & memberAppInfo', error);
83
+ }
84
+ return link.href.replace(/^https?:\/\//, 'abt://');
85
+ }, [tokenKey, tokenState.token, tokenState.appInfo, tokenState.memberAppInfo, tokenState.url]);
86
+
87
+ const handleWalletConnect = useMemoizedFn(() => {
88
+ currentState.loading = true;
89
+ setTimeout(() => {
90
+ bridge.call('authAction', { action: 'auth', deepLink });
91
+ }, 600);
92
+ setTimeout(() => {
93
+ currentState.loading = false;
94
+ }, 2000);
95
+ });
96
+
97
+ // 此处用于在 mobile 浏览器中,用户在点击打开按钮时,直接渲染 scanned 的样式(解决 safari 在页面隐藏后,无法正常发起请求带来的体验较差)
98
+ // 需要在页面再次可见时,将状态设置为正确的值
99
+ const handleOpenDeeplink = useMemoizedFn(() => {
100
+ tokenState.checking = true;
101
+ tokenState.status = 'scanned';
102
+ });
103
+
104
+ const modifyProps = {};
105
+ if (!isWalletWebview && browser.mobile.any) {
106
+ modifyProps.component = 'a';
107
+ modifyProps.href = deepLink;
108
+ }
109
+
110
+ if (!tokenState.url) {
111
+ modifyProps.sx = {
112
+ cursor: 'not-allowed',
113
+ };
114
+ }
115
+
116
+ const handleConnectFn = useMemoizedFn((fallback) => {
117
+ if (!tokenState.url) {
118
+ return;
119
+ }
120
+
121
+ if (isWalletWebview) {
122
+ handleWalletConnect();
123
+ } else if (browser.mobile.any) {
124
+ handleOpenDeeplink();
125
+ } else {
126
+ fallback();
127
+ }
128
+ });
129
+
130
+ const handleConnect = useMemoizedFn(() => {
131
+ handleConnectFn(onClick);
132
+ });
133
+
134
+ useImperativeHandle(ref, () => ({
135
+ connect: handleConnectFn,
136
+ }));
137
+
138
+ return (
139
+ <LoginMethodItem
140
+ {...rest}
141
+ {...modifyProps}
142
+ sx={mergeSx(rest?.sx, modifyProps?.sx)}
143
+ title={title}
144
+ icon={
145
+ currentState.loading || !tokenState.url ? (
146
+ <CircularProgress
147
+ color="primary"
148
+ size={24}
149
+ sx={{
150
+ '& svg': {
151
+ transform: 'scale(0.75)',
152
+ },
153
+ }}
154
+ />
155
+ ) : (
156
+ <ProviderIcon
157
+ width="24px"
158
+ height="24px"
159
+ provider={LOGIN_PROVIDER.DID_WALLET}
160
+ style={{
161
+ transform: 'scale(0.9)',
162
+ }}
163
+ />
164
+ )
165
+ }
166
+ iconScale={1}
167
+ onClick={handleConnect}
168
+ />
169
+ );
170
+ }
171
+
172
+ MobileLoginItem.propTypes = {
173
+ tokenState: PropTypes.object.isRequired,
174
+ tokenKey: PropTypes.string.isRequired,
175
+ locale: PropTypes.string.isRequired,
176
+ onClick: PropTypes.func,
177
+ disableSwitchApp: PropTypes.bool,
178
+ ref: PropTypes.any,
179
+ };
@@ -0,0 +1,52 @@
1
+ import PropTypes from 'prop-types';
2
+ import { useImperativeHandle, useRef } from 'react';
3
+ import { useMemoizedFn } from 'ahooks';
4
+
5
+ import PasskeyActions from '../../../Passkey/actions';
6
+ import { getAppId } from '../../../utils';
7
+ import { useStateContext } from '../../contexts/state';
8
+
9
+ export default function PasskeyLoginItem({ ref = null, onSuccess, tokenState, behavior = 'none', ...rest }) {
10
+ const { extraParams } = useStateContext();
11
+ const passkeyActionRef = useRef(null);
12
+
13
+ const handleSuccess = useMemoizedFn((result) =>
14
+ onSuccess(
15
+ {
16
+ ...result,
17
+ encrypted: false,
18
+ },
19
+ (val) => val,
20
+ {
21
+ sourceAppPid: extraParams?.sourceAppPid,
22
+ connected_app: getAppId(tokenState.appInfo, tokenState.memberAppInfo),
23
+ }
24
+ )
25
+ );
26
+
27
+ useImperativeHandle(ref, () => ({
28
+ connect: passkeyActionRef.current.click,
29
+ }));
30
+
31
+ if (behavior === 'none') {
32
+ return null;
33
+ }
34
+
35
+ return (
36
+ <PasskeyActions
37
+ {...rest}
38
+ ref={passkeyActionRef}
39
+ action={tokenState.action}
40
+ behavior={behavior}
41
+ onSuccess={handleSuccess}
42
+ extraParams={extraParams}
43
+ />
44
+ );
45
+ }
46
+
47
+ PasskeyLoginItem.propTypes = {
48
+ onSuccess: PropTypes.func.isRequired,
49
+ tokenState: PropTypes.object.isRequired,
50
+ behavior: PropTypes.oneOf(['none', 'both', 'only-existing', 'only-new']),
51
+ ref: PropTypes.any,
52
+ };