@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,212 @@
|
|
|
1
|
+
import { useEffect, useRef, useImperativeHandle } from 'react';
|
|
2
|
+
import trim from 'lodash/trim';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import Cookie from 'js-cookie';
|
|
5
|
+
import { CircularProgress } from '@mui/material';
|
|
6
|
+
import { Icon } from '@iconify/react';
|
|
7
|
+
import { getCookieOptions } from '@arcblock/ux/lib/Util';
|
|
8
|
+
import { useMemoizedFn } from 'ahooks';
|
|
9
|
+
import passKeyRoundedIcon from '@iconify-icons/material-symbols/passkey-rounded';
|
|
10
|
+
import personAddRoundedIcon from '@iconify-icons/material-symbols/person-add-rounded';
|
|
11
|
+
import noop from 'lodash/noop';
|
|
12
|
+
import { mergeSx } from '@arcblock/ux/lib/Util/style';
|
|
13
|
+
|
|
14
|
+
import { usePasskey } from './context';
|
|
15
|
+
import { getWebAuthnErrorMessage, logger } from '../utils';
|
|
16
|
+
import LoginMethodItem from '../Connect/components/login-item/login-method-item';
|
|
17
|
+
import PasskeyDialog from './dialog';
|
|
18
|
+
import { VERIFY_CODE_LENGTH } from './constants';
|
|
19
|
+
|
|
20
|
+
function PasskeyAction({ action, onClick, title, state, dense = false, icon, ...rest }) {
|
|
21
|
+
return (
|
|
22
|
+
<LoginMethodItem
|
|
23
|
+
{...rest}
|
|
24
|
+
icon={
|
|
25
|
+
state[action] ? (
|
|
26
|
+
<CircularProgress
|
|
27
|
+
size="1em"
|
|
28
|
+
sx={{
|
|
29
|
+
color: 'inherit',
|
|
30
|
+
'& svg': {
|
|
31
|
+
transform: 'scale(0.75)',
|
|
32
|
+
},
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
) : (
|
|
36
|
+
icon
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
iconScale={1}
|
|
40
|
+
title={title}
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
sx={mergeSx(dense ? { p: 1, backgroundColor: 'transparent' } : {}, rest?.sx)}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
PasskeyAction.propTypes = {
|
|
48
|
+
action: PropTypes.string.isRequired,
|
|
49
|
+
onClick: PropTypes.func.isRequired,
|
|
50
|
+
title: PropTypes.string.isRequired,
|
|
51
|
+
state: PropTypes.object.isRequired,
|
|
52
|
+
dense: PropTypes.bool,
|
|
53
|
+
icon: PropTypes.any.isRequired,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default function PasskeyActions({
|
|
57
|
+
ref = null,
|
|
58
|
+
action,
|
|
59
|
+
behavior = 'none',
|
|
60
|
+
onSuccess,
|
|
61
|
+
onError,
|
|
62
|
+
extraParams = {},
|
|
63
|
+
createButtonText = '',
|
|
64
|
+
createMode = 'register',
|
|
65
|
+
dense = false,
|
|
66
|
+
slotProps = {},
|
|
67
|
+
sx = {},
|
|
68
|
+
mode = 'normal',
|
|
69
|
+
onClick = noop,
|
|
70
|
+
}) {
|
|
71
|
+
const currentAction = useRef('');
|
|
72
|
+
const passkeyDialogRef = useRef(null);
|
|
73
|
+
const { t, loginPasskey, logoutPasskey, passkeyState } = usePasskey();
|
|
74
|
+
|
|
75
|
+
const handleVerifyPasskey = useMemoizedFn(async (credentialId = '') => {
|
|
76
|
+
try {
|
|
77
|
+
passkeyState.verifying = true;
|
|
78
|
+
passkeyState.error = '';
|
|
79
|
+
if (!credentialId) {
|
|
80
|
+
passkeyState.verifyingStatus = '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await loginPasskey({
|
|
84
|
+
...extraParams,
|
|
85
|
+
action,
|
|
86
|
+
credentialId,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const cookieOptions = getCookieOptions({ expireInDays: 7 });
|
|
90
|
+
Cookie.remove('connected_did', cookieOptions);
|
|
91
|
+
Cookie.remove('connected_pk', cookieOptions);
|
|
92
|
+
Cookie.remove('connected_wallet_os', cookieOptions);
|
|
93
|
+
|
|
94
|
+
passkeyState.verifying = false;
|
|
95
|
+
if (result?.sessionToken) {
|
|
96
|
+
await onSuccess({ ...result, encrypted: false }, (val) => val);
|
|
97
|
+
passkeyState.verifyingStatus = 'succeed';
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
logger.error('Failed to verify passkey', err);
|
|
101
|
+
const errorMessage = getWebAuthnErrorMessage(err, t('verifyPasskeyFailed'), t);
|
|
102
|
+
passkeyState.verifying = false;
|
|
103
|
+
passkeyState.error = errorMessage;
|
|
104
|
+
passkeyState.verifyingStatus = 'error';
|
|
105
|
+
await logoutPasskey();
|
|
106
|
+
onError(new Error(errorMessage));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const handlePaste = useMemoizedFn(
|
|
111
|
+
(e) => {
|
|
112
|
+
if (!passkeyState.sent) return;
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
const pastedData = trim(e.clipboardData.getData('text/plain').slice(0, VERIFY_CODE_LENGTH));
|
|
115
|
+
if (!/^\d+$/.test(pastedData)) return;
|
|
116
|
+
passkeyState.code = pastedData.padEnd(VERIFY_CODE_LENGTH, ' ');
|
|
117
|
+
const nextInput = document.getElementById(`code-input-${pastedData.length}`);
|
|
118
|
+
if (nextInput) {
|
|
119
|
+
nextInput.focus();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
[passkeyState]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
document.addEventListener('paste', handlePaste);
|
|
127
|
+
return () => {
|
|
128
|
+
document.removeEventListener('paste', handlePaste);
|
|
129
|
+
};
|
|
130
|
+
}, [handlePaste]);
|
|
131
|
+
|
|
132
|
+
const handleConnect = useMemoizedFn(() => {
|
|
133
|
+
if (currentAction.current === 'verifying') {
|
|
134
|
+
handleVerifyPasskey();
|
|
135
|
+
} else if (currentAction.current === 'creating') {
|
|
136
|
+
passkeyDialogRef.current.open();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
useImperativeHandle(ref, () => ({
|
|
141
|
+
click: handleConnect,
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
if (behavior === 'none') {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
{['both', 'only-existing'].includes(behavior) ? (
|
|
151
|
+
<PasskeyAction
|
|
152
|
+
action="verifying"
|
|
153
|
+
state={passkeyState}
|
|
154
|
+
title={t('usePasskey')}
|
|
155
|
+
onClick={() => {
|
|
156
|
+
currentAction.current = 'verifying';
|
|
157
|
+
handleConnect();
|
|
158
|
+
onClick();
|
|
159
|
+
}}
|
|
160
|
+
dense={dense}
|
|
161
|
+
slotProps={slotProps}
|
|
162
|
+
sx={sx}
|
|
163
|
+
mode={mode}
|
|
164
|
+
icon={<Icon icon={passKeyRoundedIcon} fontSize="1em" />}
|
|
165
|
+
/>
|
|
166
|
+
) : null}
|
|
167
|
+
{['both', 'only-new'].includes(behavior) ? (
|
|
168
|
+
<>
|
|
169
|
+
<PasskeyAction
|
|
170
|
+
action="creating"
|
|
171
|
+
state={passkeyState}
|
|
172
|
+
title={createButtonText || t('createPasskey')}
|
|
173
|
+
onClick={() => {
|
|
174
|
+
currentAction.current = 'creating';
|
|
175
|
+
handleConnect();
|
|
176
|
+
onClick();
|
|
177
|
+
}}
|
|
178
|
+
dense={dense}
|
|
179
|
+
slotProps={slotProps}
|
|
180
|
+
sx={sx}
|
|
181
|
+
mode={mode}
|
|
182
|
+
icon={<Icon icon={personAddRoundedIcon} fontSize="1em" />}
|
|
183
|
+
/>
|
|
184
|
+
<PasskeyDialog
|
|
185
|
+
ref={passkeyDialogRef}
|
|
186
|
+
action={action}
|
|
187
|
+
createMode={createMode}
|
|
188
|
+
extraParams={extraParams}
|
|
189
|
+
onSuccess={onSuccess}
|
|
190
|
+
onError={onError}
|
|
191
|
+
/>
|
|
192
|
+
</>
|
|
193
|
+
) : null}
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
PasskeyActions.propTypes = {
|
|
199
|
+
action: PropTypes.string.isRequired,
|
|
200
|
+
onSuccess: PropTypes.func.isRequired,
|
|
201
|
+
onError: PropTypes.func.isRequired,
|
|
202
|
+
behavior: PropTypes.oneOf(['none', 'both', 'only-existing', 'only-new']),
|
|
203
|
+
createButtonText: PropTypes.string,
|
|
204
|
+
createMode: PropTypes.string,
|
|
205
|
+
extraParams: PropTypes.object,
|
|
206
|
+
dense: PropTypes.bool,
|
|
207
|
+
slotProps: PropTypes.object,
|
|
208
|
+
sx: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
|
209
|
+
mode: PropTypes.oneOf(['simple', 'normal']),
|
|
210
|
+
onClick: PropTypes.func,
|
|
211
|
+
ref: PropTypes.any,
|
|
212
|
+
};
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import PropTypes from 'prop-types';
|
|
2
|
+
import isUndefined from 'lodash/isUndefined';
|
|
3
|
+
import { createContext, use } from 'react';
|
|
4
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
+
import { useCreation, useMemoizedFn, useReactive } from 'ahooks';
|
|
6
|
+
import noop from 'lodash/noop';
|
|
7
|
+
import { joinURL } from 'ufo';
|
|
8
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
9
|
+
import { BLOCKLET_SERVICE_PATH_PREFIX } from '@arcblock/ux/lib/Util/constant';
|
|
10
|
+
import { getFederatedEnabled, getMaster, getBlockletData } from '@arcblock/ux/lib/Util/federated';
|
|
11
|
+
import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
|
|
12
|
+
|
|
13
|
+
import { getApiErrorMessage, getWebAuthnErrorMessage, createAxios, logger } from '../utils';
|
|
14
|
+
import { PassportSwitcher, parseResponse } from '../OAuth/passport-switcher';
|
|
15
|
+
|
|
16
|
+
const PasskeyContext = createContext({});
|
|
17
|
+
const { Provider, Consumer: PasskeyConsumer } = PasskeyContext;
|
|
18
|
+
|
|
19
|
+
const translations = {
|
|
20
|
+
zh: {
|
|
21
|
+
cancel: '取消',
|
|
22
|
+
usePasskey: '使用 Passkey',
|
|
23
|
+
createPasskey: '新建 Passkey',
|
|
24
|
+
creatingPasskey: '创建 Passkey...',
|
|
25
|
+
connectPasskey: '绑定 Passkey',
|
|
26
|
+
nonePasskey: '未配置 Passkey 方式',
|
|
27
|
+
connectPasskeySucceed: 'Passkey 绑定成功',
|
|
28
|
+
disconnectPasskeySucceed: 'Passkey 解绑成功',
|
|
29
|
+
verifyPasskeyFailed: 'Passkey 验证失败',
|
|
30
|
+
connectPasskeyFailed: 'Passkey 绑定失败',
|
|
31
|
+
disconnectPasskeyFailed: 'Passkey 解绑失败',
|
|
32
|
+
createPasskeyDesc1: 'Passkey 是一种密码替代品,使用指纹、面部识别、设备密码或 PIN 验证您的身份。',
|
|
33
|
+
createPasskeyDesc2: 'Passkey 可以用于登录、验证,作为您账户简单且安全的替代方案。',
|
|
34
|
+
emailPlaceholder: '请输入邮箱以作为备用',
|
|
35
|
+
verifyButton: '验证邮箱',
|
|
36
|
+
sendCodeButton: '获取验证码',
|
|
37
|
+
codeSentMessage: '我们已向 {email} 发送验证码,验证码将在 30 分钟内有效,请检查您的邮箱或垃圾邮件文件夹。',
|
|
38
|
+
codeSentSuccess: '验证码发送成功',
|
|
39
|
+
noPassports: '没有可更换的通行证',
|
|
40
|
+
cancelAuth: '取消授权',
|
|
41
|
+
emailInvalid: '邮箱格式不正确',
|
|
42
|
+
webauthn: {
|
|
43
|
+
error: {
|
|
44
|
+
canceled: '身份验证已被取消',
|
|
45
|
+
security: '身份验证过程中出现安全错误',
|
|
46
|
+
notSupported: '当前浏览器不支持 Passkey 身份验证',
|
|
47
|
+
aborted: '身份验证已被中止',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
en: {
|
|
52
|
+
cancel: 'Cancel',
|
|
53
|
+
usePasskey: 'Use Existing Passkey',
|
|
54
|
+
createPasskey: 'Create New Passkey',
|
|
55
|
+
creatingPasskey: 'Creating Passkey...',
|
|
56
|
+
connectPasskey: 'Connect Passkey',
|
|
57
|
+
nonePasskey: 'Passkey is not configured',
|
|
58
|
+
connectPasskeySucceed: 'Passkey add succeed',
|
|
59
|
+
disconnectPasskeySucceed: 'Passkey remove succeed',
|
|
60
|
+
verifyPasskeyFailed: 'Passkey verify failed',
|
|
61
|
+
connectPasskeyFailed: 'Passkey add failed',
|
|
62
|
+
disconnectPasskeyFailed: 'Passkey remove failed',
|
|
63
|
+
createPasskeyDesc1:
|
|
64
|
+
'Passkeys are a password replacement that validates your identity using touch, facial recognition, a device password, or a PIN.',
|
|
65
|
+
createPasskeyDesc2:
|
|
66
|
+
'Passkeys can be used for sign-in, verification as a simple and secure alternative to your account.',
|
|
67
|
+
emailPlaceholder: 'Enter your email address',
|
|
68
|
+
verifyButton: 'Verify Email',
|
|
69
|
+
sendCodeButton: 'Get Verify Code',
|
|
70
|
+
codeSentMessage:
|
|
71
|
+
'We just sent a verification code to {email}, the code will be valid for 30 minutes, please check your inbox or spam folder.',
|
|
72
|
+
codeSentSuccess: 'Verification code sent successfully',
|
|
73
|
+
noPassports: 'No passports to switch',
|
|
74
|
+
cancelAuth: 'Cancel authentication',
|
|
75
|
+
emailInvalid: 'Invalid email address',
|
|
76
|
+
webauthn: {
|
|
77
|
+
error: {
|
|
78
|
+
canceled: 'Authentication was canceled',
|
|
79
|
+
security: 'A security error occurred during authentication',
|
|
80
|
+
notSupported: 'This browser does not support passkey authentication',
|
|
81
|
+
aborted: 'Authentication was aborted',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function PasskeyProvider({
|
|
88
|
+
children,
|
|
89
|
+
locale = 'en',
|
|
90
|
+
onAddPasskey = noop,
|
|
91
|
+
onRemovePasskey = noop,
|
|
92
|
+
onSwitchPassport = noop,
|
|
93
|
+
}) {
|
|
94
|
+
const prefix = joinURL(window.env?.apiPrefix || BLOCKLET_SERVICE_PATH_PREFIX, '/api/passkey');
|
|
95
|
+
|
|
96
|
+
const state = useReactive({
|
|
97
|
+
baseUrl: '/',
|
|
98
|
+
session: undefined,
|
|
99
|
+
connecting: false,
|
|
100
|
+
disconnecting: false,
|
|
101
|
+
targetAppPid: undefined,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const switchState = useReactive({
|
|
105
|
+
open: false,
|
|
106
|
+
currentUser: null,
|
|
107
|
+
passports: [],
|
|
108
|
+
selectedPassport: undefined,
|
|
109
|
+
reset() {
|
|
110
|
+
switchState.open = false;
|
|
111
|
+
switchState.currentUser = null;
|
|
112
|
+
switchState.passports = [];
|
|
113
|
+
switchState.selectedPassport = undefined;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const passkeyState = useReactive({
|
|
118
|
+
user: null,
|
|
119
|
+
loading: false,
|
|
120
|
+
creating: false,
|
|
121
|
+
verifying: false,
|
|
122
|
+
creatingStatus: '',
|
|
123
|
+
verifyingStatus: '',
|
|
124
|
+
error: '',
|
|
125
|
+
email: '',
|
|
126
|
+
code: '',
|
|
127
|
+
verified: !(window?.blocklet?.settings?.kyc?.email || false),
|
|
128
|
+
sent: false,
|
|
129
|
+
openDialog: false,
|
|
130
|
+
get status() {
|
|
131
|
+
// eslint-disable-next-line react/no-this-in-sfc
|
|
132
|
+
if (this.creating || this.verifying) {
|
|
133
|
+
return 'scanned';
|
|
134
|
+
}
|
|
135
|
+
// eslint-disable-next-line react/no-this-in-sfc
|
|
136
|
+
if (this.creatingStatus === 'succeed' || this.verifyingStatus === 'succeed') {
|
|
137
|
+
return 'succeed';
|
|
138
|
+
}
|
|
139
|
+
// eslint-disable-next-line react/no-this-in-sfc
|
|
140
|
+
if (this.creatingStatus === 'error' || this.verifyingStatus === 'error') {
|
|
141
|
+
return 'error';
|
|
142
|
+
}
|
|
143
|
+
return '';
|
|
144
|
+
},
|
|
145
|
+
reset() {
|
|
146
|
+
passkeyState.creating = false;
|
|
147
|
+
passkeyState.verifying = false;
|
|
148
|
+
passkeyState.creatingStatus = '';
|
|
149
|
+
passkeyState.verifyingStatus = '';
|
|
150
|
+
passkeyState.error = '';
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
155
|
+
return translate(translations, key, locale, 'en', data);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const api = useCreation(() => {
|
|
159
|
+
return createAxios({ baseURL: joinURL(state.baseUrl, prefix), sessionTokenKey: '__sst', timeout: 60 * 1000 });
|
|
160
|
+
}, [state.baseUrl, prefix]);
|
|
161
|
+
|
|
162
|
+
const getBlocklet = useMemoizedFn(async () => {
|
|
163
|
+
if (state.baseUrl === '/') {
|
|
164
|
+
return window.blocklet;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const url = new URL(state.baseUrl);
|
|
168
|
+
if (url.host === window.location.host) {
|
|
169
|
+
return window.blocklet;
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line no-empty
|
|
172
|
+
} catch {}
|
|
173
|
+
|
|
174
|
+
const blocklet = await getBlockletData(state.baseUrl);
|
|
175
|
+
return blocklet;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const componentId = window?.blocklet?.componentId;
|
|
179
|
+
|
|
180
|
+
const setBaseUrl = (value) => {
|
|
181
|
+
state.baseUrl = value || '/';
|
|
182
|
+
};
|
|
183
|
+
const setTargetAppPid = (value) => {
|
|
184
|
+
state.targetAppPid = value;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const createPasskey = useMemoizedFn(async (params) => {
|
|
188
|
+
const { data: options } = await api.get('/register', { params });
|
|
189
|
+
console.warn('passkey.create.options', options);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// NOTE: should set useAutoRegister to false to make passkey work in safari
|
|
193
|
+
const response = await startRegistration({ optionsJSON: options, useAutoRegister: false });
|
|
194
|
+
console.warn('passkey.create.response', response);
|
|
195
|
+
|
|
196
|
+
const { data: result } = await api.post('/register', response, { params: { challenge: options.challenge } });
|
|
197
|
+
console.warn('passkey.create.result', result);
|
|
198
|
+
|
|
199
|
+
if (!result.verified) {
|
|
200
|
+
throw new Error(t('createPasskeyFailed'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
// Check if this is a WebAuthn specific error
|
|
206
|
+
if (err.name) {
|
|
207
|
+
throw new Error(getWebAuthnErrorMessage(err, t('createPasskeyFailed'), t));
|
|
208
|
+
}
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const verifyPasskey = useMemoizedFn(async (params) => {
|
|
214
|
+
const { data: options } = await api.get('/auth', { params });
|
|
215
|
+
console.warn('passkey.auth.options', options);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const response = await startAuthentication({ optionsJSON: options });
|
|
219
|
+
console.warn('passkey.auth.response', response);
|
|
220
|
+
|
|
221
|
+
// FIXME: @zhanghan 这里只是临时的方案,完整的方案需要优化后端的逻辑后,前端一起改动才行
|
|
222
|
+
const { data: result } = await api.post('/auth', response, {
|
|
223
|
+
params: { challenge: options.challenge, targetAppPid: state.targetAppPid },
|
|
224
|
+
});
|
|
225
|
+
console.warn('passkey.auth.result', result);
|
|
226
|
+
|
|
227
|
+
if (!result.verified) {
|
|
228
|
+
throw new Error(t('verifyPasskeyFailed'));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Check if this is a WebAuthn specific error
|
|
234
|
+
if (err.name) {
|
|
235
|
+
throw new Error(getWebAuthnErrorMessage(err, t('verifyPasskeyFailed'), t));
|
|
236
|
+
}
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// eslint-disable-next-line consistent-return
|
|
242
|
+
const connectPasskey = async (extraParams) => {
|
|
243
|
+
state.connecting = true;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const result = await createPasskey({
|
|
247
|
+
...extraParams,
|
|
248
|
+
locale,
|
|
249
|
+
componentId,
|
|
250
|
+
});
|
|
251
|
+
Toast.success(t('connectPasskeySucceed'));
|
|
252
|
+
onAddPasskey(result);
|
|
253
|
+
return result;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
logger.error('Failed to connect passkey', err);
|
|
256
|
+
Toast.error(getApiErrorMessage(err, t('connectPasskeyFailed')));
|
|
257
|
+
} finally {
|
|
258
|
+
state.connecting = false;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const disconnectPasskey = async ({ session, connectedAccount }) => {
|
|
263
|
+
state.session = session;
|
|
264
|
+
state.disconnecting = true;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = await verifyPasskey({
|
|
268
|
+
action: 'disconnect',
|
|
269
|
+
locale,
|
|
270
|
+
componentId,
|
|
271
|
+
sourceAppPid: session?.user?.sourceAppPid,
|
|
272
|
+
credentialId: connectedAccount.id,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
Toast.success(t('disconnectPasskeySucceed'));
|
|
276
|
+
onRemovePasskey(result);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
logger.error('Failed to disconnect passkey', err);
|
|
279
|
+
Toast.error(getApiErrorMessage(err, t('disconnectPasskeyFailed')));
|
|
280
|
+
} finally {
|
|
281
|
+
state.disconnecting = false;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const switchPassport = async (user = {}) => {
|
|
286
|
+
try {
|
|
287
|
+
const { data: passports } = await api.get('/passports');
|
|
288
|
+
if (passports.length) {
|
|
289
|
+
switchState.open = true;
|
|
290
|
+
switchState.currentUser = user;
|
|
291
|
+
switchState.passports = passports || [];
|
|
292
|
+
} else {
|
|
293
|
+
Toast.error(t('noPassports'));
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
Toast.error(err.message || t('getPassportFailed'));
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleSwitchPassport = useMemoizedFn((...args) => {
|
|
301
|
+
switchState.reset();
|
|
302
|
+
onSwitchPassport(...args);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const loginPasskey = useMemoizedFn(async ({ action = 'login', ...extraParams } = {}) => {
|
|
306
|
+
const backupBaseUrl = state.baseUrl;
|
|
307
|
+
if (extraParams?.sourceAppPid === window.blocklet?.appPid) {
|
|
308
|
+
state.baseUrl = window.blocklet?.appUrl || '/';
|
|
309
|
+
} else if (extraParams?.sourceAppPid) {
|
|
310
|
+
const blocklet = await getBlocklet();
|
|
311
|
+
const federatedEnabled = getFederatedEnabled(blocklet);
|
|
312
|
+
const master = getMaster(blocklet);
|
|
313
|
+
if (federatedEnabled && master?.appPid && extraParams?.sourceAppPid === master?.appPid) {
|
|
314
|
+
state.baseUrl = master.appUrl;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const params = {
|
|
320
|
+
...extraParams,
|
|
321
|
+
action,
|
|
322
|
+
locale,
|
|
323
|
+
componentId,
|
|
324
|
+
};
|
|
325
|
+
if (isUndefined(params.inviter) && window.localStorage.getItem('inviter')) {
|
|
326
|
+
params.inviter = window.localStorage.getItem('inviter');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = parseResponse(await verifyPasskey(params));
|
|
330
|
+
result.provider = 'passkey';
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
} catch (err) {
|
|
334
|
+
// Use the WebAuthn specific error handler if the error has a name property (WebAuthn errors do)
|
|
335
|
+
const errMsg = getWebAuthnErrorMessage(err, t('verifyPasskeyFailed'), t);
|
|
336
|
+
state.baseUrl = backupBaseUrl;
|
|
337
|
+
throw new Error(errMsg);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
const logoutPasskey = useMemoizedFn(() => {});
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<Provider
|
|
344
|
+
value={{
|
|
345
|
+
api,
|
|
346
|
+
locale,
|
|
347
|
+
connectPasskey,
|
|
348
|
+
disconnectPasskey,
|
|
349
|
+
createPasskey,
|
|
350
|
+
verifyPasskey,
|
|
351
|
+
loginPasskey,
|
|
352
|
+
logoutPasskey,
|
|
353
|
+
switchPassport,
|
|
354
|
+
baseUrl: state.baseUrl,
|
|
355
|
+
setBaseUrl,
|
|
356
|
+
setTargetAppPid,
|
|
357
|
+
passkeyState,
|
|
358
|
+
disconnecting: state.disconnecting,
|
|
359
|
+
connecting: state.connecting,
|
|
360
|
+
t,
|
|
361
|
+
}}>
|
|
362
|
+
{children}
|
|
363
|
+
<PassportSwitcher api={api} locale={locale} switchState={switchState} onSwitchPassport={handleSwitchPassport} />
|
|
364
|
+
</Provider>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function usePasskey() {
|
|
369
|
+
const context = use(PasskeyContext);
|
|
370
|
+
return context;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
PasskeyProvider.propTypes = {
|
|
374
|
+
children: PropTypes.node.isRequired,
|
|
375
|
+
locale: PropTypes.string,
|
|
376
|
+
onAddPasskey: PropTypes.func,
|
|
377
|
+
onRemovePasskey: PropTypes.func,
|
|
378
|
+
onSwitchPassport: PropTypes.func,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export { PasskeyContext, PasskeyConsumer, PasskeyProvider, usePasskey };
|