@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
package/src/utils.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import SealedBox from 'tweetnacl-sealedbox-js';
|
|
3
|
+
import { getCookieOptions, setVisitorId, getVisitorId } from '@arcblock/ux/lib/Util';
|
|
4
|
+
import Cookie from 'js-cookie';
|
|
5
|
+
import isNil from 'lodash/isNil';
|
|
6
|
+
import isString from 'lodash/isString';
|
|
7
|
+
import isUndefined from 'lodash/isUndefined';
|
|
8
|
+
import { BlockletSDK, createAxios as _createAxios } from '@blocklet/js-sdk';
|
|
9
|
+
import { withQuery } from 'ufo';
|
|
10
|
+
import { isFromPublicKey, isValid as isValidDid } from '@arcblock/did';
|
|
11
|
+
import { getSafeUrl } from '@arcblock/ux/lib/Util/security';
|
|
12
|
+
import { createDebug, createLogger } from '@arcblock/ux/lib/Util/logger';
|
|
13
|
+
|
|
14
|
+
import pkg from '../package.json';
|
|
15
|
+
import { DID_CONNECT_URL_PARAMS_NAME, DEFAULT_WINDOW_TIMEOUT } from './constant';
|
|
16
|
+
import { NotOpenError } from './error';
|
|
17
|
+
|
|
18
|
+
const { version } = pkg;
|
|
19
|
+
|
|
20
|
+
export const debug = createDebug('did-connect');
|
|
21
|
+
export const logger = createLogger('did-connect');
|
|
22
|
+
|
|
23
|
+
export * from '@arcblock/ux/lib/Util/wallet';
|
|
24
|
+
|
|
25
|
+
export { setVisitorId, getVisitorId, version };
|
|
26
|
+
|
|
27
|
+
// https://github.com/joaquimserafim/base64-url/blob/54d9c9ede66a8724f280cf24fd18c38b9a53915f/index.js#L10
|
|
28
|
+
function unescape(str) {
|
|
29
|
+
return (str + '==='.slice((str.length + 3) % 4)).replace(/-/g, '+').replace(/_/g, '/');
|
|
30
|
+
}
|
|
31
|
+
function escape(str) {
|
|
32
|
+
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 如果需要 did-connect 加入一个已经存在的 session, 可以通过 url 传递一个 "__connect_url__" 查询参数, did-connect 会解析参数值中的 token 和 connect url 并加入该 session
|
|
36
|
+
// "__connect_url__" 需要编码/解码才可以正常使用
|
|
37
|
+
// 该方法只针对 browser 环境, nodejs 环境可以使用 escape(Buffer.from(url).toString('base64')) 进行编码
|
|
38
|
+
export const encodeConnectUrl = (url) => {
|
|
39
|
+
return escape(window.btoa(url));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const decodeConnectUrl = (encoded) => {
|
|
43
|
+
return window.atob(unescape(encoded));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// connectUrl 对应 create token 响应数据中的 url 字段值
|
|
47
|
+
export const parseTokenFromConnectUrl = (connectUrl) => {
|
|
48
|
+
const connectUrlObj = new URL(connectUrl);
|
|
49
|
+
const url = decodeURIComponent(connectUrlObj.searchParams.get('url'));
|
|
50
|
+
const token = new URL(url).searchParams.get('_t_');
|
|
51
|
+
return token;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 获取 appInfo 的 appId
|
|
56
|
+
* @param {{agentDid: string; publisher: string}} appInfo
|
|
57
|
+
* @returns {string} appId
|
|
58
|
+
*/
|
|
59
|
+
export const getAppId = (appInfo, memberAppInfo) => {
|
|
60
|
+
// 优先读取 agentDid,在 federated 登录模式下,当前应用会被存在 agentDid 中,而 publisher 中存储的是 federated master 的 appPid
|
|
61
|
+
let publisher = memberAppInfo?.publisher;
|
|
62
|
+
if (!publisher) {
|
|
63
|
+
// @deprecated 兼容原来的 agentDid 字段,新版中,将使用 memberAppInfo 来代替
|
|
64
|
+
if (appInfo?.agentDid) {
|
|
65
|
+
return appInfo.agentDid;
|
|
66
|
+
}
|
|
67
|
+
publisher = appInfo?.publisher;
|
|
68
|
+
}
|
|
69
|
+
if (publisher) {
|
|
70
|
+
return publisher.replace('did:abt:', '');
|
|
71
|
+
}
|
|
72
|
+
return globalThis.blocklet?.appPid || globalThis.blocklet?.appId || globalThis.env?.appId || '';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const getConnectedInfo = (data) => {
|
|
76
|
+
const result = {
|
|
77
|
+
connected_did: data.did || '',
|
|
78
|
+
connected_pk: data.pk || '',
|
|
79
|
+
connected_app: getAppId(data.appInfo, data.memberAppInfo),
|
|
80
|
+
};
|
|
81
|
+
if (data?.connectedWallet?.os) {
|
|
82
|
+
result.connected_wallet_os = data.connectedWallet.os;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
*
|
|
89
|
+
* @param {object} connectedInfo 服务端返回的 did-connect 连接信息,可通过格式化解析后成为可以写入 cookie 的值(也可以直接传入解析后的值,但第二个参数要求填写为 true)
|
|
90
|
+
* @param {boolean} parsed connectedInfo 是否被格式化解析过
|
|
91
|
+
* @returns
|
|
92
|
+
*/
|
|
93
|
+
export const updateConnectedInfo = (connectedInfo, parsed = false) => {
|
|
94
|
+
if (!connectedInfo) return;
|
|
95
|
+
// 如果输入的是原始数据,需要通过 getConnectedInfo 处理一下(兼容之前的调用方式)
|
|
96
|
+
if (!parsed) {
|
|
97
|
+
// eslint-disable-next-line no-param-reassign
|
|
98
|
+
connectedInfo = getConnectedInfo(connectedInfo);
|
|
99
|
+
}
|
|
100
|
+
const cookieOptions = getCookieOptions({
|
|
101
|
+
expireInDays: 7,
|
|
102
|
+
returnDomain: false,
|
|
103
|
+
});
|
|
104
|
+
// connected_did and connected_pk are used to skip authPrincipal
|
|
105
|
+
if (!isNil(connectedInfo.connected_did) && !isNil(connectedInfo.connected_pk)) {
|
|
106
|
+
// NOTICE: 需要严格校验 connected_did 和 connected_pk 是否合法
|
|
107
|
+
if (
|
|
108
|
+
isValidDid(connectedInfo.connected_did) &&
|
|
109
|
+
isFromPublicKey(connectedInfo.connected_did, connectedInfo.connected_pk)
|
|
110
|
+
) {
|
|
111
|
+
Cookie.set('connected_did', connectedInfo.connected_did, cookieOptions);
|
|
112
|
+
Cookie.set('connected_pk', connectedInfo.connected_pk, cookieOptions);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// connected_app is used to check session validity
|
|
116
|
+
if (!isNil(connectedInfo.connected_app) && isValidDid(connectedInfo.connected_app))
|
|
117
|
+
Cookie.set('connected_app', connectedInfo.connected_app, cookieOptions);
|
|
118
|
+
if (connectedInfo.connected_wallet_os) {
|
|
119
|
+
Cookie.set('connected_wallet_os', connectedInfo.connected_wallet_os, cookieOptions);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const encodeKey = (key) => escape(Buffer.from(key).toString('base64'));
|
|
124
|
+
export const decodeKey = (str) => Uint8Array.from(Buffer.from(unescape(str), 'base64'));
|
|
125
|
+
|
|
126
|
+
export const decrypt = (value, encryptKey, decryptKey) => {
|
|
127
|
+
const decrypted = SealedBox.open(
|
|
128
|
+
Uint8Array.from(Buffer.from(value, 'base64')),
|
|
129
|
+
decodeKey(encryptKey),
|
|
130
|
+
decodeKey(decryptKey)
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return JSON.parse(Buffer.from(decrypted).toString('utf8'));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const encrypt = (value, encryptKey) => {
|
|
137
|
+
return Buffer.from(SealedBox.seal(Uint8Array.from(Buffer.from(value)), decodeKey(encryptKey))).toString('base64');
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const parseNextWorkflow = (url, tokenKey = '_t_') => {
|
|
141
|
+
const tmp = new URL(url);
|
|
142
|
+
const nw = {
|
|
143
|
+
baseUrl: tmp.origin,
|
|
144
|
+
prefix: tmp.pathname.replace(/\/auth$/, ''),
|
|
145
|
+
token: tmp.searchParams.get(tokenKey) || '',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (!nw.baseUrl) {
|
|
149
|
+
throw new Error('Invalid next workflow: origin empty');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!nw.token) {
|
|
153
|
+
throw new Error('Invalid next workflow: token not found');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return nw;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const getApiErrorMessage = (err, defaultMessage) => {
|
|
160
|
+
let errorMessage = err.message;
|
|
161
|
+
const { response } = err;
|
|
162
|
+
if (response) {
|
|
163
|
+
errorMessage = response.data?.error || response.data?.message || response.data || defaultMessage;
|
|
164
|
+
errorMessage = typeof errorMessage === 'object' ? defaultMessage : errorMessage;
|
|
165
|
+
}
|
|
166
|
+
return errorMessage;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle WebAuthn specific errors with user-friendly messages
|
|
171
|
+
* @param {Error} err The error object from WebAuthn operations
|
|
172
|
+
* @param {string} defaultMessage Default fallback message
|
|
173
|
+
* @param {Function} t Translation function
|
|
174
|
+
* @returns {string} User-friendly error message
|
|
175
|
+
*/
|
|
176
|
+
export const getWebAuthnErrorMessage = (err, defaultMessage, t) => {
|
|
177
|
+
if (!err) {
|
|
178
|
+
return err;
|
|
179
|
+
}
|
|
180
|
+
// If it's not a WebAuthn specific error, use the general API error handler
|
|
181
|
+
if (!err.name) {
|
|
182
|
+
return getApiErrorMessage(err, defaultMessage);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle specific WebAuthn error types
|
|
186
|
+
switch (err.name) {
|
|
187
|
+
case 'NotAllowedError':
|
|
188
|
+
// User canceled the operation or it timed out
|
|
189
|
+
return t ? t('webauthn.error.canceled') : 'Authentication was canceled';
|
|
190
|
+
case 'SecurityError':
|
|
191
|
+
return t ? t('webauthn.error.security') : 'A security error occurred during authentication';
|
|
192
|
+
case 'NotSupportedError':
|
|
193
|
+
return t ? t('webauthn.error.notSupported') : 'This browser does not support passkey authentication';
|
|
194
|
+
case 'AbortError':
|
|
195
|
+
return t ? t('webauthn.error.aborted') : 'Authentication was aborted';
|
|
196
|
+
default:
|
|
197
|
+
// For unknown error types, log the error name for debugging
|
|
198
|
+
console.warn('Unhandled WebAuthn error type:', err.name);
|
|
199
|
+
return getApiErrorMessage(err, defaultMessage);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const createAxios = (options = {}, { lazy = false, lazyTime = 300 } = {}) => {
|
|
204
|
+
const mergedHeaders = {
|
|
205
|
+
...options.headers,
|
|
206
|
+
'x-did-connect-version': version,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const instance = _createAxios(
|
|
210
|
+
{
|
|
211
|
+
...options,
|
|
212
|
+
headers: mergedHeaders,
|
|
213
|
+
},
|
|
214
|
+
{ lazy, lazyTime }
|
|
215
|
+
);
|
|
216
|
+
instance.interceptors.request.use((config) => {
|
|
217
|
+
const visitorId = getVisitorId();
|
|
218
|
+
// FIXME: @zhanghan 需要在 js-sdk 改为动态获取 visitorId 后,删除该逻辑
|
|
219
|
+
if (visitorId) config.headers['x-blocklet-visitor-id'] = visitorId;
|
|
220
|
+
return config;
|
|
221
|
+
});
|
|
222
|
+
return instance;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const sleep = (time = 0) => {
|
|
226
|
+
return new Promise((resolve) => {
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
resolve();
|
|
229
|
+
}, time);
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export function getBrowserLang() {
|
|
234
|
+
if (typeof window === 'undefined') {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const lang =
|
|
239
|
+
(window.navigator.languages && window.navigator.languages[0]) ||
|
|
240
|
+
window.navigator.language ||
|
|
241
|
+
window.navigator.browserLanguage ||
|
|
242
|
+
window.navigator.userLanguage ||
|
|
243
|
+
window.navigator.systemLanguage ||
|
|
244
|
+
null;
|
|
245
|
+
|
|
246
|
+
const langMap = {
|
|
247
|
+
'zh-CN': 'zh',
|
|
248
|
+
'zh-TW': 'zh',
|
|
249
|
+
zh: 'zh',
|
|
250
|
+
en: 'en',
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return langMap[lang] || 'en';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function formatCacheTtl(ttl, defaultVal) {
|
|
257
|
+
if ([undefined, null, ''].includes(ttl)) {
|
|
258
|
+
return defaultVal;
|
|
259
|
+
}
|
|
260
|
+
return ttl / 86400;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @returns {import('./types').DIDConnectUrlOptions}
|
|
265
|
+
*/
|
|
266
|
+
export async function decodeUrlParams() {
|
|
267
|
+
const currentUrl = window.location.href;
|
|
268
|
+
try {
|
|
269
|
+
const urlInstance = new URL(currentUrl);
|
|
270
|
+
const encodedParams = urlInstance.searchParams.get(DID_CONNECT_URL_PARAMS_NAME);
|
|
271
|
+
if (encodedParams) {
|
|
272
|
+
urlInstance.searchParams.delete(DID_CONNECT_URL_PARAMS_NAME);
|
|
273
|
+
const params = JSON.parse(Buffer.from(encodedParams, 'base64').toString('utf-8'));
|
|
274
|
+
if (isString(params.forceConnected) && isUndefined(params.sourceAppPid)) {
|
|
275
|
+
const sdk = new BlockletSDK();
|
|
276
|
+
const userInfo = await sdk.user.getUserPublicInfo({ did: params.forceConnected });
|
|
277
|
+
if (!isUndefined(userInfo?.sourceAppPid)) {
|
|
278
|
+
params.sourceAppPid = userInfo.sourceAppPid;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
params,
|
|
283
|
+
url: urlInstance.href,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
console.warn('Failed to decode did-connect url params');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
params: null,
|
|
292
|
+
url: currentUrl,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
*
|
|
298
|
+
* @param {string} url - 打开一个弹窗
|
|
299
|
+
* @returns
|
|
300
|
+
*/
|
|
301
|
+
export const openPopup = (
|
|
302
|
+
_url,
|
|
303
|
+
{ width = 680, height = 720, name = 'did-connect:popup', offsetX = 0, offsetY = 0 } = {}
|
|
304
|
+
) => {
|
|
305
|
+
// 需要处理为安全的 url
|
|
306
|
+
const url = getSafeUrl(_url, { allowDomains: null });
|
|
307
|
+
|
|
308
|
+
const left = window.screenX + (window.innerWidth - width) / 2 + offsetX;
|
|
309
|
+
const top = window.screenY + (window.innerHeight - height) / 2 + offsetY;
|
|
310
|
+
|
|
311
|
+
const windowFeatures = [
|
|
312
|
+
`left=${left}`,
|
|
313
|
+
`top=${top}`,
|
|
314
|
+
`width=${width}`,
|
|
315
|
+
`height=${height}`,
|
|
316
|
+
'resizable=no', // not working
|
|
317
|
+
'scrollbars=yes',
|
|
318
|
+
'status=yes',
|
|
319
|
+
'popup=yes',
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
// HACK: IOS 必须以这种形式打开,才能被识别为 dialog 模式
|
|
323
|
+
const popup = window.open('', name, windowFeatures.join(','));
|
|
324
|
+
if (popup === null) {
|
|
325
|
+
throw new NotOpenError();
|
|
326
|
+
}
|
|
327
|
+
popup.location.href = withQuery(url, {
|
|
328
|
+
// NOTICE: 携带当前页面的 origin,用于在 popup 中通过该参数判断是否可以发送 postMessage,即使该参数被伪造,最终也只有该域名能接收到消息,所以没有关系
|
|
329
|
+
opener: window.location.origin,
|
|
330
|
+
});
|
|
331
|
+
return popup;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @typedef {Object} PopupConfig
|
|
336
|
+
* @property {number} [timeoutInSeconds] - 等待弹窗响应的超时时间
|
|
337
|
+
* @property {number} [closeTimeout] - 弹窗关闭前的等待时间
|
|
338
|
+
* @property {any} [popup] - 弹窗的实例
|
|
339
|
+
*/
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
*
|
|
343
|
+
* @param {PopupConfig} config
|
|
344
|
+
* @returns
|
|
345
|
+
*/
|
|
346
|
+
export const runPopup = (config) => {
|
|
347
|
+
return new Promise((resolve, reject) => {
|
|
348
|
+
let popupTimer;
|
|
349
|
+
let timeoutId;
|
|
350
|
+
const popupEventListener = ({ data }) => {
|
|
351
|
+
if (!data) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (data.type !== 'authorization_response') {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (data?.error) {
|
|
360
|
+
// HACK: 捕获到报错时,如果弹窗未关闭,则不应该 reject,否则调用 runPopup 的地方拿不到成功后的值
|
|
361
|
+
console.error(data.error);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
setTimeout(() => {
|
|
366
|
+
config.popup?.close();
|
|
367
|
+
// 弹窗关闭后才取消事件监听
|
|
368
|
+
clearTimeout(timeoutId);
|
|
369
|
+
clearInterval(popupTimer);
|
|
370
|
+
window.removeEventListener('message', popupEventListener, false);
|
|
371
|
+
|
|
372
|
+
resolve(data);
|
|
373
|
+
}, config.closeTimeout || 0);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
timeoutId = setTimeout(
|
|
377
|
+
() => {
|
|
378
|
+
clearInterval(popupTimer);
|
|
379
|
+
reject(new Error('Timeout'));
|
|
380
|
+
window.removeEventListener('message', popupEventListener, false);
|
|
381
|
+
},
|
|
382
|
+
(config.timeoutInSeconds || DEFAULT_WINDOW_TIMEOUT) * 1000
|
|
383
|
+
);
|
|
384
|
+
popupTimer = setInterval(() => {
|
|
385
|
+
if (config.popup?.closed) {
|
|
386
|
+
clearInterval(popupTimer);
|
|
387
|
+
clearTimeout(timeoutId);
|
|
388
|
+
window.removeEventListener('message', popupEventListener, false);
|
|
389
|
+
reject(new Error('Popup closed'));
|
|
390
|
+
}
|
|
391
|
+
}, 1000);
|
|
392
|
+
|
|
393
|
+
window.addEventListener('message', popupEventListener);
|
|
394
|
+
});
|
|
395
|
+
};
|
package/vite.config.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import svgr from 'vite-plugin-svgr';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import noBundlePlugin from 'vite-plugin-no-bundle';
|
|
5
|
+
import fg from 'fast-glob';
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [
|
|
9
|
+
react({ jsxRuntime: 'automatic' }),
|
|
10
|
+
svgr({
|
|
11
|
+
include: ['**/*.svg', '**/*.svg?react'],
|
|
12
|
+
}),
|
|
13
|
+
noBundlePlugin({
|
|
14
|
+
root: 'src',
|
|
15
|
+
copy: ['**/*.png', '**/*.gif', '**/*.jpg', '**/*.jpeg', '**/*.d.ts'],
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
build: {
|
|
19
|
+
lib: {
|
|
20
|
+
entry: fg.sync('src/**/*.{tsx,ts,jsx,js}', {
|
|
21
|
+
ignore: ['**/stories/**', '**/demo/**', '**/*.d.ts', '**/*.stories.*'],
|
|
22
|
+
}),
|
|
23
|
+
formats: ['es'],
|
|
24
|
+
fileName: (format, entryName) => `${entryName}.js`,
|
|
25
|
+
},
|
|
26
|
+
outDir: 'lib',
|
|
27
|
+
emptyOutDir: true,
|
|
28
|
+
},
|
|
29
|
+
});
|