mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Changes in ReCaptcha behavior (#2829)
* migrate to v2 API URLs for CSV exports * don't ask user to solve recaptcha for first response; pass token in header as well * hide "Try again" button if no bypass allowed * fix tests
This commit is contained in:
@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_PORT=3000
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
|
||||
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" }
|
||||
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
|
||||
|
||||
@@ -125,6 +125,28 @@ export const GENERAL_API_ADDRESS_RESOURCES = {
|
||||
path: '/api/v2/proxy/3dparty/xname/addresses/:hash',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
|
||||
// CSV EXPORTS
|
||||
address_csv_export_txs: {
|
||||
path: '/api/v2/addresses/:hash/transactions/csv',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
address_csv_export_internal_txs: {
|
||||
path: '/api/v2/addresses/:hash/internal-transactions/csv',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
address_csv_export_token_transfers: {
|
||||
path: '/api/v2/addresses/:hash/token-transfers/csv',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
address_csv_export_logs: {
|
||||
path: '/api/v2/addresses/:hash/logs/csv',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
address_csv_export_celo_election_rewards: {
|
||||
path: '/api/v2/addresses/:hash/celo/election-rewards/csv',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
} satisfies Record<string, ApiResource>;
|
||||
|
||||
export type GeneralApiAddressResourceName = `general:${ keyof typeof GENERAL_API_ADDRESS_RESOURCES }`;
|
||||
|
||||
@@ -246,12 +246,6 @@ export const GENERAL_API_MISC_RESOURCES = {
|
||||
path: '/api/v2/config/celo',
|
||||
},
|
||||
|
||||
// CSV EXPORT
|
||||
csv_export_token_holders: {
|
||||
path: '/api/v2/tokens/:hash/holders/csv',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
|
||||
// OTHER
|
||||
api_v2_key: {
|
||||
path: '/api/v2/key',
|
||||
|
||||
@@ -49,6 +49,10 @@ export const GENERAL_API_TOKEN_RESOURCES = {
|
||||
filterFields: [ 'q' as const, 'chain_ids' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
token_csv_export_holders: {
|
||||
path: '/api/v2/tokens/:hash/holders/csv',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
|
||||
// TOKEN INSTANCE
|
||||
token_instance: {
|
||||
|
||||
@@ -2,21 +2,6 @@ import type { ApiResource } from '../../types';
|
||||
import type { BlockCountdownResponse } from 'types/api/block';
|
||||
|
||||
export const GENERAL_API_V1_RESOURCES = {
|
||||
csv_export_txs: {
|
||||
path: '/api/v1/transactions-csv',
|
||||
},
|
||||
csv_export_internal_txs: {
|
||||
path: '/api/v1/internal-transactions-csv',
|
||||
},
|
||||
csv_export_token_transfers: {
|
||||
path: '/api/v1/token-transfers-csv',
|
||||
},
|
||||
csv_export_logs: {
|
||||
path: '/api/v1/logs-csv',
|
||||
},
|
||||
csv_export_epoch_rewards: {
|
||||
path: '/api/v1/celo-election-rewards-csv',
|
||||
},
|
||||
graphql: {
|
||||
path: '/api/v1/graphql',
|
||||
},
|
||||
|
||||
@@ -54,6 +54,9 @@ export default function useFetch() {
|
||||
const error = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
rateLimits: {
|
||||
bypassOptions: response.headers.get('bypass-429-option'),
|
||||
},
|
||||
};
|
||||
|
||||
if (meta?.logError && rollbar) {
|
||||
@@ -67,18 +70,16 @@ export default function useFetch() {
|
||||
if (!isJson) {
|
||||
return response.text().then(
|
||||
(textError) => Promise.reject({
|
||||
...error,
|
||||
payload: textError,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json().then(
|
||||
(jsonError) => Promise.reject({
|
||||
...error,
|
||||
payload: jsonError as Error,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
}),
|
||||
() => {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -23,6 +23,8 @@ export default function fetchFactory(
|
||||
cookie,
|
||||
...pick(_req.headers, [
|
||||
'x-csrf-token',
|
||||
'recaptcha-v2-response',
|
||||
'user-agent',
|
||||
'Authorization', // the old value, just in case
|
||||
'authorization', // Node.js automatically lowercases headers
|
||||
// feature flags
|
||||
|
||||
+13
-3
@@ -21,14 +21,24 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
|
||||
);
|
||||
|
||||
// proxy some headers from API
|
||||
const requestId = apiRes.headers.get('x-request-id');
|
||||
requestId && nextRes.setHeader('x-request-id', requestId);
|
||||
const HEADERS_TO_PROXY = [
|
||||
'x-request-id',
|
||||
'content-type',
|
||||
'bypass-429-option',
|
||||
'x-ratelimit-limit',
|
||||
'x-ratelimit-remaining',
|
||||
'x-ratelimit-reset',
|
||||
];
|
||||
|
||||
HEADERS_TO_PROXY.forEach((header) => {
|
||||
const value = apiRes.headers.get(header);
|
||||
value && nextRes.setHeader(header, value);
|
||||
});
|
||||
|
||||
const setCookie = apiRes.headers.raw()['set-cookie'];
|
||||
setCookie?.forEach((value) => {
|
||||
nextRes.appendHeader('set-cookie', value);
|
||||
});
|
||||
nextRes.setHeader('content-type', apiRes.headers.get('content-type') || '');
|
||||
|
||||
nextRes.status(apiRes.status).send(apiRes.body);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,10 @@ const useReCaptcha = () => {
|
||||
return {
|
||||
ref: { current: null },
|
||||
executeAsync: () => Promise.resolve('recaptcha_token'),
|
||||
fetchProtectedResource: async(fetcher) => {
|
||||
const result = await fetcher();
|
||||
return result;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -20,29 +20,35 @@ const ExportCSV = ({ filters }: Props) => {
|
||||
const recaptcha = useReCaptcha();
|
||||
const [ isLoading, setIsLoading ] = React.useState(false);
|
||||
|
||||
const apiFetchFactory = React.useCallback(async(recaptchaToken?: string) => {
|
||||
const url = buildUrl('general:advanced_filter_csv', undefined, {
|
||||
...filters,
|
||||
recaptcha_response: recaptchaToken,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'content-type': 'application/octet-stream',
|
||||
...(recaptchaToken && { 'recaptcha-v2-response': recaptchaToken }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText, {
|
||||
cause: {
|
||||
status: response.status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}, [ filters ]);
|
||||
|
||||
const handleExportCSV = React.useCallback(async() => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const token = await recaptcha.executeAsync();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('ReCaptcha is not solved');
|
||||
}
|
||||
|
||||
const url = buildUrl('general:advanced_filter_csv', undefined, {
|
||||
...filters,
|
||||
recaptcha_response: token,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'content-type': 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error();
|
||||
}
|
||||
const response = await recaptcha.fetchProtectedResource(apiFetchFactory);
|
||||
|
||||
const blob = await response.blob();
|
||||
const fileName = `export-filtered-txs-${ dayjs().format('YYYY-MM-DD-HH-mm-ss') }.csv`;
|
||||
@@ -56,7 +62,7 @@ const ExportCSV = ({ filters }: Props) => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [ filters, recaptcha ]);
|
||||
}, [ apiFetchFactory, recaptcha ]);
|
||||
|
||||
if (!config.services.reCaptchaV2.siteKey) {
|
||||
return null;
|
||||
|
||||
@@ -39,33 +39,40 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
|
||||
const { handleSubmit, formState } = formApi;
|
||||
const recaptcha = useReCaptcha();
|
||||
|
||||
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
|
||||
try {
|
||||
const token = await recaptcha.executeAsync();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('ReCaptcha is not solved');
|
||||
}
|
||||
|
||||
const apiFetchFactory = React.useCallback((data: FormFields) => {
|
||||
return async(recaptchaToken?: string) => {
|
||||
const url = buildUrl(resource, { hash } as never, {
|
||||
address_id: hash,
|
||||
from_period: exportType !== 'holders' ? dayjs(data.from).toISOString() : null,
|
||||
to_period: exportType !== 'holders' ? dayjs(data.to).toISOString() : null,
|
||||
filter_type: filterType,
|
||||
filter_value: filterValue,
|
||||
recaptcha_response: token,
|
||||
recaptcha_response: recaptchaToken,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'content-type': 'application/octet-stream',
|
||||
...(recaptchaToken && { 'recaptcha-v2-response': recaptchaToken }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error();
|
||||
throw new Error(response.statusText, {
|
||||
cause: {
|
||||
status: response.status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
}, [ resource, hash, exportType, filterType, filterValue ]);
|
||||
|
||||
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
|
||||
try {
|
||||
const response = await recaptcha.fetchProtectedResource<Response>(apiFetchFactory(data));
|
||||
|
||||
const blob = await response.blob();
|
||||
const fileName = exportType === 'holders' ?
|
||||
`${ fileNameTemplate }_${ hash }.csv` :
|
||||
@@ -80,7 +87,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
|
||||
});
|
||||
}
|
||||
|
||||
}, [ recaptcha, resource, hash, exportType, filterType, filterValue, fileNameTemplate ]);
|
||||
}, [ recaptcha, apiFetchFactory, exportType, fileNameTemplate, hash, filterType, filterValue ]);
|
||||
|
||||
if (!config.services.reCaptchaV2.siteKey) {
|
||||
return (
|
||||
|
||||
@@ -46,19 +46,21 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const authFetchFactory = React.useCallback((email: string) => (recaptchaToken?: string) => {
|
||||
return apiFetch('general:auth_send_otp', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { email, recaptcha_response: recaptchaToken },
|
||||
headers: {
|
||||
...(recaptchaToken && { 'recaptcha-v2-response': recaptchaToken }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [ apiFetch ]);
|
||||
|
||||
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(formData) => {
|
||||
try {
|
||||
const token = await recaptcha.executeAsync();
|
||||
|
||||
await apiFetch('general:auth_send_otp', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: formData.email,
|
||||
recaptcha_response: token,
|
||||
},
|
||||
},
|
||||
});
|
||||
await recaptcha.fetchProtectedResource(authFetchFactory(formData.email));
|
||||
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
|
||||
Source: 'Profile',
|
||||
Status: 'OTP sent',
|
||||
@@ -72,7 +74,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
|
||||
description: apiError?.message || getErrorMessage(error) || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
}, [ apiFetch, authModal, recaptcha ]);
|
||||
}, [ authFetchFactory, authModal, recaptcha ]);
|
||||
|
||||
const hasDirtyFields = Object.keys(formApi.formState.dirtyFields).length > 0;
|
||||
|
||||
|
||||
@@ -29,39 +29,39 @@ interface ExportTypeEntity {
|
||||
const EXPORT_TYPES: Record<CsvExportParams['type'], ExportTypeEntity> = {
|
||||
transactions: {
|
||||
text: 'transactions',
|
||||
resource: 'general:csv_export_txs',
|
||||
resource: 'general:address_csv_export_txs',
|
||||
fileNameTemplate: 'transactions',
|
||||
filterType: 'address',
|
||||
filterValues: AddressFromToFilterValues,
|
||||
},
|
||||
'internal-transactions': {
|
||||
text: 'internal transactions',
|
||||
resource: 'general:csv_export_internal_txs',
|
||||
resource: 'general:address_csv_export_internal_txs',
|
||||
fileNameTemplate: 'internal_transactions',
|
||||
filterType: 'address',
|
||||
filterValues: AddressFromToFilterValues,
|
||||
},
|
||||
'token-transfers': {
|
||||
text: 'token transfers',
|
||||
resource: 'general:csv_export_token_transfers',
|
||||
resource: 'general:address_csv_export_token_transfers',
|
||||
fileNameTemplate: 'token_transfers',
|
||||
filterType: 'address',
|
||||
filterValues: AddressFromToFilterValues,
|
||||
},
|
||||
logs: {
|
||||
text: 'logs',
|
||||
resource: 'general:csv_export_logs',
|
||||
resource: 'general:address_csv_export_logs',
|
||||
fileNameTemplate: 'logs',
|
||||
filterType: 'topic',
|
||||
},
|
||||
holders: {
|
||||
text: 'holders',
|
||||
resource: 'general:csv_export_token_holders',
|
||||
resource: 'general:token_csv_export_holders',
|
||||
fileNameTemplate: 'holders',
|
||||
},
|
||||
'epoch-rewards': {
|
||||
text: 'epoch rewards',
|
||||
resource: 'general:csv_export_epoch_rewards',
|
||||
resource: 'general:address_csv_export_celo_election_rewards',
|
||||
fileNameTemplate: 'epoch_rewards',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import config from 'configs/app';
|
||||
import getErrorCause from 'lib/errors/getErrorCause';
|
||||
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
|
||||
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
|
||||
import getErrorProp from 'lib/errors/getErrorProp';
|
||||
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
@@ -77,7 +78,9 @@ const AppError = ({ error, className }: Props) => {
|
||||
|
||||
switch (statusCode) {
|
||||
case 429: {
|
||||
return <AppErrorTooManyRequests/>;
|
||||
const rateLimits = getErrorProp(error, 'rateLimits');
|
||||
const bypassOptions = typeof rateLimits === 'object' && rateLimits && 'bypassOptions' in rateLimits ? rateLimits.bypassOptions : undefined;
|
||||
return <AppErrorTooManyRequests bypassOptions={ typeof bypassOptions === 'string' ? bypassOptions : undefined }/>;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
@@ -12,18 +12,30 @@ import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
|
||||
import AppErrorIcon from '../AppErrorIcon';
|
||||
import AppErrorTitle from '../AppErrorTitle';
|
||||
|
||||
const AppErrorTooManyRequests = () => {
|
||||
interface Props {
|
||||
bypassOptions?: string;
|
||||
}
|
||||
|
||||
const AppErrorTooManyRequests = ({ bypassOptions }: Props) => {
|
||||
const fetch = useFetch();
|
||||
const recaptcha = useReCaptcha();
|
||||
|
||||
const handleSubmit = React.useCallback(async() => {
|
||||
try {
|
||||
const token = await recaptcha.executeAsync();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('ReCaptcha is not solved');
|
||||
}
|
||||
|
||||
const url = buildUrl('general:api_v2_key');
|
||||
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
body: { recaptcha_response: token },
|
||||
headers: {
|
||||
'recaptcha-v2-response': token,
|
||||
},
|
||||
credentials: 'include',
|
||||
}, {
|
||||
resource: 'general:api_v2_key',
|
||||
@@ -52,7 +64,7 @@ const AppErrorTooManyRequests = () => {
|
||||
You have exceeded the request rate for a given time period. Please reduce the number of requests and try again soon.
|
||||
</Text>
|
||||
<ReCaptcha { ...recaptcha }/>
|
||||
<Button onClick={ handleSubmit } disabled={ recaptcha.isInitError } mt={ 8 }>Try again</Button>
|
||||
{ bypassOptions !== 'no_bypass' && <Button onClick={ handleSubmit } disabled={ recaptcha.isInitError } mt={ 8 }>Try again</Button> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import type ReCAPTCHA from 'react-google-recaptcha';
|
||||
|
||||
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
|
||||
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
|
||||
|
||||
export default function useReCaptcha() {
|
||||
const ref = React.useRef<ReCAPTCHA>(null);
|
||||
const rejectCb = React.useRef<((error: Error) => void) | null>(null);
|
||||
@@ -40,5 +43,31 @@ export default function useReCaptcha() {
|
||||
};
|
||||
}, [ isOpen, handleContainerClick ]);
|
||||
|
||||
return React.useMemo(() => ({ ref, executeAsync, isInitError, onInitError: handleInitError }), [ ref, executeAsync, isInitError, handleInitError ]);
|
||||
const fetchProtectedResource: <T>(fetcher: (token?: string) => Promise<T>, token?: string) => Promise<T> = React.useCallback(async(fetcher, token) => {
|
||||
try {
|
||||
const result = await fetcher(token);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const statusCode = error instanceof Error ? getErrorCauseStatusCode(error) : getErrorObjStatusCode(error);
|
||||
if (statusCode === 429) {
|
||||
const token = await executeAsync();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('ReCaptcha is not solved');
|
||||
}
|
||||
|
||||
return fetchProtectedResource(fetcher, token);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}, [ executeAsync ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
ref,
|
||||
executeAsync,
|
||||
isInitError,
|
||||
onInitError: handleInitError,
|
||||
fetchProtectedResource,
|
||||
}), [ ref, executeAsync, isInitError, handleInitError, fetchProtectedResource ]);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source, logi
|
||||
onError: handleSignInError,
|
||||
source,
|
||||
isAuth,
|
||||
executeRecaptchaAsync: recaptcha.executeAsync,
|
||||
fetchProtectedResource: recaptcha.fetchProtectedResource,
|
||||
loginToRewards,
|
||||
});
|
||||
|
||||
|
||||
@@ -37,19 +37,22 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const sendCodeFetchFactory = React.useCallback((email: string) => (recaptchaToken?: string) => {
|
||||
return apiFetch('general:auth_send_otp', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { email, recaptcha_response: recaptchaToken },
|
||||
headers: {
|
||||
...(recaptchaToken && { 'recaptcha-v2-response': recaptchaToken }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [ apiFetch ]);
|
||||
|
||||
const onFormSubmit: SubmitHandler<EmailFormFields> = React.useCallback(async(formData) => {
|
||||
try {
|
||||
const token = await recaptcha.executeAsync();
|
||||
await recaptcha.fetchProtectedResource(sendCodeFetchFactory(formData.email));
|
||||
|
||||
await apiFetch('general:auth_send_otp', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: formData.email,
|
||||
recaptcha_response: token,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (isAuth) {
|
||||
mixpanelConfig?.account_link_info.source !== 'Profile' && mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
|
||||
Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown',
|
||||
@@ -69,7 +72,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
description: getErrorObjPayload<{ message: string }>(error)?.message || getErrorMessage(error) || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
}, [ recaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source ]);
|
||||
}, [ recaptcha, sendCodeFetchFactory, isAuth, onSubmit, mixpanelConfig?.account_link_info.source ]);
|
||||
|
||||
return (
|
||||
<FormProvider { ...formApi }>
|
||||
|
||||
@@ -68,17 +68,23 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
|
||||
});
|
||||
}, [ apiFetch, email, onSuccess, isAuth, formApi ]);
|
||||
|
||||
const resendCodeFetchFactory = React.useCallback((recaptchaToken?: string) => {
|
||||
return apiFetch('general:auth_send_otp', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { email, recaptcha_response: recaptchaToken },
|
||||
headers: {
|
||||
...(recaptchaToken && { 'recaptcha-v2-response': recaptchaToken }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [ apiFetch, email ]);
|
||||
|
||||
const handleResendCodeClick = React.useCallback(async() => {
|
||||
try {
|
||||
formApi.clearErrors('code');
|
||||
setIsCodeSending(true);
|
||||
const token = await recaptcha.executeAsync();
|
||||
await apiFetch('general:auth_send_otp', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { email, recaptcha_response: token },
|
||||
},
|
||||
});
|
||||
await recaptcha.fetchProtectedResource(resendCodeFetchFactory);
|
||||
|
||||
toaster.success({
|
||||
title: 'Success',
|
||||
@@ -94,7 +100,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
|
||||
} finally {
|
||||
setIsCodeSending(false);
|
||||
}
|
||||
}, [ apiFetch, email, formApi, recaptcha ]);
|
||||
}, [ formApi, recaptcha, resendCodeFetchFactory ]);
|
||||
|
||||
return (
|
||||
<FormProvider { ...formApi }>
|
||||
|
||||
@@ -41,10 +41,10 @@ interface Props {
|
||||
source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
|
||||
isAuth?: boolean;
|
||||
loginToRewards?: boolean;
|
||||
executeRecaptchaAsync: () => Promise<string | null>;
|
||||
fetchProtectedResource: <T>(fetcher: (token?: string) => Promise<T>, token?: string) => Promise<T>;
|
||||
}
|
||||
|
||||
function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, loginToRewards, executeRecaptchaAsync }: Props) {
|
||||
function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, loginToRewards, fetchProtectedResource }: Props) {
|
||||
const [ isPending, setIsPending ] = React.useState(false);
|
||||
const isConnectingWalletRef = React.useRef(false);
|
||||
|
||||
@@ -96,24 +96,26 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log
|
||||
}
|
||||
}, [ apiFetch, loginToRewards ]);
|
||||
|
||||
const authFetchFactory = React.useCallback((message: string, signature: string) => (recaptchaToken?: string) => {
|
||||
const authResource = isAuth ? 'general:auth_link_address' : 'general:auth_siwe_verify';
|
||||
return apiFetch<typeof authResource, UserInfo, unknown>(authResource, {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { message, signature, recaptcha_response: recaptchaToken },
|
||||
headers: {
|
||||
...(recaptchaToken && { 'recaptcha-v2-response': recaptchaToken }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [ apiFetch, isAuth ]);
|
||||
|
||||
const proceedToAuth = React.useCallback(async(address: string) => {
|
||||
try {
|
||||
await switchChainAsync({ chainId: Number(config.chain.id) });
|
||||
const siweMessage = await getSiweMessage(address);
|
||||
const signature = await signMessageAsync({ message: siweMessage.message });
|
||||
const recaptchaToken = await executeRecaptchaAsync();
|
||||
|
||||
if (!recaptchaToken) {
|
||||
throw new Error('ReCaptcha is not solved');
|
||||
}
|
||||
|
||||
const authResource = isAuth ? 'general:auth_link_address' : 'general:auth_siwe_verify';
|
||||
const authResponse = await apiFetch<typeof authResource, UserInfo, unknown>(authResource, {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { message: siweMessage.message, signature, recaptcha_response: recaptchaToken },
|
||||
},
|
||||
});
|
||||
const authResponse = await fetchProtectedResource(authFetchFactory(siweMessage.message, signature));
|
||||
|
||||
const rewardsLoginResponse = siweMessage.type === 'shared' ?
|
||||
await apiFetch('rewards:login', {
|
||||
@@ -143,7 +145,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
}, [ getSiweMessage, switchChainAsync, signMessageAsync, executeRecaptchaAsync, isAuth, apiFetch, onSuccess, onError ]);
|
||||
}, [ switchChainAsync, getSiweMessage, signMessageAsync, fetchProtectedResource, authFetchFactory, apiFetch, onSuccess, onError ]);
|
||||
|
||||
const start = React.useCallback(() => {
|
||||
setIsPending(true);
|
||||
|
||||
@@ -45,16 +45,22 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
});
|
||||
}, [ setStatus ]);
|
||||
|
||||
const initializeUpdate = React.useCallback(async(tokenProp?: string) => {
|
||||
try {
|
||||
const token = tokenProp || await recaptcha.executeAsync();
|
||||
await apiFetch<'general:token_instance_refresh_metadata', unknown, unknown>('general:token_instance_refresh_metadata', {
|
||||
pathParams: { hash, id },
|
||||
fetchParams: {
|
||||
method: 'PATCH',
|
||||
body: { recaptcha_response: token },
|
||||
const apiFetchFactory = React.useCallback(async(recaptchaToken?: string) => {
|
||||
return apiFetch<'general:token_instance_refresh_metadata', unknown, unknown>('general:token_instance_refresh_metadata', {
|
||||
pathParams: { hash, id },
|
||||
fetchParams: {
|
||||
method: 'PATCH',
|
||||
body: { recaptcha_response: recaptchaToken },
|
||||
headers: {
|
||||
...(recaptchaToken && { 'recaptcha-v2-response': recaptchaToken }),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [ apiFetch, hash, id ]);
|
||||
|
||||
const initializeUpdate = React.useCallback(async() => {
|
||||
try {
|
||||
await recaptcha.fetchProtectedResource(apiFetchFactory);
|
||||
setStatus?.('WAITING_FOR_RESPONSE');
|
||||
toaster.loading({
|
||||
id: TOAST_ID,
|
||||
@@ -72,7 +78,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
setStatus?.('ERROR');
|
||||
}
|
||||
|
||||
}, [ apiFetch, handleRefreshError, hash, id, recaptcha, setStatus ]);
|
||||
}, [ apiFetchFactory, handleRefreshError, recaptcha, setStatus ]);
|
||||
|
||||
const handleModalClose = React.useCallback(({ open }: { open: boolean }) => {
|
||||
if (!open) {
|
||||
|
||||
Reference in New Issue
Block a user