@arcblock/did-connect-react 3.4.0 → 3.4.2

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 (28) hide show
  1. package/lib/Connect/assets/locale.js +4 -2
  2. package/lib/Connect/components/login-item/connect-choose-list.js +120 -114
  3. package/lib/Connect/components/login-item/connect-provider-list.js +187 -180
  4. package/lib/Connect/components/login-item/login-method-item.js +63 -47
  5. package/lib/Connect/components/login-item/passkey-login-item.js +17 -15
  6. package/lib/Connect/components/login-item/wallet-login-options.js +42 -40
  7. package/lib/Connect/connect.js +116 -117
  8. package/lib/Connect/contexts/state.js +73 -64
  9. package/lib/Connect/hooks/provider-list.js +43 -33
  10. package/lib/Connect/hooks/token.js +121 -122
  11. package/lib/Connect/plugins/email/index.js +15 -12
  12. package/lib/Connect/plugins/email/list-item.js +19 -18
  13. package/lib/Passkey/actions.js +80 -75
  14. package/lib/package.json.js +1 -1
  15. package/package.json +5 -5
  16. package/src/Connect/assets/locale.js +2 -0
  17. package/src/Connect/components/login-item/connect-choose-list.jsx +12 -5
  18. package/src/Connect/components/login-item/connect-provider-list.jsx +11 -4
  19. package/src/Connect/components/login-item/login-method-item.jsx +24 -7
  20. package/src/Connect/components/login-item/passkey-login-item.jsx +3 -1
  21. package/src/Connect/components/login-item/wallet-login-options.jsx +3 -2
  22. package/src/Connect/connect.jsx +36 -35
  23. package/src/Connect/contexts/state.jsx +10 -0
  24. package/src/Connect/hooks/provider-list.js +43 -17
  25. package/src/Connect/hooks/token.js +13 -13
  26. package/src/Connect/plugins/email/index.jsx +3 -0
  27. package/src/Connect/plugins/email/list-item.jsx +3 -2
  28. package/src/Passkey/actions.jsx +5 -0
@@ -1,28 +1,28 @@
1
1
  import { jsxs as q, Fragment as D, jsx as n } from "react/jsx-runtime";
2
- import { useRef as I, useEffect as F, useImperativeHandle as M } from "react";
2
+ import { useRef as I, useEffect as L, useImperativeHandle as M } from "react";
3
3
  import z from "lodash/trim";
4
4
  import e from "prop-types";
5
5
  import k from "js-cookie";
6
- import { CircularProgress as L } from "@mui/material";
6
+ import { CircularProgress as B } from "@mui/material";
7
7
  import { Icon as S } from "@iconify/react";
8
- import { getCookieOptions as B } from "@arcblock/ux/lib/Util";
8
+ import { getCookieOptions as H } from "@arcblock/ux/lib/Util";
9
9
  import { useMemoizedFn as v } from "ahooks";
10
- import H from "@iconify-icons/material-symbols/passkey-rounded";
11
- import V from "@iconify-icons/material-symbols/person-add-rounded";
12
- import $ from "lodash/noop";
13
- import { mergeSx as G } from "@arcblock/ux/lib/Util/style";
14
- import { usePasskey as K } from "./context.js";
15
- import { logger as N, getWebAuthnErrorMessage as W } from "../utils.js";
16
- import Y from "../Connect/components/login-item/login-method-item.js";
17
- import J from "./dialog.js";
10
+ import V from "@iconify-icons/material-symbols/passkey-rounded";
11
+ import $ from "@iconify-icons/material-symbols/person-add-rounded";
12
+ import G from "lodash/noop";
13
+ import { mergeSx as K } from "@arcblock/ux/lib/Util/style";
14
+ import { usePasskey as N } from "./context.js";
15
+ import { logger as W, getWebAuthnErrorMessage as Y } from "../utils.js";
16
+ import J from "../Connect/components/login-item/login-method-item.js";
17
+ import Q from "./dialog.js";
18
18
  import { VERIFY_CODE_LENGTH as w } from "./constants.js";
19
- function x({ action: d, onClick: a, title: i, state: c, dense: f = !1, icon: l, ...u }) {
19
+ function x({ action: d, onClick: c, title: s, state: f, dense: l = !1, icon: u, ...m }) {
20
20
  return /* @__PURE__ */ n(
21
- Y,
21
+ J,
22
22
  {
23
- ...u,
24
- icon: c[d] ? /* @__PURE__ */ n(
25
- L,
23
+ ...m,
24
+ icon: f[d] ? /* @__PURE__ */ n(
25
+ B,
26
26
  {
27
27
  size: "1em",
28
28
  sx: {
@@ -32,11 +32,11 @@ function x({ action: d, onClick: a, title: i, state: c, dense: f = !1, icon: l,
32
32
  }
33
33
  }
34
34
  }
35
- ) : l,
35
+ ) : u,
36
36
  iconScale: 1,
37
- title: i,
38
- onClick: a,
39
- sx: G(f ? { p: 1, backgroundColor: "transparent" } : {}, u?.sx)
37
+ title: s,
38
+ onClick: c,
39
+ sx: K(l ? { p: 1, backgroundColor: "transparent" } : {}, m?.sx)
40
40
  }
41
41
  );
42
42
  }
@@ -48,104 +48,108 @@ x.propTypes = {
48
48
  dense: e.bool,
49
49
  icon: e.any.isRequired
50
50
  };
51
- function Q({
51
+ function U({
52
52
  ref: d = null,
53
- action: a,
54
- behavior: i = "none",
55
- onSuccess: c,
56
- onError: f,
57
- extraParams: l = {},
58
- createButtonText: u = "",
53
+ action: c,
54
+ behavior: s = "none",
55
+ onSuccess: f,
56
+ onError: l,
57
+ extraParams: u = {},
58
+ createButtonText: m = "",
59
59
  createMode: T = "register",
60
60
  dense: P = !1,
61
61
  slotProps: R = {},
62
- sx: h = {},
63
- mode: b = "normal",
64
- onClick: E = $
62
+ sx: b = {},
63
+ mode: h = "normal",
64
+ onClick: E = G,
65
+ isLatest: j = !1
65
66
  }) {
66
- const m = I(""), C = I(null), { t: p, loginPasskey: j, logoutPasskey: O, passkeyState: r } = K(), _ = v(async (s = "") => {
67
+ const p = I(""), C = I(null), { t: i, loginPasskey: O, logoutPasskey: _, passkeyState: o } = N(), A = v(async (a = "") => {
67
68
  try {
68
- r.verifying = !0, r.error = "", s || (r.verifyingStatus = "");
69
- const o = await j({
70
- ...l,
71
- action: a,
72
- credentialId: s
73
- }), t = B({ expireInDays: 7 });
74
- k.remove("connected_did", t), k.remove("connected_pk", t), k.remove("connected_wallet_os", t), r.verifying = !1, o?.sessionToken && (await c({ ...o, encrypted: !1 }, (A) => A), r.verifyingStatus = "succeed");
75
- } catch (o) {
76
- N.error("Failed to verify passkey", o);
77
- const t = W(o, p("verifyPasskeyFailed"), p);
78
- r.verifying = !1, r.error = t, r.verifyingStatus = "error", await O(), f(new Error(t));
69
+ o.verifying = !0, o.error = "", a || (o.verifyingStatus = "");
70
+ const r = await O({
71
+ ...u,
72
+ action: c,
73
+ credentialId: a
74
+ }), t = H({ expireInDays: 7 });
75
+ k.remove("connected_did", t), k.remove("connected_pk", t), k.remove("connected_wallet_os", t), o.verifying = !1, r?.sessionToken && (await f({ ...r, encrypted: !1 }, (F) => F), o.verifyingStatus = "succeed");
76
+ } catch (r) {
77
+ W.error("Failed to verify passkey", r);
78
+ const t = Y(r, i("verifyPasskeyFailed"), i);
79
+ o.verifying = !1, o.error = t, o.verifyingStatus = "error", await _(), l(new Error(t));
79
80
  }
80
81
  }), y = v(
81
- (s) => {
82
- if (!r.sent) return;
83
- s.preventDefault();
84
- const o = z(s.clipboardData.getData("text/plain").slice(0, w));
85
- if (!/^\d+$/.test(o)) return;
86
- r.code = o.padEnd(w, " ");
87
- const t = document.getElementById(`code-input-${o.length}`);
82
+ (a) => {
83
+ if (!o.sent) return;
84
+ a.preventDefault();
85
+ const r = z(a.clipboardData.getData("text/plain").slice(0, w));
86
+ if (!/^\d+$/.test(r)) return;
87
+ o.code = r.padEnd(w, " ");
88
+ const t = document.getElementById(`code-input-${r.length}`);
88
89
  t && t.focus();
89
90
  },
90
- [r]
91
+ [o]
91
92
  );
92
- F(() => (document.addEventListener("paste", y), () => {
93
+ L(() => (document.addEventListener("paste", y), () => {
93
94
  document.removeEventListener("paste", y);
94
95
  }), [y]);
95
96
  const g = v(() => {
96
- m.current === "verifying" ? _() : m.current === "creating" && C.current.open();
97
+ p.current === "verifying" ? A() : p.current === "creating" && C.current.open();
97
98
  });
98
99
  return M(d, () => ({
99
100
  click: g
100
- })), i === "none" ? null : /* @__PURE__ */ q(D, { children: [
101
- ["both", "only-existing"].includes(i) ? /* @__PURE__ */ n(
101
+ })), s === "none" ? null : /* @__PURE__ */ q(D, { children: [
102
+ ["both", "only-existing"].includes(s) ? /* @__PURE__ */ n(
102
103
  x,
103
104
  {
105
+ isLatest: j,
106
+ t: i,
104
107
  action: "verifying",
105
- state: r,
106
- title: p("usePasskey"),
108
+ state: o,
109
+ title: i("usePasskey"),
107
110
  onClick: () => {
108
- m.current = "verifying", g(), E();
111
+ p.current = "verifying", g(), E();
109
112
  },
110
113
  dense: P,
111
114
  slotProps: R,
112
- sx: h,
113
- mode: b,
114
- icon: /* @__PURE__ */ n(S, { icon: H, fontSize: "1em" })
115
+ sx: b,
116
+ mode: h,
117
+ icon: /* @__PURE__ */ n(S, { icon: V, fontSize: "1em" })
115
118
  }
116
119
  ) : null,
117
- ["both", "only-new"].includes(i) ? /* @__PURE__ */ q(D, { children: [
120
+ ["both", "only-new"].includes(s) ? /* @__PURE__ */ q(D, { children: [
118
121
  /* @__PURE__ */ n(
119
122
  x,
120
123
  {
124
+ t: i,
121
125
  action: "creating",
122
- state: r,
123
- title: u || p("createPasskey"),
126
+ state: o,
127
+ title: m || i("createPasskey"),
124
128
  onClick: () => {
125
- m.current = "creating", g(), E();
129
+ p.current = "creating", g(), E();
126
130
  },
127
131
  dense: P,
128
132
  slotProps: R,
129
- sx: h,
130
- mode: b,
131
- icon: /* @__PURE__ */ n(S, { icon: V, fontSize: "1em" })
133
+ sx: b,
134
+ mode: h,
135
+ icon: /* @__PURE__ */ n(S, { icon: $, fontSize: "1em" })
132
136
  }
133
137
  ),
134
138
  /* @__PURE__ */ n(
135
- J,
139
+ Q,
136
140
  {
137
141
  ref: C,
138
- action: a,
142
+ action: c,
139
143
  createMode: T,
140
- extraParams: l,
141
- onSuccess: c,
142
- onError: f
144
+ extraParams: u,
145
+ onSuccess: f,
146
+ onError: l
143
147
  }
144
148
  )
145
149
  ] }) : null
146
150
  ] });
147
151
  }
148
- Q.propTypes = {
152
+ U.propTypes = {
149
153
  action: e.string.isRequired,
150
154
  onSuccess: e.func.isRequired,
151
155
  onError: e.func.isRequired,
@@ -158,8 +162,9 @@ Q.propTypes = {
158
162
  sx: e.oneOfType([e.object, e.array]),
159
163
  mode: e.oneOf(["simple", "normal"]),
160
164
  onClick: e.func,
161
- ref: e.any
165
+ ref: e.any,
166
+ isLatest: e.bool
162
167
  };
163
168
  export {
164
- Q as default
169
+ U as default
165
170
  };
@@ -1,4 +1,4 @@
1
- const o = "3.4.0", s = {
1
+ const o = "3.4.2", s = {
2
2
  version: o
3
3
  };
4
4
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/did-connect-react",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "Client side library to work with DID Connect by ArcBlock.",
5
5
  "keywords": [
6
6
  "react",
@@ -32,10 +32,10 @@
32
32
  "url": "https://github.com/ArcBlock/ux/issues"
33
33
  },
34
34
  "dependencies": {
35
- "@arcblock/bridge": "3.4.0",
35
+ "@arcblock/bridge": "3.4.2",
36
36
  "@arcblock/did": "^1.28.1",
37
- "@arcblock/icons": "3.4.0",
38
- "@arcblock/react-hooks": "3.4.0",
37
+ "@arcblock/icons": "3.4.2",
38
+ "@arcblock/react-hooks": "3.4.2",
39
39
  "@arcblock/ws": "^1.28.1",
40
40
  "@blocklet/constant": "^1.17.7",
41
41
  "@fontsource/lexend": "^5.2.9",
@@ -81,5 +81,5 @@
81
81
  "eslint-plugin-react-hooks": "^4.6.2",
82
82
  "jest": "^29.7.0"
83
83
  },
84
- "gitHead": "bbf3a178a14a767aef0da795ea31abe8dfb858c1"
84
+ "gitHead": "9ba657c3d15f93dc93e9029845631ececf385afc"
85
85
  }
@@ -71,6 +71,7 @@ export default {
71
71
  didConnectTitle: 'DID Connect',
72
72
  didConnectDescription: 'Decentralized identities and apps, W3C DID-based, future-ready.',
73
73
  noAuthenticationProvider: 'No authentication provider configured',
74
+ lastUsed: 'Last Used',
74
75
  },
75
76
  zh: {
76
77
  generateError: '二维码生成失败',
@@ -143,5 +144,6 @@ export default {
143
144
  didConnectTitle: 'DID Connect',
144
145
  didConnectDescription: '面向未来的基于 W3C DID 去中心化身份和应用',
145
146
  noAuthenticationProvider: '未配置任何身份验证提供者',
147
+ lastUsed: '上次使用',
146
148
  },
147
149
  };
@@ -48,7 +48,8 @@ export default function ConnectChooseList({
48
48
  const passkeyLoginRef = useRef(null);
49
49
  const { loginOAuth, logoutOAuth, t, oauthState } = useOAuth();
50
50
  const { passkeyState } = usePasskey();
51
- const { extraParams, locale, connectState, plugins, setPlugins, setSelectedPlugin, getPlugin } = useStateContext();
51
+ const { extraParams, locale, connectState, plugins, setPlugins, setSelectedPlugin, getPlugin, lastLoginMethod } =
52
+ useStateContext();
52
53
 
53
54
  const handleLoginOAuth = useMemoizedFn(async (item) => {
54
55
  localStorage.setItem(GA_LAST_LOGIN_METHOD, item.provider);
@@ -111,7 +112,7 @@ export default function ConnectChooseList({
111
112
  if (magicToken && showEmailLogin && plugins.some((plugin) => plugin.name === LOGIN_PROVIDER.EMAIL)) {
112
113
  const plugin = getPlugin(LOGIN_PROVIDER.EMAIL);
113
114
  if (plugin.state.status === 'idle') {
114
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'email');
115
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.EMAIL);
115
116
  plugin.state.reset();
116
117
  plugin.state.magicToken = magicToken;
117
118
  connectState.chooseMethod = LOGIN_PROVIDER.EMAIL;
@@ -160,6 +161,8 @@ export default function ConnectChooseList({
160
161
  ]}>
161
162
  {showMobileLogin && (
162
163
  <MobileLoginItem
164
+ t={t}
165
+ isLatest={lastLoginMethod && lastLoginMethod === LOGIN_PROVIDER.WALLET}
163
166
  ref={walletLoginRef}
164
167
  tokenState={tokenState}
165
168
  sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
@@ -167,7 +170,7 @@ export default function ConnectChooseList({
167
170
  tokenKey={tokenKey}
168
171
  disableSwitchApp={disableSwitchApp}
169
172
  onClick={async () => {
170
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'wallet');
173
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.WALLET);
171
174
  tokenState.reset();
172
175
  await onReset();
173
176
  tokenState.status = 'created';
@@ -181,13 +184,15 @@ export default function ConnectChooseList({
181
184
  )}
182
185
  {showWebLogin && (
183
186
  <WebLoginItem
187
+ isLatest={lastLoginMethod && lastLoginMethod === LOGIN_PROVIDER.WALLET}
184
188
  ref={webLoginRef}
185
189
  tokenState={tokenState}
186
190
  webWalletUrl={webWalletUrl}
191
+ t={t}
187
192
  sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
188
193
  disableSwitchApp={disableSwitchApp}
189
194
  onClick={() => {
190
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'wallet');
195
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.WALLET);
191
196
  // HACK: 在点击插件登录时,直接将状态置为扫码中,避免插件登录时的状态切换
192
197
  tokenState.status = 'scanned';
193
198
  connectState.chooseMethod = 'wallet-web';
@@ -222,6 +227,8 @@ export default function ConnectChooseList({
222
227
  {showOAuthLogin
223
228
  ? oauthProviderList.map((item) => (
224
229
  <LoginMethodItem
230
+ t={t}
231
+ isLatest={lastLoginMethod && lastLoginMethod === item.provider}
225
232
  key={item.provider}
226
233
  title={LOGIN_PROVIDER_NAME[item.provider]}
227
234
  icon={
@@ -254,7 +261,7 @@ export default function ConnectChooseList({
254
261
  behavior={passkeyBehavior}
255
262
  sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
256
263
  onClick={() => {
257
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'passkey');
264
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.PASSKEY);
258
265
  const connectFn = passkeyLoginRef.current.connect;
259
266
  connectState.chooseMethod = 'passkey';
260
267
  connectState.retryConnect = () => {
@@ -65,6 +65,7 @@ export default function ConnectProviderList({
65
65
  getPlugin,
66
66
  showWalletOptions,
67
67
  setShowWalletOptions,
68
+ lastLoginMethod,
68
69
  } = useStateContext();
69
70
 
70
71
  const t = useMemoizedFn((key, data = {}) => {
@@ -127,7 +128,7 @@ export default function ConnectProviderList({
127
128
  });
128
129
 
129
130
  const handleMobileLoginClick = useMemoizedFn(async () => {
130
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'wallet');
131
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.WALLET);
131
132
  tokenState.reset();
132
133
  await onReset();
133
134
  tokenState.status = 'created';
@@ -139,7 +140,7 @@ export default function ConnectProviderList({
139
140
  });
140
141
 
141
142
  const handleWebLoginClick = useMemoizedFn(() => {
142
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'wallet');
143
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.WALLET);
143
144
  // HACK: 在点击插件登录时,直接将状态置为扫码中,避免插件登录时的状态切换
144
145
  tokenState.status = 'scanned';
145
146
  connectState.chooseMethod = 'wallet-web';
@@ -178,7 +179,7 @@ export default function ConnectProviderList({
178
179
  ) {
179
180
  const plugin = getPlugin(LOGIN_PROVIDER.EMAIL);
180
181
  if (plugin.state.status === 'idle') {
181
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'email');
182
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.EMAIL);
182
183
  plugin.state.reset();
183
184
  plugin.state.magicToken = magicToken;
184
185
  connectState.chooseMethod = LOGIN_PROVIDER.EMAIL;
@@ -215,6 +216,8 @@ export default function ConnectProviderList({
215
216
  if (browser.mobile.any && !showWalletOptions) {
216
217
  return (
217
218
  <MobileLoginItem
219
+ t={t}
220
+ isLatest={lastLoginMethod && lastLoginMethod === LOGIN_PROVIDER.WALLET}
218
221
  key={LOGIN_PROVIDER.WALLET}
219
222
  ref={walletLoginRef}
220
223
  tokenState={tokenState}
@@ -233,6 +236,8 @@ export default function ConnectProviderList({
233
236
  hideQRCode || (!hideQRCode && (isSameProtocol || extension) && (!browser.mobile.any || extension));
234
237
  return shouldShowWebLoginItem ? (
235
238
  <WebLoginItem
239
+ t={t}
240
+ isLatest={lastLoginMethod && lastLoginMethod === LOGIN_PROVIDER.WALLET}
236
241
  key={LOGIN_PROVIDER.WALLET}
237
242
  ref={webLoginRef}
238
243
  tokenState={tokenState}
@@ -265,7 +270,7 @@ export default function ConnectProviderList({
265
270
  behavior={passkeyBehavior}
266
271
  sx={[size === 'small' ? { p: 1 } : { p: 2 }]}
267
272
  onClick={() => {
268
- localStorage.setItem(GA_LAST_LOGIN_METHOD, 'passkey');
273
+ localStorage.setItem(GA_LAST_LOGIN_METHOD, LOGIN_PROVIDER.PASSKEY);
269
274
  const connectFn = passkeyLoginRef.current.connect;
270
275
  connectState.chooseMethod = 'passkey';
271
276
  connectState.retryConnect = () => {
@@ -305,6 +310,8 @@ export default function ConnectProviderList({
305
310
  if (item.type === 'oauth') {
306
311
  return (
307
312
  <LoginMethodItem
313
+ t={t}
314
+ isLatest={lastLoginMethod && lastLoginMethod === item.provider}
308
315
  key={item.provider}
309
316
  title={LOGIN_PROVIDER_NAME[item.provider]}
310
317
  icon={
@@ -2,17 +2,20 @@ import { Box, Tooltip, Typography, useTheme } from '@mui/material';
2
2
  import { Icon } from '@iconify/react';
3
3
  import { isValidElement } from 'react';
4
4
  import PropTypes from 'prop-types';
5
+ import noop from 'lodash/noop';
5
6
  import { mergeSx } from '@arcblock/ux/lib/Util/style';
6
7
  import { LOGIN_PROVIDER_ICON_SIZE } from '@arcblock/ux/lib/Util/constant';
7
8
  import ArrowRightAltRoundedIcon from '@iconify-icons/material-symbols/arrow-right-alt-rounded';
8
9
 
9
10
  export default function LoginMethodItem({
11
+ isLatest = false,
10
12
  title,
11
13
  description = null,
12
14
  icon,
13
15
  iconScale = 0.95,
14
16
  slotProps = {},
15
17
  mode = 'normal',
18
+ t = noop,
16
19
  ...rest
17
20
  }) {
18
21
  const theme = useTheme();
@@ -23,13 +26,16 @@ export default function LoginMethodItem({
23
26
  className="arc-login-item"
24
27
  sx={mergeSx(
25
28
  {
29
+ position: 'relative',
26
30
  display: 'flex',
27
31
  alignItems: 'center',
28
32
  gap: 1,
29
33
  cursor: 'pointer',
30
34
  p: 1.2,
31
35
  borderRadius: 1,
32
- backgroundColor: 'grey.50',
36
+ backgroundColor: isLatest ? 'grey.100' : 'grey.50',
37
+ border: isLatest ? '1px solid' : 'none',
38
+ borderColor: isLatest ? 'grey.100' : 'transparent',
33
39
  textDecoration: 'none',
34
40
  transition: 'background-color 0.5s',
35
41
  '&:hover, &:active, &.did-connect__choose-item__active': {
@@ -100,12 +106,21 @@ export default function LoginMethodItem({
100
106
  </Typography>
101
107
  ) : null}
102
108
  </Box>
103
- <Icon
104
- className="other-item-icon"
105
- icon={ArrowRightAltRoundedIcon}
106
- fontSize="1.3rem"
107
- color={theme.palette.primary.main}
108
- />
109
+ {isLatest ? (
110
+ <Typography
111
+ component="span"
112
+ className="latest-badge"
113
+ style={{ color: theme.palette.text.secondary, fontSize: 11 }}>
114
+ {t('lastUsed')}
115
+ </Typography>
116
+ ) : (
117
+ <Icon
118
+ className="other-item-icon"
119
+ icon={ArrowRightAltRoundedIcon}
120
+ fontSize="1.3rem"
121
+ color={theme.palette.primary.main}
122
+ />
123
+ )}
109
124
  </>
110
125
  ) : null}
111
126
  </Box>
@@ -119,4 +134,6 @@ LoginMethodItem.propTypes = {
119
134
  iconScale: PropTypes.number,
120
135
  slotProps: PropTypes.object,
121
136
  mode: PropTypes.oneOf(['mini', 'simple', 'normal']),
137
+ isLatest: PropTypes.bool,
138
+ t: PropTypes.func,
122
139
  };
@@ -1,13 +1,14 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import { useImperativeHandle, useRef } from 'react';
3
3
  import { useMemoizedFn } from 'ahooks';
4
+ import { LOGIN_PROVIDER } from '@arcblock/ux/lib/Util/constant';
4
5
 
5
6
  import PasskeyActions from '../../../Passkey/actions';
6
7
  import { getAppId } from '../../../utils';
7
8
  import { useStateContext } from '../../contexts/state';
8
9
 
9
10
  export default function PasskeyLoginItem({ ref = null, onSuccess, tokenState, behavior = 'none', ...rest }) {
10
- const { extraParams } = useStateContext();
11
+ const { extraParams, lastLoginMethod } = useStateContext();
11
12
  const passkeyActionRef = useRef(null);
12
13
 
13
14
  const handleSuccess = useMemoizedFn((result) =>
@@ -36,6 +37,7 @@ export default function PasskeyLoginItem({ ref = null, onSuccess, tokenState, be
36
37
  <PasskeyActions
37
38
  {...rest}
38
39
  ref={passkeyActionRef}
40
+ isLatest={lastLoginMethod && lastLoginMethod === LOGIN_PROVIDER.PASSKEY}
39
41
  action={tokenState.action}
40
42
  behavior={behavior}
41
43
  onSuccess={handleSuccess}
@@ -24,7 +24,7 @@ export default function WalletLoginOptions({
24
24
  onWebLoginClick,
25
25
  onBack,
26
26
  }) {
27
- const { locale } = useStateContext();
27
+ const { t, locale } = useStateContext();
28
28
  const browser = useBrowser();
29
29
  const isSameProtocol = checkSameProtocol(webWalletUrl);
30
30
  const extension = detectWalletExtension();
@@ -32,7 +32,6 @@ export default function WalletLoginOptions({
32
32
  const isTablet = useCreation(() => {
33
33
  return browser.mobile.tablet || (!browser.mobile.any && window?.navigator?.maxTouchPoints > 0);
34
34
  }, [browser.mobile.tablet, browser.mobile.any]);
35
-
36
35
  return (
37
36
  <>
38
37
  {qrcode}
@@ -67,6 +66,7 @@ export default function WalletLoginOptions({
67
66
  }}>
68
67
  {isTablet ? (
69
68
  <MobileLoginItem
69
+ t={t}
70
70
  key={LOGIN_PROVIDER.WALLET}
71
71
  ref={walletLoginRef}
72
72
  tokenState={tokenState}
@@ -82,6 +82,7 @@ export default function WalletLoginOptions({
82
82
  <>
83
83
  {(isSameProtocol || extension) && (!browser.mobile.any || extension) ? (
84
84
  <WebLoginItem
85
+ t={t}
85
86
  key={LOGIN_PROVIDER.WALLET}
86
87
  ref={webLoginRef}
87
88
  tokenState={tokenState}
@@ -97,6 +97,7 @@ function Connect({
97
97
  masterBlocklet,
98
98
  showWalletOptions,
99
99
  setShowWalletOptions,
100
+ lastLoginMethod,
100
101
  } = useStateContext();
101
102
  const { state, generate, cancelWhenScanned } = useToken({
102
103
  action,
@@ -221,6 +222,7 @@ function Connect({
221
222
  mode,
222
223
  blocklet: connectState?.sourceAppPid ? masterBlocklet : blocklet,
223
224
  isSmallView,
225
+ lastLoginMethod,
224
226
  });
225
227
 
226
228
  const prevSourceAppPid = usePrevious(connectState?.sourceAppPid);
@@ -251,40 +253,38 @@ function Connect({
251
253
  );
252
254
  }, [hideCloseButton, onClose]);
253
255
 
254
- let content = null;
255
- if (showStatus) {
256
- content = (
257
- <Box
258
- sx={{
259
- flex: 1,
260
- display: 'flex',
261
- alignItems: 'center',
262
- justifyContent: 'center',
263
- }}>
264
- <Box>
265
- <ConnectStatus
266
- status={selectedPlugin?.state?.computedStatus || oauthState.status || passkeyState.status || state.status}
267
- nextWorkflow={state.nextWorkflow}
268
- mfaCode={state.mfaCode}
269
- onCancel={handleCancel}
270
- onRetry={handleRetry}
271
- onClose={onClose}
272
- messages={statusMessages}
273
- locale={locale}
274
- className="did-connect__auth-status auth-status"
275
- loadingIcon={
276
- connectState.chooseMethod ? (
277
- <ProviderIcon provider={connectState.chooseMethod} sx={{ color: 'text.primary' }} />
278
- ) : null
279
- }
280
- chooseMethod={chooseMethod}
281
- hideRetry={connectState.chooseMethod === 'email'}
282
- hideBack={!!magicToken}
283
- />
284
- </Box>
256
+ // 预渲染 ConnectStatus,通过 CSS 控制显示/隐藏,避免 DOM 的创建/销毁导致卡顿
257
+ const statusContent = (
258
+ <Box
259
+ sx={{
260
+ flex: 1,
261
+ display: showStatus ? 'flex' : 'none',
262
+ alignItems: 'center',
263
+ justifyContent: 'center',
264
+ }}>
265
+ <Box>
266
+ <ConnectStatus
267
+ status={selectedPlugin?.state?.computedStatus || oauthState.status || passkeyState.status || state.status}
268
+ nextWorkflow={state.nextWorkflow}
269
+ mfaCode={state.mfaCode}
270
+ onCancel={handleCancel}
271
+ onRetry={handleRetry}
272
+ onClose={onClose}
273
+ messages={statusMessages}
274
+ locale={locale}
275
+ className="did-connect__auth-status auth-status"
276
+ loadingIcon={
277
+ connectState.chooseMethod ? (
278
+ <ProviderIcon provider={connectState.chooseMethod} sx={{ color: 'text.primary' }} />
279
+ ) : null
280
+ }
281
+ chooseMethod={chooseMethod}
282
+ hideRetry={connectState.chooseMethod === 'email'}
283
+ hideBack={!!magicToken}
284
+ />
285
285
  </Box>
286
- );
287
- }
286
+ </Box>
287
+ );
288
288
 
289
289
  const urlWithParams = useAuthUrl({ disableSwitchApp, tokenState: state });
290
290
 
@@ -453,8 +453,9 @@ function Connect({
453
453
  display: 'flex',
454
454
  flex: 1,
455
455
  }}>
456
- {/* eslint-disable-next-line no-nested-ternary */}
457
- {content}
456
+ {/* ConnectStatus:始终渲染,通过 CSS 控制显示/隐藏 */}
457
+ {statusContent}
458
+ {/* ConnectProviderList:始终渲染,通过 CSS 控制显示/隐藏 */}
458
459
  <ConnectProviderList
459
460
  slotProps={{
460
461
  root: {