@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.
- package/LICENSE +13 -0
- package/README.md +134 -0
- package/lib/Address/index.js +4 -0
- package/lib/Avatar/index.js +4 -0
- package/lib/Button/index.js +17 -0
- package/lib/Connect/assets/locale.js +143 -0
- package/lib/Connect/assets/login-bg.png +0 -0
- package/lib/Connect/assets/login-slogan.js +9 -0
- package/lib/Connect/components/action-button.js +26 -0
- package/lib/Connect/components/app-tips.js +132 -0
- package/lib/Connect/components/auto-height.js +31 -0
- package/lib/Connect/components/back-button.js +24 -0
- package/lib/Connect/components/connect-status.js +263 -0
- package/lib/Connect/components/did-connect-title.js +126 -0
- package/lib/Connect/components/download-tips.js +52 -0
- package/lib/Connect/components/loading.js +26 -0
- package/lib/Connect/components/login-item/connect-choose-list.js +249 -0
- package/lib/Connect/components/login-item/login-method-item.js +129 -0
- package/lib/Connect/components/login-item/mobile-login-item.js +114 -0
- package/lib/Connect/components/login-item/passkey-login-item.js +44 -0
- package/lib/Connect/components/login-item/web-login-item.js +97 -0
- package/lib/Connect/components/mask-overlay.js +34 -0
- package/lib/Connect/components/refresh-overlay.js +57 -0
- package/lib/Connect/components/switch-app.js +70 -0
- package/lib/Connect/contexts/state.js +142 -0
- package/lib/Connect/fullpage.js +5 -0
- package/lib/Connect/hooks/auth-url.js +23 -0
- package/lib/Connect/hooks/method-list.js +46 -0
- package/lib/Connect/hooks/page-show.js +17 -0
- package/lib/Connect/hooks/security.js +27 -0
- package/lib/Connect/hooks/token.js +305 -0
- package/lib/Connect/hooks/use-apps.js +19 -0
- package/lib/Connect/hooks/use-quick-connect.js +97 -0
- package/lib/Connect/index.js +498 -0
- package/lib/Connect/landing-page.js +5 -0
- package/lib/Connect/plugins/email/index.js +62 -0
- package/lib/Connect/plugins/email/list-item.js +28 -0
- package/lib/Connect/plugins/email/placeholder.js +283 -0
- package/lib/Connect/plugins/index.js +4 -0
- package/lib/Connect/use-connect.js +164 -0
- package/lib/Connect/with-blocklet.js +15 -0
- package/lib/Connect/with-bridge-call.js +108 -0
- package/lib/Federated/context.js +61 -0
- package/lib/Federated/index.js +7 -0
- package/lib/Logo/index.js +4 -0
- package/lib/OAuth/context.js +234 -0
- package/lib/OAuth/guest.svg.js +5 -0
- package/lib/OAuth/index.js +7 -0
- package/lib/OAuth/passport-switcher.js +114 -0
- package/lib/Passkey/actions.js +165 -0
- package/lib/Passkey/constants.js +4 -0
- package/lib/Passkey/context.js +266 -0
- package/lib/Passkey/dialog.js +277 -0
- package/lib/Passkey/icon.js +13 -0
- package/lib/Passkey/index.js +9 -0
- package/lib/Service/index.js +62 -0
- package/lib/Session/assets/did-spaces-guide-cover.svg.js +135 -0
- package/lib/Session/assets/did-spaces-guide-icon.svg.js +9 -0
- package/lib/Session/context.js +5 -0
- package/lib/Session/did-spaces-guide.js +136 -0
- package/lib/Session/hooks/use-federated.js +64 -0
- package/lib/Session/hooks/use-mobile.js +8 -0
- package/lib/Session/hooks/use-protected-routes.js +11 -0
- package/lib/Session/hooks/use-session-token.js +169 -0
- package/lib/Session/hooks/use-verify.js +45 -0
- package/lib/Session/index.js +896 -0
- package/lib/Session/libs/constants.js +15 -0
- package/lib/Session/libs/did-spaces.js +10 -0
- package/lib/Session/libs/federated.js +42 -0
- package/lib/Session/libs/index.js +15 -0
- package/lib/Session/libs/locales.js +161 -0
- package/lib/Session/libs/login-mobile.js +55 -0
- package/lib/Session/window-focus-aware.js +17 -0
- package/lib/SessionManager/index.js +4 -0
- package/lib/Storage/engine/cookie.js +21 -0
- package/lib/Storage/engine/local-storage.js +36 -0
- package/lib/Storage/index.js +23 -0
- package/lib/User/index.js +6 -0
- package/lib/User/use-did.js +59 -0
- package/lib/User/wrap-did.js +13 -0
- package/lib/WebWalletSWKeeper/index.js +5 -0
- package/lib/constant.js +22 -0
- package/lib/error.js +8 -0
- package/lib/hooks/use-locale.js +7 -0
- package/lib/index.js +33 -0
- package/lib/locales/en.js +17 -0
- package/lib/locales/index.js +10 -0
- package/lib/locales/zh.js +17 -0
- package/lib/package.json.js +7 -0
- package/lib/types.d.ts +355 -0
- package/lib/utils.js +214 -0
- package/package.json +84 -0
- package/src/Address/index.jsx +2 -0
- package/src/Avatar/index.jsx +2 -0
- package/src/Button/Button.stories.jsx +7 -0
- package/src/Button/index.jsx +21 -0
- package/src/Connect/Connect.stories.jsx +34 -0
- package/src/Connect/assets/locale.js +145 -0
- package/src/Connect/assets/login-bg.png +0 -0
- package/src/Connect/assets/login-slogan.js +7 -0
- package/src/Connect/components/action-button.jsx +22 -0
- package/src/Connect/components/app-tips.jsx +156 -0
- package/src/Connect/components/auto-height.jsx +38 -0
- package/src/Connect/components/back-button.jsx +23 -0
- package/src/Connect/components/connect-status.jsx +259 -0
- package/src/Connect/components/did-connect-title.jsx +106 -0
- package/src/Connect/components/download-tips.jsx +55 -0
- package/src/Connect/components/loading.jsx +25 -0
- package/src/Connect/components/login-item/connect-choose-list.jsx +304 -0
- package/src/Connect/components/login-item/login-method-item.jsx +118 -0
- package/src/Connect/components/login-item/mobile-login-item.jsx +179 -0
- package/src/Connect/components/login-item/passkey-login-item.jsx +52 -0
- package/src/Connect/components/login-item/web-login-item.jsx +149 -0
- package/src/Connect/components/mask-overlay.jsx +32 -0
- package/src/Connect/components/refresh-overlay.jsx +52 -0
- package/src/Connect/components/switch-app.jsx +69 -0
- package/src/Connect/contexts/state.jsx +219 -0
- package/src/Connect/fullpage.jsx +3 -0
- package/src/Connect/hooks/auth-url.js +31 -0
- package/src/Connect/hooks/method-list.js +121 -0
- package/src/Connect/hooks/page-show.js +24 -0
- package/src/Connect/hooks/security.js +40 -0
- package/src/Connect/hooks/token.js +639 -0
- package/src/Connect/hooks/use-apps.js +69 -0
- package/src/Connect/hooks/use-quick-connect.js +130 -0
- package/src/Connect/index.jsx +600 -0
- package/src/Connect/landing-page.jsx +3 -0
- package/src/Connect/plugins/email/index.jsx +82 -0
- package/src/Connect/plugins/email/list-item.jsx +31 -0
- package/src/Connect/plugins/email/placeholder.jsx +365 -0
- package/src/Connect/plugins/index.js +2 -0
- package/src/Connect/use-connect.jsx +321 -0
- package/src/Connect/with-blocklet.jsx +26 -0
- package/src/Connect/with-bridge-call.jsx +138 -0
- package/src/Federated/context.jsx +93 -0
- package/src/Federated/index.jsx +1 -0
- package/src/Logo/index.jsx +2 -0
- package/src/OAuth/context.jsx +346 -0
- package/src/OAuth/guest.svg +20 -0
- package/src/OAuth/index.jsx +1 -0
- package/src/OAuth/passport-switcher.jsx +133 -0
- package/src/Passkey/actions.jsx +212 -0
- package/src/Passkey/constants.js +2 -0
- package/src/Passkey/context.jsx +381 -0
- package/src/Passkey/dialog.jsx +391 -0
- package/src/Passkey/icon.jsx +10 -0
- package/src/Passkey/index.jsx +2 -0
- package/src/Service/index.jsx +96 -0
- package/src/Session/assets/did-spaces-guide-cover.svg +128 -0
- package/src/Session/assets/did-spaces-guide-icon.svg +7 -0
- package/src/Session/context.jsx +7 -0
- package/src/Session/did-spaces-guide.jsx +173 -0
- package/src/Session/hooks/use-federated.js +88 -0
- package/src/Session/hooks/use-mobile.jsx +6 -0
- package/src/Session/hooks/use-protected-routes.js +16 -0
- package/src/Session/hooks/use-session-token.js +365 -0
- package/src/Session/hooks/use-verify.jsx +76 -0
- package/src/Session/index.jsx +1687 -0
- package/src/Session/libs/constants.js +14 -0
- package/src/Session/libs/did-spaces.js +38 -0
- package/src/Session/libs/federated.js +79 -0
- package/src/Session/libs/index.js +5 -0
- package/src/Session/libs/locales.js +160 -0
- package/src/Session/libs/login-mobile.js +80 -0
- package/src/Session/window-focus-aware.jsx +28 -0
- package/src/SessionManager/index.jsx +2 -0
- package/src/Storage/engine/cookie.js +23 -0
- package/src/Storage/engine/local-storage.js +55 -0
- package/src/Storage/index.js +25 -0
- package/src/User/index.js +4 -0
- package/src/User/use-did.js +80 -0
- package/src/User/wrap-did.jsx +18 -0
- package/src/WebWalletSWKeeper/index.jsx +3 -0
- package/src/constant.js +26 -0
- package/src/error.js +6 -0
- package/src/hooks/use-locale.jsx +6 -0
- package/src/index.js +43 -0
- package/src/locales/en.jsx +15 -0
- package/src/locales/index.jsx +13 -0
- package/src/locales/zh.jsx +15 -0
- package/src/types.d.ts +355 -0
- package/src/utils.js +395 -0
- package/vite.config.mjs +29 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import tweetnacl from 'tweetnacl';
|
|
2
|
+
import { useLocalStorageState } from 'ahooks';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
import { encodeKey, decrypt as _decrypt, encrypt as _encrypt } from '../../utils';
|
|
6
|
+
|
|
7
|
+
const keyPair = tweetnacl.box.keyPair();
|
|
8
|
+
|
|
9
|
+
export default function useSecurity() {
|
|
10
|
+
const [encryptKey, setEncryptKey] = useLocalStorageState('__encKey', {
|
|
11
|
+
defaultValue: encodeKey(keyPair.publicKey),
|
|
12
|
+
deserializer: (str) => str,
|
|
13
|
+
serializer: (str) => str,
|
|
14
|
+
});
|
|
15
|
+
const [decryptKey, setDecryptKey] = useLocalStorageState('__decKey', {
|
|
16
|
+
defaultValue: encodeKey(keyPair.secretKey),
|
|
17
|
+
deserializer: (str) => str,
|
|
18
|
+
serializer: (str) => str,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!localStorage.getItem('__encKey')) {
|
|
23
|
+
setEncryptKey(encodeKey(keyPair.publicKey));
|
|
24
|
+
}
|
|
25
|
+
if (!localStorage.getItem('__decKey')) {
|
|
26
|
+
setDecryptKey(encodeKey(keyPair.secretKey));
|
|
27
|
+
}
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const decrypt = (v, _encryptKey = encryptKey, _decryptKey = decryptKey) => _decrypt(v, _encryptKey, _decryptKey);
|
|
32
|
+
const encrypt = (v, _encryptKey = encryptKey) => _encrypt(v, _encryptKey);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
encryptKey,
|
|
36
|
+
decryptKey,
|
|
37
|
+
decrypt,
|
|
38
|
+
encrypt,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import get from 'lodash/get';
|
|
3
|
+
import Cookie from 'js-cookie';
|
|
4
|
+
import omitBy from 'lodash/omitBy';
|
|
5
|
+
import isNil from 'lodash/isNil';
|
|
6
|
+
import omit from 'lodash/omit';
|
|
7
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
8
|
+
import { useCreation, useMemoizedFn, useReactive } from 'ahooks';
|
|
9
|
+
import useInterval from '@arcblock/react-hooks/lib/useInterval';
|
|
10
|
+
import useBrowser from '@arcblock/react-hooks/lib/useBrowser';
|
|
11
|
+
import { getVisitorId, isUrl, stringifyQuery } from '@arcblock/ux/lib/Util';
|
|
12
|
+
import { WsClient } from '@arcblock/ws';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
decodeConnectUrl,
|
|
16
|
+
parseTokenFromConnectUrl,
|
|
17
|
+
updateConnectedInfo,
|
|
18
|
+
parseNextWorkflow,
|
|
19
|
+
createAxios,
|
|
20
|
+
sleep,
|
|
21
|
+
getConnectedInfo,
|
|
22
|
+
} from '../../utils';
|
|
23
|
+
|
|
24
|
+
import translations from '../assets/locale';
|
|
25
|
+
import useSecurity from './security';
|
|
26
|
+
import usePageShow from './page-show';
|
|
27
|
+
import { RELAY_SOCKET_PREFIX } from '../../constant';
|
|
28
|
+
|
|
29
|
+
function getSocketHost(baseUrl) {
|
|
30
|
+
const targetUrl = baseUrl || window.location.href;
|
|
31
|
+
return new URL(targetUrl).host;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getExtraHeaders(baseUrl) {
|
|
35
|
+
const headers = {};
|
|
36
|
+
const authUrl = baseUrl || window.location.href;
|
|
37
|
+
|
|
38
|
+
if (authUrl) {
|
|
39
|
+
const { hostname, protocol, port } = new URL(authUrl);
|
|
40
|
+
headers['x-real-hostname'] = hostname;
|
|
41
|
+
headers['x-real-port'] = port;
|
|
42
|
+
headers['x-real-protocol'] = protocol.endsWith(':') ? protocol.substring(0, protocol.length - 1) : protocol;
|
|
43
|
+
}
|
|
44
|
+
return headers;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fns = {};
|
|
48
|
+
function createTokenFn({ action, prefix, checkFn, extraParams, baseUrl }) {
|
|
49
|
+
const key = `${prefix}/token?${stringifyQuery(extraParams)}`;
|
|
50
|
+
if (!fns[key]) {
|
|
51
|
+
fns[key] = async function createToken() {
|
|
52
|
+
const startTime = +new Date();
|
|
53
|
+
const res = await checkFn(key, { headers: getExtraHeaders(baseUrl) });
|
|
54
|
+
const endTime = +new Date();
|
|
55
|
+
if (endTime - startTime < 500) {
|
|
56
|
+
await sleep(500 - (endTime - startTime));
|
|
57
|
+
}
|
|
58
|
+
if (res?.data?.token) {
|
|
59
|
+
return res.data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (res?.data?.error) {
|
|
63
|
+
throw new Error(res.data.error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const errorUrl = isUrl(prefix) ? prefix : `${baseUrl}${prefix}`;
|
|
67
|
+
throw new Error(`Error generating ${action} QR Code from: ${errorUrl}`);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return fns[key];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 从 url params 中获取已存在的 session (token & connect url)
|
|
74
|
+
const parseExistingSession = () => {
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(window.location.href);
|
|
77
|
+
const connectUrlParam = url.searchParams.get('__connect_url__');
|
|
78
|
+
if (!connectUrlParam) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const connectUrl = decodeConnectUrl(connectUrlParam);
|
|
82
|
+
const token = parseTokenFromConnectUrl(connectUrl);
|
|
83
|
+
return {
|
|
84
|
+
token,
|
|
85
|
+
url: connectUrl,
|
|
86
|
+
};
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return {
|
|
89
|
+
error: e,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时,不应该直接从 window.blocklet 去取值
|
|
95
|
+
const getAppId = () =>
|
|
96
|
+
get(globalThis, 'blocklet.appPid') || get(globalThis, 'blocklet.appId') || get(globalThis, 'env.appId') || '';
|
|
97
|
+
const getAppPrefix = () =>
|
|
98
|
+
(get(globalThis, 'env.apiPrefix') || '/').replace(/\/$/, '').replace(RELAY_SOCKET_PREFIX, '');
|
|
99
|
+
const getRelayChannel = (token) => `relay:${getAppId()}:${token}`;
|
|
100
|
+
const getRelayProtocol = () => (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
|
101
|
+
const isSameOrigin = (baseUrl) => !baseUrl || new URL(baseUrl).origin === window.location.origin;
|
|
102
|
+
const isSessionEnded = (status) => ['succeed', 'error', 'timeout', 'busy'].includes(status);
|
|
103
|
+
|
|
104
|
+
export default function useToken({
|
|
105
|
+
action,
|
|
106
|
+
checkFn,
|
|
107
|
+
checkInterval,
|
|
108
|
+
checkTimeout,
|
|
109
|
+
extraParams,
|
|
110
|
+
locale,
|
|
111
|
+
prefix,
|
|
112
|
+
tokenKey,
|
|
113
|
+
encKey,
|
|
114
|
+
onError,
|
|
115
|
+
onSuccess,
|
|
116
|
+
baseUrl,
|
|
117
|
+
autoConnect = true,
|
|
118
|
+
forceConnected = true,
|
|
119
|
+
saveConnect = true, // FIXME: @zhanghan 将来需要设置为默认 false,仅在需要的时候进行保存 login, switch/role/profile
|
|
120
|
+
useSocket = true,
|
|
121
|
+
provider = 'wallet',
|
|
122
|
+
}) {
|
|
123
|
+
/**
|
|
124
|
+
* @type {import('../../types').TokenState}
|
|
125
|
+
*/
|
|
126
|
+
const state = useReactive({
|
|
127
|
+
checking: false,
|
|
128
|
+
loading: false,
|
|
129
|
+
token: '',
|
|
130
|
+
url: '',
|
|
131
|
+
store: null,
|
|
132
|
+
status: 'created',
|
|
133
|
+
error: '',
|
|
134
|
+
checkCount: 0,
|
|
135
|
+
mfaCode: 0,
|
|
136
|
+
appInfo: null,
|
|
137
|
+
memberAppInfo: null,
|
|
138
|
+
connectedDid: '',
|
|
139
|
+
saveConnect: false,
|
|
140
|
+
inExistingSession: false,
|
|
141
|
+
nextWorkflow: '',
|
|
142
|
+
baseUrl,
|
|
143
|
+
prefix: `${prefix}/${action}`,
|
|
144
|
+
extraParams: omitBy(extraParams, isNil),
|
|
145
|
+
checkFn,
|
|
146
|
+
results: {},
|
|
147
|
+
action,
|
|
148
|
+
provider,
|
|
149
|
+
reset() {
|
|
150
|
+
this.error = '';
|
|
151
|
+
this.status = 'created';
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const statusExtraParams = useRef(null);
|
|
156
|
+
|
|
157
|
+
const translation = translations[locale] || translations.en;
|
|
158
|
+
const existingSession = useCreation(() => parseExistingSession(), []);
|
|
159
|
+
const maxCheckCount = Math.ceil(checkTimeout / checkInterval);
|
|
160
|
+
|
|
161
|
+
const currentState = useReactive({
|
|
162
|
+
onSuccessCalled: false,
|
|
163
|
+
cancelWhenScannedCounter: 0,
|
|
164
|
+
isSocketAvailable: false,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const { encryptKey, decrypt } = useSecurity();
|
|
168
|
+
|
|
169
|
+
// scanned 状态下 cancel (Back 按钮) 操作的计数器, 每次 cancel 操作时 +1
|
|
170
|
+
// - 外部可以传入 autoConnect 表示是否启用自动连接
|
|
171
|
+
// - 计数器默认为 0, 表示还未进行过 cancel 操作
|
|
172
|
+
// - 如果计数器 > 0, 说明用户进行过 cancel 操作
|
|
173
|
+
// (临时禁用, 重新打开 did-connect 窗口时恢复)
|
|
174
|
+
const browser = useBrowser();
|
|
175
|
+
const visitorId = getVisitorId();
|
|
176
|
+
const socket = useRef(null);
|
|
177
|
+
const subscription = useRef(null);
|
|
178
|
+
|
|
179
|
+
const params = {
|
|
180
|
+
...state.extraParams,
|
|
181
|
+
locale,
|
|
182
|
+
[tokenKey]: state.token || existingSession?.token,
|
|
183
|
+
provider: state.provider,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const unsubscribe = async () => {
|
|
187
|
+
// HACK: 如果 url 中已经存在 session,则不允许注销当前的 session(需要利用当前 session 作为 sourceToken 去创建一个新的 session)
|
|
188
|
+
if (existingSession?.token && existingSession?.token === state.token) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
if (params[tokenKey] && state.status === 'created') {
|
|
193
|
+
await state.checkFn(`${state.prefix}/timeout?${stringifyQuery(params)}`, {
|
|
194
|
+
headers: getExtraHeaders(state.baseUrl),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// Do nothing
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (state.token && subscription.current) {
|
|
203
|
+
await socket.current.unsubscribe(getRelayChannel(state.token));
|
|
204
|
+
subscription.current = null;
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
// Do nothing
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// HACK: 为了保证 createToken 函数永远都是最新的,需要使用 useMemoizedFn 来包裹
|
|
212
|
+
const createToken = useMemoizedFn((...args) => {
|
|
213
|
+
const innerParams = {
|
|
214
|
+
[encKey]: encryptKey,
|
|
215
|
+
...state.extraParams,
|
|
216
|
+
locale,
|
|
217
|
+
forceConnected,
|
|
218
|
+
// - autoConnect 请求参数用于控制服务端是否给钱包应用发送自动连接通知
|
|
219
|
+
// - 使用 connect 组件时明确传入了 autoConnect = false, 则 autoConnect 请求参数 为 false
|
|
220
|
+
// - 如果 cancelWhenScannedCounter > 0, 说明用户进行过 cancel 操作, 则临时禁用自动连接, autoConnect 请求参数 为 false
|
|
221
|
+
// (避免 "无限自动连接问题")
|
|
222
|
+
// - 如果上次连接设备 (connected_wallet_os) 不是 ios/android, 则禁用自动连接
|
|
223
|
+
autoConnect:
|
|
224
|
+
autoConnect &&
|
|
225
|
+
// 如果是 wallet webview 环境, 不发送通知, 避免 wallet 连续弹出 auth 窗口 2 次 (#341)
|
|
226
|
+
!(browser.wallet || browser.arcSphere) &&
|
|
227
|
+
!currentState.cancelWhenScannedCounter &&
|
|
228
|
+
['ios', 'android'].includes(Cookie.get('connected_wallet_os')),
|
|
229
|
+
provider: state.provider,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (visitorId) {
|
|
233
|
+
// NOTICE: 这个 visitorId 必须保留,才能在三方的 did connect 流程中正确传递
|
|
234
|
+
innerParams.visitorId = visitorId;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const mergeInData = omit(statusExtraParams.current || {}, ['sourceAppPid', 'verbose']);
|
|
238
|
+
Object.keys(mergeInData).forEach((x) => {
|
|
239
|
+
if (!innerParams[x] || (x === encKey && mergeInData[x])) {
|
|
240
|
+
innerParams[x] = mergeInData[x];
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
if (existingSession?.token && !innerParams.sourceToken) {
|
|
244
|
+
innerParams.sourceToken = existingSession?.token;
|
|
245
|
+
}
|
|
246
|
+
const fn = createTokenFn({
|
|
247
|
+
action,
|
|
248
|
+
prefix: state.prefix,
|
|
249
|
+
baseUrl: state.baseUrl,
|
|
250
|
+
checkFn: state.checkFn,
|
|
251
|
+
extraParams: innerParams,
|
|
252
|
+
});
|
|
253
|
+
return fn(...args);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// 切换账户或者 token 过期之后需要重新生成
|
|
257
|
+
const generate = useMemoizedFn(async (cleanup = true) => {
|
|
258
|
+
if (cleanup) {
|
|
259
|
+
unsubscribe();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
state.loading = true;
|
|
264
|
+
state.token = '';
|
|
265
|
+
state.url = '';
|
|
266
|
+
state.store = null;
|
|
267
|
+
state.extraParams = omitBy(extraParams, isNil);
|
|
268
|
+
// HACK: 更改了 createToken 中依赖的参数,需要强制等待 createToken 函数更新后,再调用 createToken 函数
|
|
269
|
+
await sleep();
|
|
270
|
+
|
|
271
|
+
const data = await createToken();
|
|
272
|
+
const extra = data.extra || {};
|
|
273
|
+
|
|
274
|
+
state.loading = false;
|
|
275
|
+
state.token = data.token;
|
|
276
|
+
state.url = data.url;
|
|
277
|
+
state.status = 'created';
|
|
278
|
+
state.error = '';
|
|
279
|
+
state.checkCount = 0;
|
|
280
|
+
state.appInfo = data.appInfo;
|
|
281
|
+
state.memberAppInfo = data.memberAppInfo;
|
|
282
|
+
state.connectedDid = extra.connectedDid;
|
|
283
|
+
state.saveConnect = extra.saveConnect;
|
|
284
|
+
} catch (err) {
|
|
285
|
+
state.loading = false;
|
|
286
|
+
state.status = 'error';
|
|
287
|
+
state.error = `${translation.generateError}: ${err.message}`;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// 每次 cancel 操作时计数器 +1 => 重新生成 token
|
|
292
|
+
const cancelWhenScanned = useMemoizedFn(() => {
|
|
293
|
+
currentState.cancelWhenScannedCounter++;
|
|
294
|
+
});
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
// FIXME: @zhanghan 在有 nw 的情况下,需要考虑如何进行 regenrate
|
|
297
|
+
// 计数器 > 0, 说明人为触发了 cancel, 重新生成 token,但是只在没有 nextWorkflow 的情况下重新生成
|
|
298
|
+
if (currentState.cancelWhenScannedCounter > 0) {
|
|
299
|
+
if (extraParams.nw) {
|
|
300
|
+
onError(new Error(translation.retryForbidden));
|
|
301
|
+
} else {
|
|
302
|
+
generate();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
306
|
+
}, [currentState.cancelWhenScannedCounter]);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
const isSafari = /^((?!chrome|android).)*safari/i.test(globalThis.navigator.userAgent);
|
|
310
|
+
const disableSocket = isSafari || Object.hasOwn(globalThis?.blocklet || {}, 'DID_CONNECT_DISABLE_SOCKET');
|
|
311
|
+
// FIXME: @zhanghan websocket 默认支持跨域,这里为何要检查域名是同源
|
|
312
|
+
if (!disableSocket && useSocket && isSameOrigin(state.baseUrl) && getAppId()) {
|
|
313
|
+
const needReconnect =
|
|
314
|
+
!socket.current || socket.current.isConnected() === false || socket.current.baseUrl !== state.baseUrl;
|
|
315
|
+
if (needReconnect) {
|
|
316
|
+
socket.current = new WsClient(
|
|
317
|
+
`${getRelayProtocol()}//${getSocketHost(state.baseUrl)}${getAppPrefix()}${RELAY_SOCKET_PREFIX}/relay`,
|
|
318
|
+
{
|
|
319
|
+
longpollerTimeout: 5000, // connection timeout
|
|
320
|
+
heartbeatIntervalMs: 30 * 1000,
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
socket.current.baseUrl = state.baseUrl;
|
|
324
|
+
socket.current.onOpen(() => {
|
|
325
|
+
currentState.isSocketAvailable = true;
|
|
326
|
+
});
|
|
327
|
+
socket.current.connect();
|
|
328
|
+
}
|
|
329
|
+
} else if (currentState.isSocketAvailable) {
|
|
330
|
+
currentState.isSocketAvailable = false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return () => {
|
|
334
|
+
if (socket.current) {
|
|
335
|
+
socket.current.disconnect();
|
|
336
|
+
socket.current = null;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}, [state.baseUrl]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
340
|
+
|
|
341
|
+
const getCheckInterval = () => {
|
|
342
|
+
if (!state.token) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
if (state.loading) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
if (isSessionEnded(state.status)) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return checkInterval;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// 任何导致 Connect 组件 unmounted 的操作 (比如 dialog 关闭) => 调用 timeout api 清除 token
|
|
356
|
+
// 关闭时如果 token 状态处于 created,那么应该通知后端给删掉
|
|
357
|
+
// 说明: 此处需要借助 useRef 在 useEffect return function 中访问 state
|
|
358
|
+
const closeSessionRef = useRef(null);
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
closeSessionRef.current = { state, params };
|
|
361
|
+
});
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
return () => {
|
|
364
|
+
// eslint-disable-next-line no-shadow
|
|
365
|
+
const { state, params } = closeSessionRef.current;
|
|
366
|
+
if (state.status === 'created') {
|
|
367
|
+
if (params[tokenKey]) {
|
|
368
|
+
state.checkFn(`${state.prefix}/timeout?${stringifyQuery(params)}`, {
|
|
369
|
+
headers: getExtraHeaders(state.baseUrl),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
375
|
+
}, []);
|
|
376
|
+
|
|
377
|
+
// Check auth token status
|
|
378
|
+
// FIXME: @zhanghan 这个函数会造成 connect 组件每秒重渲染一次,需要从 setState 入手,优化这个问题。优化的原则是,页面数据没有变更时,不应该触发重渲染
|
|
379
|
+
const checkStatus = useMemoizedFn(async (force = false) => {
|
|
380
|
+
if ((state.checking || document.hidden) && !force) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// NOTICE: 仅当状态为 created 时才去计算次数统计
|
|
385
|
+
if (state.status === 'created') {
|
|
386
|
+
state.checkCount++;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (currentState.isSocketAvailable && !force) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
state.checking = true;
|
|
395
|
+
const res = await state.checkFn(`${state.prefix}/status?${stringifyQuery(params)}`, {
|
|
396
|
+
headers: getExtraHeaders(state.baseUrl),
|
|
397
|
+
});
|
|
398
|
+
const { status, error: newError, mfaCode = 0 } = res.data;
|
|
399
|
+
|
|
400
|
+
state.store = res.data;
|
|
401
|
+
state.mfaCode = mfaCode;
|
|
402
|
+
state.checking = false;
|
|
403
|
+
if (state.status === 'scanned' && status === 'created') {
|
|
404
|
+
// HACK: 不把 scanned 状态切换回 created
|
|
405
|
+
} else {
|
|
406
|
+
state.status = status;
|
|
407
|
+
}
|
|
408
|
+
if (status === 'error' && newError) {
|
|
409
|
+
const err = new Error(newError);
|
|
410
|
+
err.response = res;
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
return res.data;
|
|
414
|
+
} catch (err) {
|
|
415
|
+
const { response } = err;
|
|
416
|
+
if (response?.status) {
|
|
417
|
+
if (params[tokenKey]) {
|
|
418
|
+
state.checkFn(`${state.prefix}/timeout?${stringifyQuery(params)}`, {
|
|
419
|
+
headers: getExtraHeaders(state.baseUrl),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const _msg = response?.data?.error ? response.data.error : err.message;
|
|
423
|
+
const _err = new Error(_msg);
|
|
424
|
+
_err.code = response.status;
|
|
425
|
+
state.status = 'error';
|
|
426
|
+
state.checking = false;
|
|
427
|
+
state.error = _msg;
|
|
428
|
+
onError(_err, state?.store, decrypt);
|
|
429
|
+
} else {
|
|
430
|
+
state.status = 'error';
|
|
431
|
+
state.checking = false;
|
|
432
|
+
state.error = translation.generateError;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const applyExistingToken = async () => {
|
|
439
|
+
try {
|
|
440
|
+
if (existingSession.error) {
|
|
441
|
+
throw existingSession.error;
|
|
442
|
+
}
|
|
443
|
+
state.loading = true;
|
|
444
|
+
state.token = existingSession.token;
|
|
445
|
+
state.url = existingSession.url;
|
|
446
|
+
state.inExistingSession = true;
|
|
447
|
+
let data = await checkStatus();
|
|
448
|
+
|
|
449
|
+
// 1. 如果 existingSession 当前状态已经是 scanned,则刷新页面后,应该根据 existingSession.token 生成一个新的 session,继续完成后续的操作
|
|
450
|
+
// 2. HACK: 如果当前 existingSession 已经产生过新的 session,则目前已经无法通过 existingSession 本身来监听变化了,需要再创建一个新的 session 才能完成监听状态
|
|
451
|
+
if (['scanned'].includes(data?.status) || data?.sourceToken === existingSession?.token) {
|
|
452
|
+
await generate(false);
|
|
453
|
+
data = await checkStatus();
|
|
454
|
+
}
|
|
455
|
+
statusExtraParams.current = data?.extraParams || null;
|
|
456
|
+
// existingSession.token 合法的初始状态必须是 created
|
|
457
|
+
if (data?.status && !['created'].includes(data?.status)) {
|
|
458
|
+
throw new Error(`${translation.invalidSessionStatus} [${data.status}]`);
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {
|
|
461
|
+
state.status = 'error';
|
|
462
|
+
state.error = e.message;
|
|
463
|
+
onError(e, state?.store, decrypt);
|
|
464
|
+
} finally {
|
|
465
|
+
state.loading = false;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
if (!state.token && !state.store && !state.loading && !state.error) {
|
|
472
|
+
// connect to existing session if any
|
|
473
|
+
if (existingSession) {
|
|
474
|
+
applyExistingToken();
|
|
475
|
+
// Create our first token if we do not have one
|
|
476
|
+
} else {
|
|
477
|
+
generate(false);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Mark token as expired if exceed max retry count
|
|
483
|
+
if (state.status !== 'timeout' && state.checkCount > maxCheckCount) {
|
|
484
|
+
state.status = 'timeout';
|
|
485
|
+
unsubscribe();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Trigger on success if we completed the process
|
|
490
|
+
if (state.status === 'succeed' && !currentState.onSuccessCalled) {
|
|
491
|
+
// save connected_did to cookie if only on same origin
|
|
492
|
+
const connectedInfo = getConnectedInfo({
|
|
493
|
+
...state.store,
|
|
494
|
+
appInfo: state.appInfo,
|
|
495
|
+
memberAppInfo: state.memberAppInfo,
|
|
496
|
+
});
|
|
497
|
+
if (isSameOrigin(state.baseUrl) && saveConnect && state.saveConnect && state.store.did) {
|
|
498
|
+
updateConnectedInfo(connectedInfo, true);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (state.store) {
|
|
502
|
+
const { nextWorkflow } = state.store;
|
|
503
|
+
// Switch to nextWorkflow if we have
|
|
504
|
+
if (nextWorkflow) {
|
|
505
|
+
try {
|
|
506
|
+
const parsed = parseNextWorkflow(nextWorkflow, tokenKey);
|
|
507
|
+
state.nextWorkflow = parsed?.nextWorkflow || nextWorkflow;
|
|
508
|
+
Object.assign(state, parsed);
|
|
509
|
+
state.store = null;
|
|
510
|
+
state.error = '';
|
|
511
|
+
state.url = '';
|
|
512
|
+
state.appInfo = null;
|
|
513
|
+
state.memberAppInfo = null;
|
|
514
|
+
state.checkCount = 0;
|
|
515
|
+
state.status = 'scanned';
|
|
516
|
+
state.results = {
|
|
517
|
+
...state.results,
|
|
518
|
+
[state.token]: state.store,
|
|
519
|
+
};
|
|
520
|
+
state.checkFn = createAxios({ baseURL: parsed.baseUrl, timeout: 8000 }).get;
|
|
521
|
+
checkStatus();
|
|
522
|
+
} catch (err) {
|
|
523
|
+
console.error(`Invalid nextWorkflow: ${nextWorkflow}`, err);
|
|
524
|
+
state.status = 'error';
|
|
525
|
+
state.error = `Invalid nextWorkflow: ${nextWorkflow}: ${err.message}`;
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
if (state.nextWorkflow) {
|
|
529
|
+
state.nextWorkflow = '';
|
|
530
|
+
state.url = '';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// trigger success callback when last workflow complete
|
|
534
|
+
if (typeof onSuccess === 'function') {
|
|
535
|
+
const results = Object.values({ ...state.results, [state.token]: state.store });
|
|
536
|
+
currentState.onSuccessCalled = true;
|
|
537
|
+
const result = results.length > 1 ? results : results[0];
|
|
538
|
+
const clonedResult = cloneDeep(result);
|
|
539
|
+
onSuccess(clonedResult, decrypt, {
|
|
540
|
+
sourceAppPid: extraParams?.sourceAppPid,
|
|
541
|
+
...connectedInfo,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// FIXME: @zhanghan 待调查最近 ws 通道不会将 status 设置为 status,而直接进行到下一 nextWorkflow status 为 scanned 的情况
|
|
550
|
+
// 此处仅用于处理上述情况
|
|
551
|
+
if (state.store?.nextWorkflow) {
|
|
552
|
+
const { nextWorkflow } = state.store;
|
|
553
|
+
try {
|
|
554
|
+
const parsed = parseNextWorkflow(nextWorkflow, tokenKey);
|
|
555
|
+
state.nextWorkflow = parsed?.nextWorkflow || nextWorkflow;
|
|
556
|
+
Object.assign(state, parsed);
|
|
557
|
+
state.store = null;
|
|
558
|
+
state.error = '';
|
|
559
|
+
state.url = '';
|
|
560
|
+
state.appInfo = null;
|
|
561
|
+
state.memberAppInfo = null;
|
|
562
|
+
state.checkCount = 0;
|
|
563
|
+
state.status = 'scanned';
|
|
564
|
+
state.results = {
|
|
565
|
+
...state.results,
|
|
566
|
+
[state.token]: state.store,
|
|
567
|
+
};
|
|
568
|
+
state.checkFn = createAxios({ baseURL: parsed.baseUrl, timeout: 8000 }).get;
|
|
569
|
+
checkStatus();
|
|
570
|
+
} catch (err) {
|
|
571
|
+
console.error(`Invalid nextWorkflow: ${nextWorkflow}`, err);
|
|
572
|
+
state.status = 'error';
|
|
573
|
+
state.error = `Invalid nextWorkflow: ${nextWorkflow}: ${err.message}`;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
const subscriptionUpdatedAt = useRef(0);
|
|
578
|
+
|
|
579
|
+
// Try to use websocket
|
|
580
|
+
useEffect(() => {
|
|
581
|
+
if (state.token && currentState.isSocketAvailable && socket.current) {
|
|
582
|
+
let needSubscription = false;
|
|
583
|
+
if (subscription.current) {
|
|
584
|
+
if (subscription.current.token !== state.token) {
|
|
585
|
+
socket.current.unsubscribe(getRelayChannel(subscription.current.token));
|
|
586
|
+
needSubscription = true;
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
needSubscription = true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (needSubscription) {
|
|
593
|
+
subscription.current = socket.current.subscribe(getRelayChannel(state.token));
|
|
594
|
+
// 每次重新监听时,需要重置 subscriptionUpdatedAt
|
|
595
|
+
subscriptionUpdatedAt.current = 0;
|
|
596
|
+
subscription.current.token = state.token;
|
|
597
|
+
subscription.current.on('updated', ({ response }) => {
|
|
598
|
+
const updatedAt = +new Date(response.updatedAt);
|
|
599
|
+
if (updatedAt <= subscriptionUpdatedAt.current) {
|
|
600
|
+
console.warn('Ignore outdated message', response);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
subscriptionUpdatedAt.current = updatedAt;
|
|
605
|
+
let { status, error } = response;
|
|
606
|
+
|
|
607
|
+
if (status === 'forbidden') {
|
|
608
|
+
error = translation.forbidden;
|
|
609
|
+
status = 'error';
|
|
610
|
+
}
|
|
611
|
+
state.status = status;
|
|
612
|
+
state.mfaCode = response.mfaCode || 0;
|
|
613
|
+
state.store = response;
|
|
614
|
+
state.error = error;
|
|
615
|
+
// ws 通道发现的错误,也需要 onError 回调
|
|
616
|
+
if (error) {
|
|
617
|
+
onError(new Error(error), state?.store, decrypt);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}, [state.token, currentState.isSocketAvailable, socket.current]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
623
|
+
|
|
624
|
+
useInterval(checkStatus, getCheckInterval());
|
|
625
|
+
|
|
626
|
+
// Restore session status when page lost focus and get focus again
|
|
627
|
+
usePageShow(() => {
|
|
628
|
+
// HACK: 由于 safari 的特性,在页面不可见时,无法正常发起请求,所以需要在页面可见时强制发送一次请求来更新状态
|
|
629
|
+
if (state.token && isSessionEnded(state.status) === false) {
|
|
630
|
+
state.checking = false;
|
|
631
|
+
// HACK: 不能在页面可见时立刻发起请求,仍然可能会被 safari 拦截,需要等一段时间
|
|
632
|
+
setTimeout(() => {
|
|
633
|
+
checkStatus(true);
|
|
634
|
+
}, 100);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
return { state, generate, cancelWhenScanned };
|
|
639
|
+
}
|