@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,1687 @@
1
+ /* eslint-disable no-use-before-define */
2
+ import PropTypes from 'prop-types';
3
+ import omit from 'lodash/omit';
4
+ import pick from 'lodash/pick';
5
+ import cloneDeep from 'lodash/cloneDeep';
6
+ import isFunction from 'lodash/isFunction';
7
+ import isUndefined from 'lodash/isUndefined';
8
+ import defaultsDeep from 'lodash/defaultsDeep';
9
+ import Cookie from 'js-cookie';
10
+ import EventEmitter from 'eventemitter3';
11
+ import pWaitFor from 'p-wait-for';
12
+ import { Box, CircularProgress, Typography, useTheme } from '@mui/material';
13
+ import { getVisitorId, setVisitorId, ensureVisitorId, isUrl } from '@arcblock/ux/lib/Util';
14
+ import Toast, { ToastProvider } from '@arcblock/ux/lib/Toast';
15
+ import Center from '@arcblock/ux/lib/Center';
16
+ import { useCreation, useLatest, useMemoizedFn, useMount, usePrevious, useReactive, useUpdateEffect } from 'ahooks';
17
+ import { joinURL } from 'ufo';
18
+ import { Icon } from '@iconify/react';
19
+ import KeyboardDoubleArrowRightRoundedIcon from '@iconify-icons/material-symbols/keyboard-double-arrow-right-rounded';
20
+ import useBrowser from '@arcblock/react-hooks/lib/useBrowser';
21
+ import useConfirm from '@arcblock/ux/lib/Dialog/use-confirm';
22
+ import DID from '@arcblock/ux/lib/DID';
23
+ import Avatar from '@arcblock/ux/lib/Avatar';
24
+ import { getCurrentAppPid } from '@arcblock/ux/lib/SessionUser/libs/utils';
25
+ import { translate } from '@arcblock/ux/lib/Locale/util';
26
+ import { BlockletSDK, getBlockletSDK } from '@blocklet/js-sdk';
27
+ import bridge from '@arcblock/bridge';
28
+ import { getFederatedEnabled } from '@arcblock/ux/lib/Util/federated';
29
+ import noop from 'lodash/noop';
30
+ import isNil from 'lodash/isNil';
31
+
32
+ import { useState } from 'react';
33
+ import createStorage from '../Storage';
34
+ import {
35
+ getAppId,
36
+ updateConnectedInfo,
37
+ getBrowserLang,
38
+ formatCacheTtl,
39
+ sleep,
40
+ decodeUrlParams,
41
+ logger,
42
+ debug,
43
+ } from '../utils';
44
+ import WindowFocusAware from './window-focus-aware';
45
+ import { OAuthProvider, useOAuth, OAuthConsumer, OAuthContext } from '../OAuth';
46
+ import { PasskeyProvider, usePasskey, PasskeyConsumer, PasskeyContext } from '../Passkey';
47
+ import { useDid, WrapDid } from '../User';
48
+ import useFederated from './hooks/use-federated';
49
+ import useSessionToken from './hooks/use-session-token';
50
+ import useProtectedRoutes from './hooks/use-protected-routes';
51
+ import { SessionContext } from './context';
52
+ import useConnect from '../Connect/use-connect';
53
+ import { EVENTS } from './libs/constants';
54
+ import { translations } from './libs/locales';
55
+ import { NotOpenError } from '../error';
56
+ import {
57
+ API_DID_PREFIX,
58
+ BLOCKLET_SERVICE_PATH_PREFIX,
59
+ DEFAULT_TIMEOUT,
60
+ REFRESH_TOKEN_STORAGE_KEY,
61
+ SESSION_TOKEN_STORAGE_KEY,
62
+ DID_SPACES_BASE_URL,
63
+ } from '../constant';
64
+ import { checkEnableAutoLogin, getMobileVisitorId, login as loginInMobile } from './libs/login-mobile';
65
+ import useQuickConnect from '../Connect/hooks/use-quick-connect';
66
+ import { didSpacesIsRequired } from './libs/did-spaces';
67
+ import { getWalletDid } from '../User/use-did';
68
+ import useDIDSpacesGuide from './did-spaces-guide';
69
+ import { FederatedProvider, useFederatedContext } from '../Federated';
70
+ import useVerify from './hooks/use-verify';
71
+
72
+ export * from './libs';
73
+
74
+ const sdk = getBlockletSDK();
75
+
76
+ const { Provider, Consumer } = SessionContext;
77
+
78
+ const didConnectTimeoutError = {
79
+ en: 'DID Connect timeout, please reopen DID Connect popup',
80
+ zh: 'DID Connect 超时, 请重新打开 DID Connect',
81
+ };
82
+
83
+ const waitForDidConnect = {
84
+ en: 'DID Connect is already opened, please wait for it to complete',
85
+ zh: 'DID Connect 已打开,请等待完成',
86
+ };
87
+
88
+ export default function createSessionContext(
89
+ storageKey = SESSION_TOKEN_STORAGE_KEY,
90
+ storageEngine = 'ls',
91
+ storageOptions = {},
92
+ opts = {}
93
+ ) {
94
+ let defaultServiceHost = '/';
95
+ // FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时,不应该直接从 window.blocklet 去取值
96
+ if (globalThis?.blocklet?.prefix) {
97
+ defaultServiceHost = globalThis.blocklet.prefix;
98
+ }
99
+ if (typeof opts === 'boolean') {
100
+ // eslint-disable-next-line no-param-reassign
101
+ opts = {
102
+ appendAuthServicePrefix: opts,
103
+ extraParams: {},
104
+ };
105
+ }
106
+
107
+ defaultsDeep(opts, {
108
+ rolling: true,
109
+ appendAuthServicePrefix: false,
110
+ extraParams: {},
111
+ refreshTokenStorageKey: REFRESH_TOKEN_STORAGE_KEY,
112
+ });
113
+
114
+ function notifyBridge(sessionState) {
115
+ if (globalThis.blocklet) {
116
+ if (sessionState?.user) {
117
+ const data = {
118
+ did: sessionState.user.did,
119
+ host: window.location.host,
120
+ appPid: globalThis.blocklet.appPid,
121
+ visitorId: getVisitorId(),
122
+ sourceAppPid: sessionState.user.sourceAppPid,
123
+ fullName: sessionState.user.fullName,
124
+ };
125
+ debug('bridge callArc: onLogin', data);
126
+ bridge.callArc('onLogin', { ...data, user: data });
127
+ } else {
128
+ bridge.callArc('onLogin', { error: 'no user', code: 400 });
129
+ debug('notifyBridge failed', { sessionState });
130
+ }
131
+ }
132
+ }
133
+
134
+ const sessionTokenStorage = createStorage(storageKey, storageEngine, storageOptions);
135
+
136
+ const refreshTokenStorage = createStorage(opts.refreshTokenStorageKey, 'ls');
137
+
138
+ function SessionProvider({ ...rawProps }) {
139
+ const props = Object.assign({}, rawProps);
140
+ if (isUndefined(props.serviceHost)) {
141
+ props.serviceHost = defaultServiceHost;
142
+ }
143
+ if (isUndefined(props.locale)) {
144
+ props.locale = '';
145
+ }
146
+ if (isUndefined(props.action)) {
147
+ props.action = 'login';
148
+ }
149
+ if (isUndefined(props.prefix)) {
150
+ props.prefix = API_DID_PREFIX;
151
+ }
152
+ if (isUndefined(props.appendAuthServicePrefix)) {
153
+ props.appendAuthServicePrefix = false;
154
+ }
155
+ if (isUndefined(props.extraParams)) {
156
+ props.extraParams = {};
157
+ }
158
+ if (isUndefined(props.options)) {
159
+ props.options = {};
160
+ }
161
+ if (isUndefined(props.autoConnect)) {
162
+ props.autoConnect = false;
163
+ }
164
+ if (isUndefined(props.autoDisconnect)) {
165
+ props.autoDisconnect = true;
166
+ }
167
+ if (isUndefined(props.useSocket)) {
168
+ props.useSocket = true;
169
+ }
170
+ if (isUndefined(props.timeout)) {
171
+ props.timeout = DEFAULT_TIMEOUT * 1000;
172
+ }
173
+ if (isUndefined(props.webWalletUrl)) {
174
+ props.webWalletUrl = undefined;
175
+ }
176
+ if (isUndefined(props.protectedRoutes)) {
177
+ props.protectedRoutes = ['*'];
178
+ }
179
+ if (isUndefined(props.apiOptions)) {
180
+ props.apiOptions = {};
181
+ }
182
+ if (isUndefined(props.lazyRefreshToken)) {
183
+ props.lazyRefreshToken = false;
184
+ }
185
+
186
+ const { connectApi, connectHolder } = useConnect();
187
+ const [unReadCount, setUnReadCount] = useState(0);
188
+ const browser = useBrowser();
189
+ const { requestStorageAccess } = useFederatedContext();
190
+ const { palette } = useTheme();
191
+ const searchParams = new URLSearchParams(window.location.search);
192
+
193
+ /**
194
+ * @typedef PageState
195
+ * @property {object} extraParams - 额外参数
196
+ * @property {object} options - 配置项
197
+ * @property {string} currentLocale - 当前语言
198
+ * @property {boolean} allowWallet - 是否显示钱包操作入口
199
+ * @property {('popup'|'window'|'iframe')} [openMode] - 打开的方式
200
+ * @property {boolean} autoConnect - 是否自动连接
201
+ * @property {string} prefix - 自动生成的当前应用的 prefix
202
+ */
203
+ /**
204
+ * 页面状态
205
+ * @type {PageState}
206
+ */
207
+ const pageState = useReactive({
208
+ extraParams: {},
209
+ options: {},
210
+ currentLocale: props.locale,
211
+ allowWallet: undefined,
212
+ openMode: 'window',
213
+ get autoConnect() {
214
+ // for backward compatibility
215
+ if (typeof props.autoConnect === 'boolean') {
216
+ return props.autoConnect;
217
+ }
218
+ // eslint-disable-next-line react/prop-types
219
+ return !!props.autoLogin;
220
+ },
221
+ get prefix() {
222
+ if (opts.appendAuthServicePrefix || props.appendAuthServicePrefix) {
223
+ return joinURL(BLOCKLET_SERVICE_PATH_PREFIX, props.prefix);
224
+ }
225
+ return props.prefix;
226
+ },
227
+ get notificationPrefix() {
228
+ const _prefix = '/api/notifications';
229
+ if (opts.appendAuthServicePrefix || props.appendAuthServicePrefix) {
230
+ return joinURL(BLOCKLET_SERVICE_PATH_PREFIX, _prefix);
231
+ }
232
+ return _prefix;
233
+ },
234
+ });
235
+
236
+ const state = useReactive({
237
+ action: props.action,
238
+ error: '',
239
+ initialized: false,
240
+ loading: false,
241
+ open: false,
242
+ user: null,
243
+ provider: '',
244
+ walletOS: '',
245
+ baseUrl: '',
246
+ unReadCount: 0,
247
+ // 不可以直接个性 props.autoConnect (readonly)
248
+ });
249
+ const currentLocale = useCreation(() => {
250
+ return props.locale || pageState.currentLocale || getBrowserLang();
251
+ }, [props.locale, pageState.currentLocale]);
252
+
253
+ const { confirmApi, confirmHolder } = useConfirm();
254
+
255
+ const stopOnOpen = useMemoizedFn(() => {
256
+ if (state.open && !browser.arcSphere) {
257
+ const msg = waitForDidConnect[currentLocale] || waitForDidConnect.en;
258
+ Toast.warning(msg);
259
+ throw new Error(msg);
260
+ }
261
+ });
262
+
263
+ const events = useCreation(() => new EventEmitter(), []);
264
+ /**
265
+ * 用于包装 did-connect 的 open 和 close 事件,添加一些默认的行为
266
+ * 1. 在 open 中添加的事件订阅,did-connect close 时需要取消订阅
267
+ * 2. session 内部的事件订阅使用与外部事件不同命的名字
268
+ * @param {string} eventName - 订阅的事件名
269
+ * @param {function} doneFn - 订阅事件被调用时,执行的函数
270
+ * @param {function} cancelFn - 关闭 did-connect 时,执行的回调
271
+ * @returns
272
+ */
273
+ const wrapOnceEmit = useMemoizedFn((eventName, doneFn, cancelFn) => {
274
+ const fnKeyMap = {
275
+ login: [EVENTS.LOGIN, EVENTS.CANCEL_LOGIN],
276
+ 'bind-wallet': [EVENTS.BIND_WALLET, EVENTS.CANCEL_BIND_WALLET],
277
+ 'switch-passport': [EVENTS.SWITCH_PASSPORT, EVENTS.CANCEL_SWITCH_PASSPORT],
278
+ 'switch-profile': [EVENTS.SWITCH_PROFILE, EVENTS.CANCEL_SWITCH_PROFILE],
279
+ // HACK: 对于内部来说,switch-did 就是 login,所以内部的监听事件仍然是 login
280
+ 'switch-did': [EVENTS.LOGIN, EVENTS.CANCEL_LOGIN],
281
+ // @FIXME: @zhanghan 整个事件发射的机制需要重构,目前的实现不是很优雅,对于用户来说不是很好用。 https://github.com/ArcBlock/ux/pull/1414#discussion_r1904830954
282
+ 'did-space-connected': [EVENTS.DID_SPACE_CONNECTED],
283
+ };
284
+
285
+ if (!Object.keys(fnKeyMap).includes(eventName)) {
286
+ return;
287
+ }
288
+
289
+ const listenFn = async (...args) => {
290
+ if (isFunction(doneFn)) {
291
+ await doneFn(...args);
292
+ }
293
+ events.emit(eventName, ...args);
294
+ };
295
+ events.once(fnKeyMap[eventName][0], listenFn);
296
+ events.once(fnKeyMap[eventName][1], async (...args) => {
297
+ if (isFunction(listenFn)) {
298
+ events.off(fnKeyMap[eventName][0], listenFn);
299
+ }
300
+ if (isFunction(cancelFn)) {
301
+ await cancelFn(...args);
302
+ }
303
+ events.emit(`cancel-${eventName}`, ...args);
304
+ });
305
+ });
306
+
307
+ const prevInitialized = usePrevious(state.initialized, (prev, next) => {
308
+ if (prev !== next) {
309
+ return true;
310
+ }
311
+ if (prev === true && next === true) {
312
+ return true;
313
+ }
314
+ return false;
315
+ });
316
+
317
+ const {
318
+ syncSessionSate,
319
+ handleRefreshUser,
320
+ handleRefreshToken,
321
+ renewToken,
322
+ clearSession,
323
+ handleLoginResult,
324
+ decrypt,
325
+ service,
326
+ getSessionToken,
327
+ getRefreshToken,
328
+ setRefreshToken,
329
+ setSessionToken,
330
+ } = useSessionToken({
331
+ state,
332
+ pageState,
333
+ sessionTokenStorage,
334
+ refreshTokenStorage,
335
+ serviceHost: props.serviceHost,
336
+ apiOptions: props.apiOptions,
337
+ lazyRefreshToken: props.lazyRefreshToken,
338
+ onRefresh({ type, sessionToken, refreshToken, user }) {
339
+ debug('onRefresh', { type, user });
340
+
341
+ if (globalThis.blocklet) {
342
+ const data = {
343
+ did: user.did,
344
+ host: window.location.host,
345
+ appPid: globalThis.blocklet.appPid,
346
+ visitorId: getVisitorId(),
347
+ sourceAppPid: user.sourceAppPid,
348
+ fullName: user.fullName,
349
+ };
350
+ if (type === 'refreshToken') {
351
+ debug('bridge callArc: onRefreshToken');
352
+ bridge.callArc('onRefreshToken', {
353
+ sessionToken,
354
+ refreshToken,
355
+ user: data,
356
+ });
357
+ } else if (type === 'refreshUser') {
358
+ debug('bridge callArc: onRefreshUser');
359
+ bridge.callArc('onRefreshUser', {
360
+ sessionToken,
361
+ refreshToken,
362
+ user: data,
363
+ });
364
+ }
365
+ }
366
+ },
367
+ });
368
+
369
+ const handleVerify = useVerify({ state, currentLocale, connectApi });
370
+
371
+ const loginWallet = useMemoizedFn((done, extraParams = {}, options = {}) => {
372
+ stopOnOpen();
373
+ const params = done;
374
+ if (typeof params === 'object') {
375
+ done = params.onSuccess; // eslint-disable-line no-param-reassign
376
+ extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
377
+ options = params.options || {}; // eslint-disable-line no-param-reassign
378
+ // FIXME: @zhanghan 将来需要移除这种传递方式,配置参数统一放到 options 中
379
+ options.origin = params.origin || ''; // eslint-disable-line no-param-reassign
380
+ }
381
+
382
+ if (!state.user || options.origin === 'switch-did') {
383
+ if (options.origin === 'switch-did') {
384
+ wrapOnceEmit('switch-did', done, params?.onCancel);
385
+ } else {
386
+ wrapOnceEmit('login', done, params?.onCancel);
387
+ }
388
+
389
+ pageState.extraParams = extraParams;
390
+ pageState.options = options;
391
+ state.action = 'login';
392
+ openConnect();
393
+ }
394
+ });
395
+
396
+ // 高阶函数包装器 - 二次认证装饰器
397
+ const withSecondaryAuth = useMemoizedFn((fn, options = {}) => {
398
+ // 保存原始函数和上下文
399
+ const originalFn = fn;
400
+
401
+ return function wrappedSecondaryAuth(...args) {
402
+ // 保存调用时的 this 上下文
403
+ const context = this;
404
+
405
+ return (async () => {
406
+ try {
407
+ const result = await handleVerify(options);
408
+ if (!result.sessionId) {
409
+ throw new Error('Authentication failed');
410
+ }
411
+ // 使用 call 方法确保 this 指向正确
412
+ return await originalFn.call(context, result, ...args);
413
+ } catch (error) {
414
+ // 如果是用户取消认证,不需要显示错误信息
415
+ console.error('Authentication failed', error);
416
+ throw error;
417
+ }
418
+ })();
419
+ };
420
+ });
421
+ /**
422
+ * did-connect 登录函数
423
+ * @type {import('../types').LoginSessionFn}
424
+ */
425
+ const login = useMemoizedFn(async (done, extraParams = {}, options = {}) => {
426
+ stopOnOpen();
427
+ const params = done;
428
+ if (typeof params === 'object') {
429
+ done = params.onSuccess; // eslint-disable-line no-param-reassign
430
+ extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
431
+ options = params.options || {}; // eslint-disable-line no-param-reassign
432
+ // FIXME: @zhanghan 移除了 params.onCancel 回调,需要观察是否会导致别的问题,目前未找到任何地方使用了 onCancel 回调
433
+ }
434
+
435
+ if (isUndefined(extraParams?.inviter) && window.localStorage.getItem('inviter')) {
436
+ extraParams.inviter = window.localStorage.getItem('inviter');
437
+ }
438
+
439
+ // 使用 Url 中编码的参数
440
+ const { params: decodedParams } = await decodeUrlParams();
441
+ if (isUndefined(extraParams?.forceConnected) && decodedParams?.forceConnected) {
442
+ extraParams.forceConnected = decodedParams.forceConnected;
443
+ if (isUndefined(options.showQuickConnect)) {
444
+ options.showQuickConnect = false;
445
+ }
446
+ }
447
+ if (isUndefined(extraParams?.sourceAppPid) && !isUndefined(decodedParams?.sourceAppPid)) {
448
+ extraParams.sourceAppPid = decodedParams.sourceAppPid;
449
+ }
450
+ if (extraParams?.openMode === 'redirect') {
451
+ const redirect = extraParams?.redirect || '/';
452
+ const url = new URL(`${BLOCKLET_SERVICE_PATH_PREFIX}/login`, window.location.origin);
453
+ url.searchParams.set('redirect', redirect);
454
+ if (extraParams?.forceConnected) {
455
+ url.searchParams.set('forceConnected', extraParams?.forceConnected);
456
+ }
457
+ if (!isUndefined(extraParams?.sourceAppPid)) {
458
+ url.searchParams.set('sourceAppPid', extraParams?.sourceAppPid);
459
+ }
460
+ if (!isUndefined(extraParams?.inviter)) {
461
+ url.searchParams.set('inviter', extraParams?.inviter);
462
+ }
463
+ if (options?.origin) {
464
+ url.searchParams.set('origin', options.origin);
465
+ }
466
+ // 这里是安全的
467
+ window.location.href = url.href;
468
+ return;
469
+ }
470
+
471
+ let sourceAppPid = extraParams?.sourceAppPid;
472
+ if (isUndefined(sourceAppPid) && federatedEnabled) {
473
+ sourceAppPid = masterSite.appPid;
474
+ }
475
+ // 必须等到初始化完成后才能执行
476
+ await pWaitFor(() => state.initialized);
477
+
478
+ // @compatible: 兼容旧的传递 origin 的方式
479
+ if (typeof options === 'string') {
480
+ // eslint-disable-next-line no-param-reassign
481
+ options = { origin: options };
482
+ }
483
+ // @compatible end
484
+
485
+ const copyExtraParams = cloneDeep(extraParams);
486
+ copyExtraParams.passkeyBehavior = 'both';
487
+
488
+ // if (!isUndefined(copyExtraParams.allowWallet)) {
489
+ // pageState.allowWallet = copyExtraParams.allowWallet;
490
+ // delete copyExtraParams.allowWallet;
491
+ // } else {
492
+ // pageState.allowWallet = undefined;
493
+ // }
494
+ pageState.allowWallet = undefined;
495
+
496
+ const openMode = copyExtraParams?.openMode || props?.extraParams?.openMode;
497
+
498
+ if (!isUndefined(openMode)) {
499
+ pageState.openMode = openMode;
500
+ // 如果处于统一登录模式下,并且当前应用不是 master,才需要使用 window.open 的方式
501
+ } else if (sourceAppPid) {
502
+ if (sourceAppPid !== globalThis.blocklet.appPid && getFederatedEnabled(globalThis.blocklet)) {
503
+ pageState.openMode = 'window';
504
+ }
505
+ } else {
506
+ pageState.openMode = 'popup';
507
+ }
508
+
509
+ // 如果已经有跨站存储的权限,并且当前是 window 模式,则强制改为 popup 模式
510
+ if (pageState.openMode === 'window' && !browser.arcSphere && !browser.wallet) {
511
+ const result = await requestStorageAccess();
512
+ if (result) {
513
+ pageState.openMode = 'popup';
514
+ }
515
+ }
516
+
517
+ if (sourceAppPid) {
518
+ state.baseUrl = globalThis.blocklet.appUrl;
519
+ } else {
520
+ state.baseUrl = '';
521
+ }
522
+ loginWallet(done, copyExtraParams, options);
523
+ });
524
+
525
+ // eslint-disable-next-line require-await
526
+ const logout = useMemoizedFn(async (done) => {
527
+ const visitorId = getVisitorId();
528
+
529
+ if (globalThis.blocklet) {
530
+ // TODO: 需要增加 webapp 退出登录的处理
531
+ sdk.user.logout({ visitorId }).catch((error) => {
532
+ console.warn('Failed to logout remote', error);
533
+ });
534
+ await sleep(100);
535
+ }
536
+
537
+ // HACK: 仅在 blocklet 环境中才发起请求
538
+ if (globalThis.blocklet) {
539
+ const logoutData = {
540
+ visitorId,
541
+ appPid: globalThis.blocklet.appPid,
542
+ userDid: state.user.did,
543
+ };
544
+ debug('bridge callArc: onLogout', logoutData);
545
+ bridge.callArc('onLogout', logoutData);
546
+ }
547
+
548
+ clearSession();
549
+ closeConnect();
550
+ state.user = null;
551
+ state.provider = '';
552
+ state.walletOS = '';
553
+ state.error = '';
554
+ state.loading = false;
555
+ events.emit('logout');
556
+
557
+ if (typeof done === 'function') {
558
+ done();
559
+ }
560
+ });
561
+
562
+ const {
563
+ userSessions,
564
+ refresh: refreshUserSessions,
565
+ loaded: loadedUserSessions,
566
+ loginUserSession,
567
+ } = useQuickConnect({
568
+ appPid: getCurrentAppPid(state.user),
569
+ loginAppPid: globalThis?.blocklet?.appPid,
570
+ sourceAppPid: state?.user?.sourceAppPid,
571
+ autoFetch: false,
572
+ fetchAll: true,
573
+ });
574
+
575
+ const ensureLogin = useMemoizedFn(async () => {
576
+ if (!state?.user) {
577
+ await new Promise((resolve) => {
578
+ login(() => {
579
+ resolve();
580
+ });
581
+ });
582
+ }
583
+ });
584
+
585
+ // HACK: 先保留这段代码,后续改版时删除
586
+ // const cleanConnectedState = useMemoizedFn(() => {
587
+ // const cookieOptions = getCookieOptions({ returnDomain: false });
588
+ // Cookie.remove('connected_did', cookieOptions);
589
+ // Cookie.remove('connected_pk', cookieOptions);
590
+ // Cookie.remove('connected_app', cookieOptions);
591
+ // Cookie.remove('connected_wallet_os', cookieOptions);
592
+ // });
593
+
594
+ // See:
595
+ // - https://github.com/ArcBlock/ux/issues/520
596
+ // - https://github.com/ArcBlock/did-connect/issues/56
597
+ const switchDid = useMemoizedFn((done, extraParams = {}, options = {}) => {
598
+ stopOnOpen();
599
+ const params = done;
600
+ if (typeof params === 'object') {
601
+ done = params.onSuccess; // eslint-disable-line no-param-reassign
602
+ extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
603
+ options = params.options || {}; // eslint-disable-line no-param-reassign
604
+ }
605
+
606
+ // @compatible: 兼容旧的传递 origin 的方式
607
+ if (typeof options === 'string') {
608
+ // eslint-disable-next-line no-param-reassign
609
+ options = { origin: options };
610
+ }
611
+ // @compatible end
612
+
613
+ if (options?.userSession) {
614
+ loginUserSession(options.userSession).then((result) => {
615
+ updateConnectedInfo(
616
+ {
617
+ connected_did: options.userSession.user.did,
618
+ connected_pk: options.userSession.user.pk,
619
+ connected_wallet_os: options.userSession.extra.walletOS,
620
+ connected_app: getAppId(),
621
+ },
622
+ true
623
+ );
624
+ wrapOnceEmit('switch-did', done);
625
+ handleLoginResult({ ...result, encrypted: false });
626
+ handleRefreshUser({ showProgress: true }).then(async () => {
627
+ await sleep(200);
628
+ events.emit(EVENTS.LOGIN, result, decrypt, session.current);
629
+ notifyBridge(session.current);
630
+ await connectToDidSpaceForFullAccess();
631
+ });
632
+ });
633
+ } else {
634
+ if (isUndefined(extraParams?.forceConnected)) {
635
+ // switchDid 时,禁用 forceConnected,以便切换到其他的钱包
636
+ extraParams.forceConnected = false;
637
+ }
638
+ extraParams.passkeyBehavior = 'both';
639
+ login(done, extraParams, { ...options, origin: 'switch-did' });
640
+ }
641
+ });
642
+
643
+ const refreshProfile = useMemoizedFn(async () => {
644
+ await sdk.user.refreshProfile();
645
+ });
646
+
647
+ const switchProfile = useMemoizedFn(async (done, extraParams = {}) => {
648
+ await ensureLogin();
649
+ stopOnOpen();
650
+ const params = done;
651
+ if (typeof params === 'object') {
652
+ done = params.onSuccess; // eslint-disable-line no-param-reassign
653
+ extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
654
+ }
655
+ wrapOnceEmit('switch-profile', done, params?.onCancel);
656
+ pageState.extraParams = extraParams;
657
+ pageState.options = {};
658
+ state.action = 'switch-profile';
659
+ openConnect();
660
+ });
661
+
662
+ const switchPassport = useMemoizedFn(async (done, extraParams = {}) => {
663
+ await ensureLogin();
664
+ stopOnOpen();
665
+ const params = done;
666
+ if (typeof params === 'object') {
667
+ done = params.onSuccess; // eslint-disable-line no-param-reassign
668
+ extraParams = params.extraParams || {}; // eslint-disable-line no-param-reassign
669
+ }
670
+ wrapOnceEmit('switch-passport', done, params?.onCancel);
671
+
672
+ pageState.extraParams = extraParams;
673
+ pageState.options = {};
674
+ state.action = 'switch-passport';
675
+ openConnect();
676
+ });
677
+
678
+ const connectToDidSpace = useMemoizedFn(async ({ extraParams = {}, onSuccess = () => {}, ...rest } = {}) => {
679
+ await ensureLogin();
680
+ stopOnOpen();
681
+
682
+ pageState.extraParams = {
683
+ referrer: window.location.href,
684
+ purpose: 'authorize-for-import',
685
+ ...extraParams,
686
+ saveConnect: false, // 连接 spaces 不保存连接信息
687
+ };
688
+ pageState.options = {};
689
+ state.action = 'connect-to-did-spaces-for-user';
690
+ openConnect({
691
+ hideCloseButton: true,
692
+ onSuccess,
693
+ ...rest,
694
+ });
695
+ });
696
+
697
+ /**
698
+ * @description
699
+ * @param {{
700
+ * extraParams: Record<string, any>,
701
+ * onSuccess: (response: Record<string, any>, decrypt: (value: string) => string),
702
+ * }} [{ extraParams = {}, onSuccess = {} }={}]
703
+ */
704
+ const connectToDidSpaceForImport = useMemoizedFn(async ({ extraParams = {}, onSuccess = {} } = {}) => {
705
+ await ensureLogin();
706
+ connectToDidSpace({
707
+ extraParams: {
708
+ // service 交互
709
+ purpose: 'authorize-for-import',
710
+ ...extraParams,
711
+ },
712
+ onSuccess,
713
+ hideCloseButton: false,
714
+ });
715
+ });
716
+
717
+ const { didSpacesGuideApi, DidSpacesGuideView } = useDIDSpacesGuide({
718
+ autoClose: false,
719
+ });
720
+
721
+ const connectToDidSpaceForFullAccess = async ({ extraParams = {}, onSuccess = () => {} } = {}) => {
722
+ await ensureLogin();
723
+ if (!(await didSpacesIsRequired(state?.user))) {
724
+ return;
725
+ }
726
+ didSpacesGuideApi.open();
727
+ didSpacesGuideApi.onConnect((callback = noop) => {
728
+ const popupProps = {
729
+ prefix: '/connect-to-did-space',
730
+ custom: true,
731
+ extraParams: {
732
+ provider: 'wallet',
733
+ appPid: window.blocklet?.appPid,
734
+ appDid: window.blocklet?.appId,
735
+ appName: window.blocklet?.appName,
736
+ appDescription: window.blocklet?.appDescription,
737
+ appUrl: window.blocklet?.appUrl,
738
+ referrer: window.location.href,
739
+ connectScope: 'user',
740
+ ...extraParams,
741
+ },
742
+ onSuccess: async (response, decryptFunc) => {
743
+ const spaceGateway = decryptFunc ? decryptFunc(response.spaceGateway) : response.spaceGateway;
744
+ await new BlockletSDK().user.updateDidSpace({ spaceGateway });
745
+ await handleRefreshUser({ showProgress: true });
746
+ await onSuccess(response, decryptFunc);
747
+ await didSpacesGuideApi.close();
748
+ events.emit(EVENTS.DID_SPACE_CONNECTED, response, decryptFunc);
749
+ if (globalThis.blocklet) {
750
+ debug('bridge callArc: onDidSpaceConnected');
751
+ bridge.callArc('onDidSpaceConnected');
752
+ }
753
+ callback(true);
754
+ },
755
+ onError: (error) => {
756
+ console.error(error);
757
+ Toast.error(error.message);
758
+ didSpacesGuideApi.close();
759
+ callback(false);
760
+ },
761
+ onClose: () => {
762
+ callback();
763
+ },
764
+ };
765
+
766
+ connectApi.openPopup(popupProps, {
767
+ baseUrl: DID_SPACES_BASE_URL,
768
+ });
769
+ });
770
+ };
771
+
772
+ const bindWallet = useMemoizedFn(async (done, extraParams = {}) => {
773
+ await ensureLogin();
774
+ if (getWalletDid(state.user)) {
775
+ done({}, decrypt, session.current);
776
+ return;
777
+ }
778
+ stopOnOpen();
779
+ const params = done;
780
+ if (typeof params === 'object') {
781
+ done = params.onSuccess; // eslint-disable-line no-param-reassign
782
+ }
783
+ wrapOnceEmit('bind-wallet', done, params?.onCancel);
784
+ pageState.extraParams = extraParams;
785
+ pageState.options = {};
786
+ state.action = 'bind-wallet';
787
+ openConnect();
788
+ });
789
+
790
+ const onSwitchPassport = useMemoizedFn(async (result) => {
791
+ debug('onSwitchPassport', { result });
792
+ pageState.extraParams = {};
793
+ pageState.options = {};
794
+ handleLoginResult(result);
795
+ state.loading = false;
796
+ await handleRefreshUser({ showProgress: true });
797
+ await sleep(200);
798
+ events.emit(EVENTS.SWITCH_PASSPORT, result, decrypt, session.current);
799
+ if (globalThis.blocklet) {
800
+ debug('bridge callArc: onSwitchPassport');
801
+ bridge.callArc('onSwitchPassport');
802
+ }
803
+ });
804
+
805
+ const onLogin = useMemoizedFn(async (result) => {
806
+ if (state.action === 'switch-passport') {
807
+ onSwitchPassport(result);
808
+ return;
809
+ }
810
+
811
+ debug('onLogin', { result });
812
+ pageState.extraParams = {};
813
+ pageState.options = {};
814
+ handleLoginResult(result);
815
+ state.loading = false;
816
+ await handleRefreshUser({ showProgress: true });
817
+ await sleep(200);
818
+ if (state.action === 'login') {
819
+ debug('onLogin: emit LOGIN event', { result });
820
+ events.emit(EVENTS.LOGIN, result, decrypt, session.current);
821
+ notifyBridge(session.current);
822
+ connectToDidSpaceForFullAccess();
823
+ }
824
+ });
825
+
826
+ const onBindWallet = useMemoizedFn((result) => {
827
+ pageState.extraParams = {};
828
+ pageState.options = {};
829
+ handleLoginResult(result);
830
+ state.loading = false;
831
+ handleRefreshUser({ showProgress: true }).then(async () => {
832
+ await sleep(100);
833
+ events.emit(EVENTS.BIND_WALLET, result, decrypt, session.current);
834
+ if (globalThis.blocklet) {
835
+ debug('bridge callArc: onBindWallet');
836
+ bridge.callArc('onBindWallet');
837
+ }
838
+ });
839
+ // NOTICE: auth0 绑定 wallet 在新的版本中,不会再要求切换 login_token,这里的处理是为了兼容有无 sessionToken 的两种情况,都可以顺利完成当前绑定操作
840
+ });
841
+
842
+ const onSwitchProfile = useMemoizedFn((result) => {
843
+ debug('onSwitchProfile', { result });
844
+ pageState.extraParams = {};
845
+ pageState.options = {};
846
+ state.loading = false;
847
+ handleRefreshUser({ showProgress: true }).then(async () => {
848
+ await sleep(100);
849
+ events.emit(EVENTS.SWITCH_PROFILE, result, decrypt, session.current);
850
+ if (globalThis.blocklet) {
851
+ debug('bridge callArc: onSwitchProfile');
852
+ bridge.callArc('onSwitchProfile');
853
+ }
854
+ });
855
+ });
856
+
857
+ const onClose = useMemoizedFn((action) => {
858
+ pageState.extraParams = {};
859
+ pageState.options = {};
860
+ closeConnect();
861
+ const fnMap = {
862
+ login: EVENTS.CANCEL_LOGIN,
863
+ 'switch-profile': EVENTS.CANCEL_SWITCH_PROFILE,
864
+ 'switch-passport': EVENTS.CANCEL_SWITCH_PASSPORT,
865
+ 'bind-wallet': EVENTS.CANCEL_BIND_WALLET,
866
+ };
867
+ const fnCancelMap = {
868
+ login: 'offLogin',
869
+ 'switch-profile': 'offSwitchProfile',
870
+ 'switch-passport': 'offSwitchPassport',
871
+ 'bind-wallet': 'offBindWallet',
872
+ };
873
+ events.emit(fnMap[action]);
874
+
875
+ if (globalThis.blocklet) {
876
+ debug(`bridge callArc: ${fnCancelMap[action]}`);
877
+ bridge.callArc(`${fnCancelMap[action]}`);
878
+ }
879
+ });
880
+
881
+ const onSuccess = useMemoizedFn((action, ...args) => {
882
+ try {
883
+ // 跳转
884
+ callbacks[action](...args);
885
+ } catch (err) {
886
+ logger.error('Catch error in onSuccess', {
887
+ action,
888
+ args,
889
+ err,
890
+ });
891
+ }
892
+ });
893
+
894
+ const {
895
+ federatedMaster,
896
+ federatedEnabled,
897
+ master: masterSite,
898
+ } = useFederated({
899
+ locale: currentLocale,
900
+ decrypt,
901
+ login: loginWallet,
902
+ handleLoginResult,
903
+ handleRefreshUser,
904
+ setRefreshToken,
905
+ setSessionToken,
906
+ });
907
+
908
+ const { checkMatch } = useProtectedRoutes({ protectedRoutes: props.protectedRoutes || [] });
909
+
910
+ const latestUserSessions = useLatest(userSessions);
911
+
912
+ const getUserSessions = useMemoizedFn(async () => {
913
+ if (!loadedUserSessions) {
914
+ await refreshUserSessions();
915
+ await sleep(100);
916
+ }
917
+ return latestUserSessions.current;
918
+ });
919
+
920
+ const closeConnect = useMemoizedFn(() => {
921
+ state.open = false;
922
+ connectApi.close();
923
+ });
924
+
925
+ /**
926
+ * 打开 did-connect
927
+ * @param {object} openConnectOptions
928
+ * @param {boolean} openConnectOptions.hideCloseButton - 是否隐藏关闭按钮
929
+ * @param {function} openConnectOptions.onSuccess - 成功回调
930
+ */
931
+ const openConnect = async (openConnectOptions = {}) => {
932
+ let localeAction = state.action;
933
+ if (pageState?.options?.origin === 'switch-did') {
934
+ localeAction = pageState?.options?.origin;
935
+ }
936
+ const messages = translations[localeAction];
937
+
938
+ const connectMessage = messages[currentLocale] || messages.en;
939
+ const extraParams = {
940
+ ...opts.extraParams,
941
+ ...props.extraParams,
942
+ ...pageState.extraParams,
943
+ previousUserDid: state?.user?.did,
944
+ email: state?.user?.email,
945
+ };
946
+
947
+ if (localeAction === 'switch-did' && extraParams?.forceConnected) {
948
+ const t = (key, data = {}) => {
949
+ return translate(translations['switch-specified-did'], key, currentLocale, 'en', data);
950
+ };
951
+ connectMessage.title = t('title');
952
+ connectMessage.scan = t('scan', { did: extraParams.forceConnected });
953
+ connectMessage.confirm = t('confirm');
954
+ connectMessage.success = t('success');
955
+ }
956
+ // HACK: 部分参数不用传递给 server 端,需要手动移除
957
+
958
+ delete extraParams?.openMode;
959
+ const params = {
960
+ action: state.action,
961
+ locale: currentLocale,
962
+ hideCloseButton: openConnectOptions?.hideCloseButton,
963
+ extraParams,
964
+ options: { ...(props.options || {}), ...pageState.options },
965
+ checkTimeout: props.timeout,
966
+ webWalletUrl: props.webWalletUrl,
967
+ messages: connectMessage,
968
+ useSocket: props.useSocket,
969
+ ...omit(props, [
970
+ 'action',
971
+ 'locale',
972
+ // NOTICE: 将 serviceHost 也透传给 DID Connect 组件
973
+ // 'serviceHost',
974
+ 'appendAuthServicePrefix',
975
+ 'autoDisconnect',
976
+ 'children',
977
+ 'timeout',
978
+ 'extraParams',
979
+ 'options',
980
+ 'webWalletUrl',
981
+ 'messages',
982
+ 'useSocket',
983
+ 'lazyRefreshToken',
984
+ 'apiOptions',
985
+ 'protectedRoutes',
986
+ ]),
987
+ // 允许 login/switchProfile/switchDid 的时候传入自定义参数
988
+ ...pick(pageState.extraParams || {}, [
989
+ 'autoConnect',
990
+ 'forceConnected',
991
+ 'saveConnect',
992
+ 'useSocket',
993
+ 'passkeyBehavior',
994
+ ]),
995
+ // baseUrl: state.baseUrl || props.baseUrl || '',
996
+
997
+ // 注意 prefix 经过了特殊处理, 优先级高于 "...rest", 所以放在其后
998
+ prefix: pageState.prefix,
999
+ allowWallet: !isUndefined(pageState.allowWallet)
1000
+ ? pageState.allowWallet
1001
+ : state.action !== 'login' ||
1002
+ ['1', 'true', undefined, null].includes(globalThis.blocklet?.DID_CONNECT_ALLOW_WALLET),
1003
+ onSuccess: async (...args) => {
1004
+ onSuccess(state.action, ...args);
1005
+ if (isFunction(openConnectOptions?.onSuccess)) {
1006
+ await openConnectOptions.onSuccess(...args);
1007
+ }
1008
+ state.open = false;
1009
+ },
1010
+ onClose(...args) {
1011
+ onClose(state.action, ...args);
1012
+ },
1013
+ onError(err) {
1014
+ console.error(err);
1015
+ },
1016
+ };
1017
+ state.open = true;
1018
+
1019
+ if (
1020
+ ['login'].includes(state.action) &&
1021
+ pageState.openMode === 'window' &&
1022
+ !(browser.wallet || browser.arcSphere)
1023
+ ) {
1024
+ try {
1025
+ const mergeParams = {
1026
+ ...params,
1027
+ baseUrl: window.location.origin,
1028
+ };
1029
+ // HACK: 在某些情况下,prefix 中已经携带了域名,这时不能再拼接一次
1030
+ if (!isUrl(pageState.prefix)) {
1031
+ mergeParams.prefix = joinURL(window.location.origin, props.serviceHost, pageState.prefix);
1032
+ }
1033
+ if (!mergeParams.extraParams?.provider) {
1034
+ mergeParams.extraParams.provider = state?.user?.provider || 'wallet';
1035
+ }
1036
+ await connectApi.openPopup(mergeParams, { locale: currentLocale });
1037
+ } catch (err) {
1038
+ if (err instanceof NotOpenError) {
1039
+ closeConnect();
1040
+ }
1041
+ if (err.message === 'Timeout') {
1042
+ Toast.error(didConnectTimeoutError[currentLocale] || didConnectTimeoutError.en);
1043
+ } else {
1044
+ console.error(err);
1045
+ }
1046
+ }
1047
+ } else {
1048
+ connectApi.open(params);
1049
+ }
1050
+ };
1051
+
1052
+ const autoSwitchDid = useMemoizedFn(async () => {
1053
+ // NOTICE: 此处获得的 trimedUrl 是当前 location.href 精简参数后的 url,无需进行安全处理
1054
+ const { params: decodedParams, url: trimedUrl } = await decodeUrlParams();
1055
+ if (decodedParams) {
1056
+ const { switchBehavior, forceConnected, sourceAppPid, showClose } = decodedParams;
1057
+
1058
+ if (forceConnected === state.user.did) {
1059
+ window.history.replaceState(null, 'trim', trimedUrl);
1060
+ } else if (switchBehavior === 'required') {
1061
+ switchDid(() => {}, {
1062
+ openMode: 'redirect',
1063
+ redirect: trimedUrl,
1064
+ sourceAppPid,
1065
+ forceConnected,
1066
+ });
1067
+ } else if (switchBehavior === 'disabled') {
1068
+ window.history.replaceState(null, 'trim', trimedUrl);
1069
+ } else {
1070
+ confirmApi.open({
1071
+ title: translations.switchAccountDialog[currentLocale].title,
1072
+ content: (
1073
+ <>
1074
+ <Typography variant="body1" sx={{ textAlign: 'center', mb: 2 }}>
1075
+ {translations.switchAccountDialog[currentLocale].description}
1076
+ </Typography>
1077
+ <Box sx={{ display: 'flex', alignItems: 'center', color: 'grey.A700' }}>
1078
+ <Box
1079
+ sx={{
1080
+ display: 'flex',
1081
+ flexDirection: 'column',
1082
+ alignItems: 'center',
1083
+ gap: 1.5,
1084
+ lineHeight: 1,
1085
+ }}>
1086
+ <Avatar did={state.user.did} shape="circle" variant="circle" size={80} />
1087
+ <DID did={state.user.did} showAvatar={false} compact responsive={false} />
1088
+ {translations.switchAccountDialog[currentLocale].currentAccount}
1089
+ </Box>
1090
+ <Box sx={{ display: 'flex', justifyContent: 'center', px: 2 }}>
1091
+ <Box
1092
+ component={Icon}
1093
+ icon={KeyboardDoubleArrowRightRoundedIcon}
1094
+ sx={{
1095
+ fontSize: 24,
1096
+ color: palette.grey[400],
1097
+ animation: 'right 2.5s linear infinite',
1098
+
1099
+ '@keyframes right': {
1100
+ '0%': {
1101
+ transform: 'translateX(-3px)',
1102
+ opacity: 0,
1103
+ },
1104
+ '60%': {
1105
+ transform: 'translateX(3px)',
1106
+ opacity: 1,
1107
+ },
1108
+ '100%': {
1109
+ transform: 'translateX(0px)',
1110
+ opacity: 0,
1111
+ },
1112
+ },
1113
+ }}
1114
+ />
1115
+ </Box>
1116
+ <Box
1117
+ sx={{
1118
+ display: 'flex',
1119
+ flexDirection: 'column',
1120
+ alignItems: 'center',
1121
+ gap: 1.5,
1122
+ lineHeight: 1,
1123
+ fontWeight: 500,
1124
+ }}>
1125
+ <Avatar did={forceConnected} shape="circle" variant="circle" size={80} />
1126
+ <DID did={forceConnected} showAvatar={false} compact responsive={false} />
1127
+ {translations.switchAccountDialog[currentLocale].nextAccount}
1128
+ </Box>
1129
+ </Box>
1130
+ </>
1131
+ ),
1132
+ showCloseButton: showClose,
1133
+ showCancelButton: false,
1134
+ confirmButtonText: translations.switchAccountDialog[currentLocale].confirm,
1135
+ async onConfirm(close) {
1136
+ const userSessionList = await getUserSessions();
1137
+ const findUserSession = userSessionList.find((x) => x.userDid === forceConnected);
1138
+
1139
+ if (findUserSession) {
1140
+ loginUserSession(findUserSession).then((result) => {
1141
+ updateConnectedInfo(
1142
+ {
1143
+ connected_did: findUserSession.user.did,
1144
+ connected_pk: findUserSession.user.pk,
1145
+ connected_wallet_os: findUserSession.extra.walletOS,
1146
+ connected_app: getAppId(),
1147
+ },
1148
+ true
1149
+ );
1150
+ handleLoginResult({ ...result, encrypted: false });
1151
+ handleRefreshUser({ showProgress: true }).then(async () => {
1152
+ await sleep(200);
1153
+ events.emit(EVENTS.LOGIN, result, decrypt, session.current);
1154
+ notifyBridge(session.current);
1155
+ await connectToDidSpaceForFullAccess();
1156
+ });
1157
+ });
1158
+ close();
1159
+ return;
1160
+ }
1161
+ switchDid(
1162
+ () => {
1163
+ close();
1164
+ window.location.replace(trimedUrl);
1165
+ },
1166
+ { sourceAppPid, forceConnected, enableSwitchApp: false },
1167
+ { showQuickConnect: true }
1168
+ );
1169
+ },
1170
+ });
1171
+ }
1172
+ }
1173
+ });
1174
+
1175
+ /**
1176
+ * @type {import('../types').OpenDidConnect}
1177
+ */
1178
+ const openDidConnect = useMemoizedFn(async (params, options) => {
1179
+ const requirements = Object.assign(
1180
+ {
1181
+ login: true,
1182
+ bindWallet: true,
1183
+ bindDidSpaces: false,
1184
+ },
1185
+ options?.requirements
1186
+ );
1187
+ const openMode = options?.openMode || 'popup';
1188
+ if (requirements.login) {
1189
+ await ensureLogin();
1190
+ }
1191
+ if (requirements.bindWallet) {
1192
+ await new Promise((resolve) => {
1193
+ bindWallet(() => {
1194
+ resolve();
1195
+ });
1196
+ });
1197
+ }
1198
+ if (requirements.bindDidSpaces) {
1199
+ if (!(await didSpacesIsRequired(state?.user))) {
1200
+ await new Promise((resolve) => {
1201
+ if (requirements.bindDidSpaces === 'full') {
1202
+ connectToDidSpaceForFullAccess({
1203
+ onSuccess() {
1204
+ resolve();
1205
+ },
1206
+ });
1207
+ } else if (requirements.bindDidSpaces === 'read') {
1208
+ connectToDidSpaceForImport({
1209
+ onSuccess() {
1210
+ resolve();
1211
+ },
1212
+ });
1213
+ }
1214
+ });
1215
+ }
1216
+ }
1217
+
1218
+ if (openMode === 'window') {
1219
+ connectApi.openPopup(params, options);
1220
+ } else if (openMode === 'popup') {
1221
+ connectApi.open(params);
1222
+ }
1223
+ });
1224
+
1225
+ useUpdateEffect(() => {
1226
+ const count = Number(state.unReadCount || 0); // 避免 unReadCount 为 string 类型
1227
+ setUnReadCount(count);
1228
+ }, [state.unReadCount]);
1229
+
1230
+ const removeMagicToken = useMemoizedFn(() => {
1231
+ if (searchParams.get('magicToken')) {
1232
+ searchParams.delete('magicToken');
1233
+ }
1234
+ if (searchParams.toString()) {
1235
+ window.history.replaceState({}, '', `${window.location.pathname}?${searchParams.toString()}`);
1236
+ }
1237
+ });
1238
+
1239
+ const verifyMagicToken = useMemoizedFn((magicToken) => {
1240
+ return new Promise((resolve, reject) => {
1241
+ connectApi.open({
1242
+ action: 'login',
1243
+ locale: currentLocale,
1244
+ prefix: pageState.prefix,
1245
+ useSocket: false,
1246
+ messages: {
1247
+ title: currentLocale === 'zh' ? '验证 MagicLink Token' : 'Verify MagicLink Token',
1248
+ },
1249
+ magicToken,
1250
+ onSuccess(...args) {
1251
+ onLogin(...args);
1252
+ removeMagicToken();
1253
+ resolve();
1254
+ },
1255
+ onError(err) {
1256
+ console.error(err);
1257
+ reject(err);
1258
+ },
1259
+ });
1260
+ });
1261
+ });
1262
+
1263
+ const currentSession = {
1264
+ ...state,
1265
+ loading: pageState.autoConnect ? !state.user || state.loading : state.loading,
1266
+ openDidConnect,
1267
+ login,
1268
+ logout,
1269
+ switchDid,
1270
+ autoSwitchDid,
1271
+ refreshProfile,
1272
+ switchProfile,
1273
+ switchPassport,
1274
+ connectToDidSpaceForImport,
1275
+ connectToDidSpaceForFullAccess,
1276
+ bindWallet,
1277
+ refresh: handleRefreshUser,
1278
+ updateConnectedInfo,
1279
+ // federated relates
1280
+ federatedMaster,
1281
+ // oauth relates
1282
+ useOAuth,
1283
+ OAuthProvider,
1284
+ OAuthConsumer,
1285
+ OAuthContext,
1286
+ // passkey relates
1287
+ usePasskey,
1288
+ PasskeyProvider,
1289
+ PasskeyConsumer,
1290
+ PasskeyContext,
1291
+ // user related
1292
+ useDid,
1293
+ WrapDid,
1294
+ getUserSessions,
1295
+ async loginUserSession(userSessionItem) {
1296
+ const loginResult = await loginUserSession(userSessionItem);
1297
+ updateConnectedInfo(
1298
+ {
1299
+ connected_did: userSessionItem.user.did,
1300
+ connected_pk: userSessionItem.user.pk,
1301
+ connected_wallet_os: userSessionItem.extra.walletOS,
1302
+ connected_app: getAppId(),
1303
+ },
1304
+ true
1305
+ );
1306
+ handleLoginResult({ ...loginResult, encrypted: false });
1307
+ await handleRefreshUser({ showProgress: true });
1308
+ await sleep(200);
1309
+ events.emit(EVENTS.LOGIN, loginResult, decrypt, session.current);
1310
+ notifyBridge(session.current);
1311
+ await connectToDidSpaceForFullAccess();
1312
+ },
1313
+ unReadCount,
1314
+ setUnReadCount,
1315
+
1316
+ verifyMagicToken,
1317
+ // 二次认证相关方法
1318
+ withSecondaryAuth,
1319
+ };
1320
+
1321
+ const session = useLatest(currentSession);
1322
+
1323
+ const sessionValue = {
1324
+ api: service,
1325
+ events,
1326
+ storage: sessionTokenStorage,
1327
+ connectApi,
1328
+ session: session.current,
1329
+ };
1330
+
1331
+ const callbacks = {
1332
+ login: onLogin,
1333
+ 'switch-profile': onSwitchProfile,
1334
+ 'switch-passport': onSwitchPassport,
1335
+ 'bind-wallet': onBindWallet,
1336
+ };
1337
+
1338
+ // user change 事件
1339
+ useUpdateEffect(() => {
1340
+ if (state.initialized) {
1341
+ const isFirst = !prevInitialized;
1342
+ events.emit('change', { user: cloneDeep(state.user), isFirst });
1343
+ if (state.user) {
1344
+ if (globalThis.blocklet) {
1345
+ const data = {
1346
+ did: state.user.did,
1347
+ host: window.location.host,
1348
+ appPid: globalThis.blocklet.appPid,
1349
+ visitorId: getVisitorId(),
1350
+ sourceAppPid: state.user.sourceAppPid,
1351
+ fullName: state.user.fullName,
1352
+ };
1353
+ debug('bridge callArc: onChange', {
1354
+ isFirst,
1355
+ user: data,
1356
+ });
1357
+ bridge.callArc('onChange', {
1358
+ ...data,
1359
+ user: data,
1360
+ isFirst,
1361
+ });
1362
+ }
1363
+ }
1364
+ }
1365
+ }, [state.user, state.initialized, events]);
1366
+
1367
+ // 检查当前登录的用户与 url 中参数指定的用户是否一致
1368
+ useUpdateEffect(() => {
1369
+ if (state.initialized && state.user) {
1370
+ autoSwitchDid();
1371
+ }
1372
+ }, [state.initialized]);
1373
+
1374
+ useMount(async () => {
1375
+ const bridgeOnLogin = (result) => {
1376
+ if (isNil(result.encrypted)) {
1377
+ result.encrypted = false;
1378
+ }
1379
+ if (window.temporaryDIDConnectOnSuccess instanceof Function) {
1380
+ debug('bridgeOnLogin: temporaryDIDConnectOnSuccess', { result });
1381
+ window.temporaryDIDConnectOnSuccess(result, (v) => v);
1382
+ } else {
1383
+ debug('bridgeOnLogin: onLogin', { result });
1384
+ onLogin(result);
1385
+ }
1386
+ };
1387
+ bridge.registerBlocklet('callLoginOAuth', (options) => {
1388
+ debug('bridge registerBlocklet: callLoginOAuth', options);
1389
+ connectApi.loginOAuth({
1390
+ ...options,
1391
+ onLogin: bridgeOnLogin,
1392
+ });
1393
+ });
1394
+ bridge.registerBlocklet('logout', logout);
1395
+ // NOTICE: login 是指 arcsphere 已经完成登录,传回的 result 是登录后的数据
1396
+ bridge.registerBlocklet('login', (result) => {
1397
+ debug('bridge registerBlocklet: login', result);
1398
+ bridgeOnLogin(result);
1399
+ });
1400
+
1401
+ // 确保 普通浏览器中 visitorId 存在
1402
+ ensureVisitorId();
1403
+ const visitorId = getVisitorId();
1404
+
1405
+ if (!visitorId) {
1406
+ // 确保 ArcSphere 中 visitorId 存在
1407
+ if (browser.arcSphere) {
1408
+ debug('bridge callArc: getVisitorId');
1409
+ const walletVisitorId = await bridge.callArc('getVisitorId');
1410
+ debug('bridge callArc: getVisitorId result', { walletVisitorId });
1411
+ setVisitorId(walletVisitorId);
1412
+ } else if (browser.wallet) {
1413
+ const walletVisitorId = await getMobileVisitorId();
1414
+ setVisitorId(walletVisitorId);
1415
+ }
1416
+ }
1417
+
1418
+ // ensure we are in the same app
1419
+ const connectedApp = Cookie.get('connected_app');
1420
+ const actualApp = getAppId();
1421
+
1422
+ if (props.autoDisconnect && connectedApp && actualApp && connectedApp !== actualApp) {
1423
+ clearSession();
1424
+ state.initialized = true;
1425
+ if (pageState.autoConnect) {
1426
+ openConnect();
1427
+ } else {
1428
+ closeConnect();
1429
+ }
1430
+ return;
1431
+ }
1432
+
1433
+ const sessionToken = getSessionToken();
1434
+
1435
+ // HACK: 如果页面处于 iframe 中,将不进行自动登录的逻辑,防止页面会产生两次钱包的自动登录流程
1436
+ if (window?.self === window?.parent) {
1437
+ let platform = 'web';
1438
+ if (browser.mobile.apple.device) {
1439
+ platform = 'ios';
1440
+ } else if (browser.mobile.android.device) {
1441
+ platform = 'android';
1442
+ }
1443
+ if (browser.wallet && checkEnableAutoLogin({ version: browser.walletVersion, platform })) {
1444
+ if (!sessionToken) {
1445
+ try {
1446
+ state.loading = true;
1447
+ const result = await loginInMobile();
1448
+ if (result) {
1449
+ await onLogin({
1450
+ sessionToken: result.sessionToken,
1451
+ refreshToken: result.refreshToken,
1452
+ visitorId: result.visitorId,
1453
+ encrypted: false,
1454
+ });
1455
+ state.initialized = true;
1456
+ return;
1457
+ }
1458
+ } finally {
1459
+ state.loading = false;
1460
+ }
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ const magicToken = searchParams.get('magicToken');
1466
+ if (magicToken) {
1467
+ try {
1468
+ state.initialized = true;
1469
+ await verifyMagicToken(magicToken);
1470
+ } catch (err) {
1471
+ console.error('verifyMagicToken failed', err);
1472
+ }
1473
+ }
1474
+
1475
+ if (sessionToken) {
1476
+ await handleRefreshUser({ showProgress: true });
1477
+ state.initialized = true;
1478
+
1479
+ if (state.user) {
1480
+ await connectToDidSpaceForFullAccess();
1481
+ }
1482
+ return;
1483
+ }
1484
+
1485
+ if (typeof window !== 'undefined') {
1486
+ // If a login token exist in url, set that token in storage
1487
+ const url = new URL(window.location.href);
1488
+ const loginToken = url.searchParams.get('loginToken');
1489
+ if (loginToken) {
1490
+ handleLoginResult({ loginToken, encrypted: false });
1491
+
1492
+ url.searchParams.delete('loginToken');
1493
+ window.history.replaceState({}, window.title, url.href);
1494
+ return;
1495
+ }
1496
+ }
1497
+
1498
+ // If a refresh token exist and session token NOT exist, do refresh session
1499
+ const refreshToken = getRefreshToken();
1500
+ if (refreshToken) {
1501
+ await handleRefreshToken(true);
1502
+ state.initialized = true;
1503
+ return;
1504
+ }
1505
+
1506
+ try {
1507
+ // await tryAutoLoginFederated();
1508
+ } catch (err) {
1509
+ console.error('Federated: tryAutoLogin failed', err);
1510
+ } finally {
1511
+ state.initialized = true;
1512
+ }
1513
+
1514
+ if (pageState.autoConnect) {
1515
+ openConnect();
1516
+ } else {
1517
+ closeConnect();
1518
+ }
1519
+
1520
+ if (
1521
+ browser.arcSphere &&
1522
+ ![
1523
+ // 不应该唤起自动登录的路由
1524
+ joinURL(BLOCKLET_SERVICE_PATH_PREFIX, 'login'),
1525
+ joinURL(BLOCKLET_SERVICE_PATH_PREFIX, 'connect'),
1526
+ joinURL(BLOCKLET_SERVICE_PATH_PREFIX, 'lost-passport'),
1527
+ ].includes(window.location.pathname)
1528
+ ) {
1529
+ if (!sessionToken) {
1530
+ try {
1531
+ debug('bridge callArc: autoLogin');
1532
+ const result = await bridge.callArc('autoLogin');
1533
+ debug('bridgecallArc: autoLogin result', result);
1534
+ if (result) {
1535
+ await onLogin({
1536
+ sessionToken: result.sessionToken,
1537
+ refreshToken: result.refreshToken,
1538
+ visitorId: result.visitorId,
1539
+ encrypted: false,
1540
+ });
1541
+ }
1542
+ } catch (err) {
1543
+ logger.error('Failed to autoLogin ArcSphere', err);
1544
+ }
1545
+ }
1546
+ }
1547
+ });
1548
+ const seamless = searchParams.has('popup');
1549
+
1550
+ if (checkMatch()) {
1551
+ if (!state.initialized) {
1552
+ return <Center>{seamless ? null : <CircularProgress />}</Center>;
1553
+ }
1554
+ }
1555
+
1556
+ const providerProps = {
1557
+ locale: currentLocale,
1558
+ onSwitchPassport: ({ sessionToken, refreshToken }) =>
1559
+ onSwitchPassport({ sessionToken, refreshToken, encrypted: false }),
1560
+ onUnbindOAuth: () => handleRefreshUser(),
1561
+ onBindOAuth: () => handleRefreshUser(),
1562
+ onAddPasskey: () => handleRefreshUser(),
1563
+ onRemovePasskey: () => handleRefreshUser(),
1564
+ };
1565
+
1566
+ return (
1567
+ <Provider value={sessionValue}>
1568
+ <PasskeyProvider {...providerProps}>
1569
+ <OAuthProvider {...providerProps}>
1570
+ {!state.open && typeof props.children === 'function' ? props.children(state) : props.children}
1571
+ {connectHolder}
1572
+ {confirmHolder}
1573
+ {DidSpacesGuideView}
1574
+ {opts.rolling && <WindowFocusAware callback={() => renewToken()} />}
1575
+ <WindowFocusAware callback={syncSessionSate} />
1576
+ </OAuthProvider>
1577
+ </PasskeyProvider>
1578
+ </Provider>
1579
+ );
1580
+ }
1581
+
1582
+ SessionProvider.propTypes = {
1583
+ children: PropTypes.any.isRequired,
1584
+ serviceHost: PropTypes.string,
1585
+ action: PropTypes.string,
1586
+ prefix: PropTypes.string,
1587
+ appendAuthServicePrefix: PropTypes.bool,
1588
+ locale: PropTypes.string,
1589
+ timeout: PropTypes.number,
1590
+ autoConnect: PropTypes.bool, // should we open connect dialog when session not found
1591
+ autoDisconnect: PropTypes.bool, // should we auto disconnect on appId mismatch
1592
+ useSocket: PropTypes.bool, // should we auto disconnect on appId mismatch
1593
+ extraParams: PropTypes.object,
1594
+ options: PropTypes.object,
1595
+ webWalletUrl: PropTypes.string,
1596
+ protectedRoutes: PropTypes.arrayOf(PropTypes.string),
1597
+ apiOptions: PropTypes.object,
1598
+ lazyRefreshToken: PropTypes.bool,
1599
+ };
1600
+
1601
+ function withSession(Component) {
1602
+ return function WithSessionComponent(props) {
1603
+ return (
1604
+ <ToastProvider>
1605
+ <Consumer>{(sessionProps) => <Component {...props} {...sessionProps} />}</Consumer>
1606
+ </ToastProvider>
1607
+ );
1608
+ };
1609
+ }
1610
+
1611
+ return {
1612
+ SessionProvider: (props) => {
1613
+ return (
1614
+ <ToastProvider>
1615
+ <FederatedProvider>
1616
+ <SessionProvider {...props} />
1617
+ </FederatedProvider>
1618
+ </ToastProvider>
1619
+ );
1620
+ },
1621
+ SessionConsumer: Consumer,
1622
+ SessionContext,
1623
+ withSession,
1624
+ };
1625
+ }
1626
+
1627
+ export function createAuthServiceSessionContext({ storageEngine = 'cookie' } = {}) {
1628
+ const storageKey = SESSION_TOKEN_STORAGE_KEY;
1629
+ const refreshTokenStorageKey = REFRESH_TOKEN_STORAGE_KEY;
1630
+
1631
+ // componentId
1632
+ let componentId = null;
1633
+ if (typeof window !== 'undefined') {
1634
+ const blocklet = globalThis.blocklet || {};
1635
+
1636
+ // FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时,不应该直接从 window.blocklet 去取值
1637
+ componentId = blocklet.componentId;
1638
+ }
1639
+
1640
+ if (storageEngine === 'cookie') {
1641
+ let path = '/';
1642
+ // FIXME: @zhanghan sessionToken 也应该从 jwt 中读取过期时间,而不是通过配置来判断?
1643
+ let sessionTokenExpireInDays = 1;
1644
+
1645
+ if (typeof window !== 'undefined') {
1646
+ const env = window.env || {};
1647
+ const blocklet = window.blocklet || {};
1648
+
1649
+ // FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时,不应该直接从 window.blocklet 去取值
1650
+ path = env.groupPathPrefix || blocklet.groupPrefix || blocklet.prefix || '';
1651
+ path = path.replace(/\/+$/, '');
1652
+ path = path || '/';
1653
+
1654
+ // for backward compatibility
1655
+ // 不支持 refreshToken 的旧版 server 不会在 window.blocklet 中返回 serverVersion
1656
+ if (blocklet.serverVersion) {
1657
+ sessionTokenExpireInDays = formatCacheTtl(window.blocklet?.settings?.session?.cacheTtl, 1 / 24); // 1h
1658
+ }
1659
+ }
1660
+
1661
+ return createSessionContext(
1662
+ storageKey,
1663
+ 'cookie',
1664
+ {
1665
+ path,
1666
+ returnDomain: false,
1667
+ expireInDays: sessionTokenExpireInDays,
1668
+ },
1669
+ {
1670
+ appendAuthServicePrefix: true,
1671
+ extraParams: { componentId },
1672
+ refreshTokenStorageKey,
1673
+ }
1674
+ );
1675
+ }
1676
+
1677
+ if (storageEngine === 'localStorage') {
1678
+ return createSessionContext(storageKey, 'ls', {}, true, {
1679
+ appendAuthServicePrefix: true,
1680
+ extraParams: { componentId },
1681
+ });
1682
+ }
1683
+
1684
+ throw new Error('storageEngine must be cookie or localStorage');
1685
+ }
1686
+
1687
+ export { SessionContext };