Commit edd30e9a authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merits: activity cards and tracking (#2662)

parent 8e52e6a5
...@@ -6,7 +6,7 @@ on: ...@@ -6,7 +6,7 @@ on:
envs_preset: envs_preset:
description: ENVs preset description: ENVs preset
required: false required: false
default: "" default: main
type: choice type: choice
options: options:
- none - none
...@@ -22,6 +22,7 @@ on: ...@@ -22,6 +22,7 @@ on:
- eth_goerli - eth_goerli
- filecoin - filecoin
- immutable - immutable
- main
- mekong - mekong
- neon_devnet - neon_devnet
- optimism - optimism
......
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.2929 9.87888C11.6834 10.2694 12.3166 10.2694 12.7071 9.87888L16.243 6.34299C16.6335 5.95252 17.2665 5.95252 17.657 6.34299C18.0475 6.73345 18.0475 7.36652 17.657 7.75699L14.1211 11.2929C13.7306 11.6834 13.7306 12.3166 14.1211 12.7071L17.657 16.243C18.0475 16.6335 18.0475 17.2665 17.657 17.657C17.2665 18.0475 16.6335 18.0475 16.243 17.657L12.7071 14.1211C12.3166 13.7306 11.6834 13.7306 11.2929 14.1211L7.75699 17.657C7.36652 18.0475 6.73345 18.0475 6.34299 17.657C5.95252 17.2665 5.95252 16.6335 6.34299 16.243L9.87888 12.7071C10.2694 12.3166 10.2694 11.6834 9.87888 11.2929L6.34299 7.75699C5.95252 7.36652 5.95252 6.73345 6.34299 6.34299C6.73345 5.95252 7.36652 5.95252 7.75699 6.34299L11.2929 9.87888Z" fill="currentColor"/> <path d="M11.293 9.879a1 1 0 0 0 1.414 0l3.536-3.536a1 1 0 1 1 1.414 1.414l-3.536 3.536a1 1 0 0 0 0 1.414l3.536 3.536a1 1 0 1 1-1.414 1.414l-3.536-3.536a1 1 0 0 0-1.414 0l-3.536 3.536a1 1 0 1 1-1.414-1.414l3.536-3.536a1 1 0 0 0 0-1.414L6.343 7.757a1 1 0 1 1 1.414-1.414l3.536 3.536Z" fill="currentColor"/>
</svg> </svg>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99984 18.3334C5.39734 18.3334 1.6665 14.6026 1.6665 10.0001C1.6665 5.39758 5.39734 1.66675 9.99984 1.66675C14.6023 1.66675 18.3332 5.39758 18.3332 10.0001C18.3332 14.6026 14.6023 18.3334 9.99984 18.3334ZM9.1665 9.16675V14.1667H10.8332V9.16675H9.1665ZM9.1665 5.83342V7.50008H10.8332V5.83342H9.1665Z" fill="currentColor"/> <path d="M10 18.333a8.333 8.333 0 1 1 0-16.666 8.333 8.333 0 1 1 0 16.666Zm-.834-9.166v5h1.667v-5H9.166Zm0-3.334V7.5h1.667V5.833H9.166Z" fill="currentColor"/>
</svg> </svg>
import type * as bens from '@blockscout/bens-types'; import type * as bens from '@blockscout/bens-types';
import type * as rewards from '@blockscout/points-types';
import type * as stats from '@blockscout/stats-types'; import type * as stats from '@blockscout/stats-types';
import type * as visualizer from '@blockscout/visualizer-types'; import type * as visualizer from '@blockscout/visualizer-types';
import { getFeaturePayload } from 'configs/app/features/types'; import { getFeaturePayload } from 'configs/app/features/types';
...@@ -99,17 +100,6 @@ import type { ...@@ -99,17 +100,6 @@ import type {
} from 'types/api/optimisticL2'; } from 'types/api/optimisticL2';
import type { Pool, PoolsResponse } from 'types/api/pools'; import type { Pool, PoolsResponse } from 'types/api/pools';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type {
RewardsConfigResponse,
RewardsCheckRefCodeResponse,
RewardsNonceResponse,
RewardsCheckUserResponse,
RewardsLoginResponse,
RewardsUserBalancesResponse,
RewardsUserDailyCheckResponse,
RewardsUserDailyClaimResponse,
RewardsUserReferralsResponse,
} from 'types/api/rewards';
import type { import type {
ScrollL2BatchesResponse, ScrollL2BatchesResponse,
ScrollL2TxnBatch, ScrollL2TxnBatch,
...@@ -444,6 +434,47 @@ export const RESOURCES = { ...@@ -444,6 +434,47 @@ export const RESOURCES = {
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint, endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath, basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
}, },
rewards_user_check_activity_pass: {
path: '/api/v1/activity/check-pass',
filterFields: [ 'address' as const ],
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_activity: {
path: '/api/v1/user/activity/rewards',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_activity_track_tx: {
path: '/api/v1/user/activity/track/transaction',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_activity_track_tx_confirm: {
path: '/api/v1/activity/track/transaction/confirm',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_activity_track_contract: {
path: '/api/v1/user/activity/track/contract',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_activity_track_contract_confirm: {
path: '/api/v1/activity/track/contract/confirm',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_user_activity_track_usage: {
path: '/api/v1/user/activity/track/usage',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_instances: {
path: '/api/v1/instances',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
// BLOCKS, TXS // BLOCKS, TXS
blocks: { blocks: {
...@@ -1446,15 +1477,20 @@ Q extends 'contract_mud_system_info' ? SmartContractMudSystemInfo : ...@@ -1446,15 +1477,20 @@ Q extends 'contract_mud_system_info' ? SmartContractMudSystemInfo :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse : Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'rewards_config' ? RewardsConfigResponse : Q extends 'rewards_config' ? rewards.GetConfigResponse :
Q extends 'rewards_check_ref_code' ? RewardsCheckRefCodeResponse : Q extends 'rewards_check_ref_code' ? rewards.AuthCodeResponse :
Q extends 'rewards_nonce' ? RewardsNonceResponse : Q extends 'rewards_nonce' ? rewards.AuthNonceResponse :
Q extends 'rewards_check_user' ? RewardsCheckUserResponse : Q extends 'rewards_check_user' ? rewards.AuthUserResponse :
Q extends 'rewards_login' ? RewardsLoginResponse : Q extends 'rewards_login' ? rewards.AuthLoginResponse :
Q extends 'rewards_user_balances' ? RewardsUserBalancesResponse : Q extends 'rewards_user_balances' ? rewards.GetUserBalancesResponse :
Q extends 'rewards_user_daily_check' ? RewardsUserDailyCheckResponse : Q extends 'rewards_user_daily_check' ? rewards.DailyRewardCheckResponse :
Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse : Q extends 'rewards_user_daily_claim' ? rewards.DailyRewardClaimResponse :
Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse : Q extends 'rewards_user_referrals' ? rewards.GetReferralDataResponse :
Q extends 'rewards_user_check_activity_pass' ? rewards.CheckActivityPassResponse :
Q extends 'rewards_user_activity' ? rewards.GetActivityRewardsResponse :
Q extends 'rewards_user_activity_track_tx' ? rewards.PreSubmitTransactionResponse :
Q extends 'rewards_user_activity_track_contract' ? rewards.PreVerifyContractResponse :
Q extends 'rewards_instances' ? rewards.GetInstancesResponse :
Q extends 'token_transfers_all' ? TokenTransferResponse : Q extends 'token_transfers_all' ? TokenTransferResponse :
Q extends 'address_xstar_score' ? AddressXStarResponse : Q extends 'address_xstar_score' ? AddressXStarResponse :
Q extends 'advanced_filter' ? AdvancedFilterResponse : Q extends 'advanced_filter' ? AdvancedFilterResponse :
......
...@@ -5,13 +5,7 @@ import { useRouter } from 'next/router'; ...@@ -5,13 +5,7 @@ import { useRouter } from 'next/router';
import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react';
import { useSignMessage, useSwitchChain } from 'wagmi'; import { useSignMessage, useSwitchChain } from 'wagmi';
import type { import type * as rewards from '@blockscout/points-types';
RewardsUserBalancesResponse, RewardsUserDailyCheckResponse,
RewardsNonceResponse, RewardsCheckUserResponse,
RewardsLoginResponse, RewardsCheckRefCodeResponse,
RewardsUserDailyClaimResponse, RewardsUserReferralsResponse,
RewardsConfigResponse,
} from 'types/api/rewards';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
...@@ -34,18 +28,18 @@ type ContextQueryResult<Response> = ...@@ -34,18 +28,18 @@ type ContextQueryResult<Response> =
Pick<UseQueryResult<Response, ResourceError<unknown>>, 'data' | 'isLoading' | 'refetch' | 'isPending' | 'isFetching' | 'isError'>; Pick<UseQueryResult<Response, ResourceError<unknown>>, 'data' | 'isLoading' | 'refetch' | 'isPending' | 'isFetching' | 'isError'>;
type TRewardsContext = { type TRewardsContext = {
balancesQuery: ContextQueryResult<RewardsUserBalancesResponse>; balancesQuery: ContextQueryResult<rewards.GetUserBalancesResponse>;
dailyRewardQuery: ContextQueryResult<RewardsUserDailyCheckResponse>; dailyRewardQuery: ContextQueryResult<rewards.DailyRewardCheckResponse>;
referralsQuery: ContextQueryResult<RewardsUserReferralsResponse>; referralsQuery: ContextQueryResult<rewards.GetReferralDataResponse>;
rewardsConfigQuery: ContextQueryResult<RewardsConfigResponse>; rewardsConfigQuery: ContextQueryResult<rewards.GetConfigResponse>;
checkUserQuery: ContextQueryResult<RewardsCheckUserResponse>; checkUserQuery: ContextQueryResult<rewards.AuthUserResponse>;
apiToken: string | undefined; apiToken: string | undefined;
isInitialized: boolean; isInitialized: boolean;
isLoginModalOpen: boolean; isLoginModalOpen: boolean;
openLoginModal: () => void; openLoginModal: () => void;
closeLoginModal: () => void; closeLoginModal: () => void;
saveApiToken: (token: string | undefined) => void; saveApiToken: (token: string | undefined) => void;
login: (refCode: string) => Promise<{ isNewUser: boolean; reward: string | null; invalidRefCodeError?: boolean }>; login: (refCode: string) => Promise<{ isNewUser: boolean; reward?: string; invalidRefCodeError?: boolean }>;
claim: () => Promise<void>; claim: () => Promise<void>;
}; };
...@@ -70,7 +64,7 @@ const initialState = { ...@@ -70,7 +64,7 @@ const initialState = {
openLoginModal: () => {}, openLoginModal: () => {},
closeLoginModal: () => {}, closeLoginModal: () => {},
saveApiToken: () => {}, saveApiToken: () => {},
login: async() => ({ isNewUser: false, reward: null }), login: async() => ({ isNewUser: false }),
claim: async() => {}, claim: async() => {},
}; };
...@@ -209,16 +203,15 @@ export function RewardsContextProvider({ children }: Props) { ...@@ -209,16 +203,15 @@ export function RewardsContextProvider({ children }: Props) {
throw new Error(); throw new Error();
} }
const [ nonceResponse, checkCodeResponse ] = await Promise.all([ const [ nonceResponse, checkCodeResponse ] = await Promise.all([
apiFetch('rewards_nonce') as Promise<RewardsNonceResponse>, apiFetch('rewards_nonce') as Promise<rewards.AuthNonceResponse>,
refCode ? refCode ?
apiFetch('rewards_check_ref_code', { pathParams: { code: refCode } }) as Promise<RewardsCheckRefCodeResponse> : apiFetch('rewards_check_ref_code', { pathParams: { code: refCode } }) as Promise<rewards.AuthCodeResponse> :
Promise.resolve({ valid: true, reward: null }), Promise.resolve({ valid: true, reward: undefined }),
]); ]);
if (!checkCodeResponse.valid) { if (!checkCodeResponse.valid) {
return { return {
invalidRefCodeError: true, invalidRefCodeError: true,
isNewUser: false, isNewUser: false,
reward: null,
}; };
} }
await switchChainAsync({ chainId: Number(config.chain.id) }); await switchChainAsync({ chainId: Number(config.chain.id) });
...@@ -233,7 +226,7 @@ export function RewardsContextProvider({ children }: Props) { ...@@ -233,7 +226,7 @@ export function RewardsContextProvider({ children }: Props) {
signature, signature,
}, },
}, },
}) as RewardsLoginResponse; }) as rewards.AuthLoginResponse;
saveApiToken(loginResponse.token); saveApiToken(loginResponse.token);
return { return {
isNewUser: loginResponse.created, isNewUser: loginResponse.created,
...@@ -253,7 +246,7 @@ export function RewardsContextProvider({ children }: Props) { ...@@ -253,7 +246,7 @@ export function RewardsContextProvider({ children }: Props) {
method: 'POST', method: 'POST',
...fetchParams, ...fetchParams,
}, },
}) as RewardsUserDailyClaimResponse; }) as rewards.DailyRewardClaimResponse;
} catch (_error) { } catch (_error) {
errorToast(_error); errorToast(_error);
throw _error; throw _error;
......
import { useCallback, useRef, useEffect } from 'react';
import type { PreSubmitTransactionResponse, PreVerifyContractResponse } from '@blockscout/points-types';
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import { MINUTE } from 'lib/consts';
import { useRewardsContext } from 'lib/contexts/rewards';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
const feature = config.features.rewards;
const LAST_EXPLORE_TIME_KEY = 'rewards_activity_last_explore_time';
type RewardsActivityEndpoint =
| 'rewards_user_activity_track_tx'
| 'rewards_user_activity_track_tx_confirm'
| 'rewards_user_activity_track_contract'
| 'rewards_user_activity_track_contract_confirm'
| 'rewards_user_activity_track_usage';
export default function useRewardsActivity() {
const { apiToken } = useRewardsContext();
const apiFetch = useApiFetch();
const lastExploreTime = useRef<number>(0);
const profileQuery = useProfileQuery();
const checkActivityPassQuery = useApiQuery('rewards_user_check_activity_pass', {
queryOptions: {
enabled: feature.isEnabled && Boolean(apiToken) && Boolean(profileQuery.data?.address_hash),
},
queryParams: {
address: profileQuery.data?.address_hash ?? '',
},
});
useEffect(() => {
try {
const storedTime = window.localStorage.getItem(LAST_EXPLORE_TIME_KEY);
if (storedTime) {
lastExploreTime.current = Number(storedTime);
}
} catch {}
}, []);
const makeRequest = useCallback(async(endpoint: RewardsActivityEndpoint, params: Record<string, string>) => {
if (!apiToken || !checkActivityPassQuery.data?.is_valid) {
return;
}
try {
return await apiFetch(endpoint, {
fetchParams: {
method: 'POST',
body: params,
headers: { Authorization: `Bearer ${ apiToken }` },
},
});
} catch {}
}, [ apiFetch, checkActivityPassQuery.data, apiToken ]);
const trackTransaction = useCallback(async(from: string, to: string) => {
return (
await makeRequest('rewards_user_activity_track_tx', {
from_address: from,
to_address: to,
chain_id: config.chain.id ?? '',
})
) as PreSubmitTransactionResponse | undefined;
}, [ makeRequest ]);
const trackTransactionConfirm = useCallback((hash: string, token: string) =>
makeRequest('rewards_user_activity_track_tx_confirm', { tx_hash: hash, token }),
[ makeRequest ],
);
const trackContract = useCallback(async(address: string) => {
return (
await makeRequest('rewards_user_activity_track_contract', {
address,
chain_id: config.chain.id ?? '',
})
) as PreVerifyContractResponse | undefined;
}, [ makeRequest ]);
const trackContractConfirm = useCallback((token: string) =>
makeRequest('rewards_user_activity_track_contract_confirm', { token }),
[ makeRequest ],
);
const trackUsage = useCallback((action: string) => {
// check here because this function is called on page load
if (!apiToken || !checkActivityPassQuery.data?.is_valid) {
return;
}
if (action === 'explore') {
const now = Date.now();
if (now - lastExploreTime.current < 5 * MINUTE) {
return;
}
lastExploreTime.current = now;
try {
window.localStorage.setItem(LAST_EXPLORE_TIME_KEY, String(now));
} catch {}
}
return makeRequest('rewards_user_activity_track_usage', {
action,
chain_id: config.chain.id ?? '',
});
}, [ makeRequest, apiToken, checkActivityPassQuery.data ]);
return {
trackTransaction,
trackTransactionConfirm,
trackContract,
trackContractConfirm,
trackUsage,
};
}
...@@ -3,6 +3,8 @@ import type { AddEthereumChainParameter } from 'viem'; ...@@ -3,6 +3,8 @@ import type { AddEthereumChainParameter } from 'viem';
import config from 'configs/app'; import config from 'configs/app';
import { SECOND } from '../consts';
import useRewardsActivity from '../hooks/useRewardsActivity';
import useProvider from './useProvider'; import useProvider from './useProvider';
import { getHexadecimalChainId } from './utils'; import { getHexadecimalChainId } from './utils';
...@@ -26,15 +28,23 @@ function getParams(): AddEthereumChainParameter { ...@@ -26,15 +28,23 @@ function getParams(): AddEthereumChainParameter {
export default function useAddChain() { export default function useAddChain() {
const { wallet, provider } = useProvider(); const { wallet, provider } = useProvider();
const { trackUsage } = useRewardsActivity();
return React.useCallback(() => { return React.useCallback(async() => {
if (!wallet || !provider) { if (!wallet || !provider) {
throw new Error('Wallet or provider not found'); throw new Error('Wallet or provider not found');
} }
return provider.request({ const start = Date.now();
await provider.request({
method: 'wallet_addEthereumChain', method: 'wallet_addEthereumChain',
params: [ getParams() ], params: [ getParams() ],
}); });
}, [ wallet, provider ]);
// if network is already added, the promise resolves immediately
if (Date.now() - start > SECOND) {
await trackUsage('add_network');
}
}, [ wallet, provider, trackUsage ]);
} }
import type { GetActivityRewardsResponse } from '@blockscout/points-types';
export const base: GetActivityRewardsResponse = {
items: [
{
date: '2025-03-10',
end_date: '2025-03-16',
activity: 'sent_transactions',
amount: '60',
percentile: 0.5,
is_pending: true,
},
{
date: '2025-03-10',
end_date: '2025-03-16',
activity: 'verified_contracts',
amount: '40',
percentile: 0.3,
is_pending: true,
},
{
date: '2025-03-10',
end_date: '2025-03-16',
activity: 'blockscout_usage',
amount: '80',
percentile: 0.8,
is_pending: true,
},
],
last_week: [
{
date: '2025-03-03',
end_date: '2025-03-09',
activity: 'sent_transactions',
amount: '40',
percentile: 0.25,
is_pending: false,
},
{
date: '2025-03-03',
end_date: '2025-03-09',
activity: 'verified_contracts',
amount: '60',
percentile: 0.6,
is_pending: false,
},
{
date: '2025-03-03',
end_date: '2025-03-09',
activity: 'blockscout_usage',
amount: '100',
percentile: 0.95,
is_pending: false,
},
],
};
import type { RewardsUserBalancesResponse } from 'types/api/rewards'; import type { GetUserBalancesResponse } from '@blockscout/points-types';
export const base: RewardsUserBalancesResponse = { export const base: GetUserBalancesResponse = {
total: '250', total: '250',
staked: '0', staked: '0',
unstaked: '0', unstaked: '0',
......
import type { RewardsUserDailyCheckResponse } from 'types/api/rewards'; import type { DailyRewardCheckResponse } from '@blockscout/points-types';
export const base: RewardsUserDailyCheckResponse = { export const base: DailyRewardCheckResponse = {
available: true, available: true,
daily_reward: '10', daily_reward: '10',
streak_reward: '10', streak_reward: '10',
......
import type { RewardsUserReferralsResponse } from 'types/api/rewards'; import type { GetReferralDataResponse } from '@blockscout/points-types';
export const base: RewardsUserReferralsResponse = { export const base: GetReferralDataResponse = {
code: 'QWERTY', code: 'QWERTY',
link: 'https://example.com?ref=QWERTY', link: 'https://example.com?ref=QWERTY',
referrals: '15', referrals: '15',
......
import type { RewardsConfigResponse } from 'types/api/rewards'; import type { GetConfigResponse } from '@blockscout/points-types';
export const base: RewardsConfigResponse = { export const base: GetConfigResponse = {
rewards: { rewards: {
registration: '100', registration: '100',
registration_with_referral: '200', registration_with_referral: '200',
daily_claim: '10', daily_claim: '10',
referral_share: '0.1', referral_share: '0.1',
streak_bonuses: {},
sent_transactions_activity_rewards: {
'1': '100',
},
verified_contracts_activity_rewards: {
'1': '100',
},
blockscout_usage_activity_rewards: {
'1': '100',
},
blockscout_activity_pass_id: '1',
}, },
auth: { auth: {
shared_siwe_login: true, shared_siwe_login: true,
}, },
activity: {
sent_transactions_activity_enabled: true,
verified_contracts_activity_enabled: true,
blockscout_usage_activity_enabled: true,
},
}; };
...@@ -22,6 +22,7 @@ import { SocketProvider } from 'lib/socket/context'; ...@@ -22,6 +22,7 @@ import { SocketProvider } from 'lib/socket/context';
import { Provider as ChakraProvider } from 'toolkit/chakra/provider'; import { Provider as ChakraProvider } from 'toolkit/chakra/provider';
import { Toaster } from 'toolkit/chakra/toaster'; import { Toaster } from 'toolkit/chakra/toaster';
import RewardsLoginModal from 'ui/rewards/login/RewardsLoginModal'; import RewardsLoginModal from 'ui/rewards/login/RewardsLoginModal';
import RewardsActivityTracker from 'ui/rewards/RewardsActivityTracker';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer'; import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
...@@ -87,7 +88,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -87,7 +88,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<SettingsContextProvider> <SettingsContextProvider>
{ getLayout(<Component { ...pageProps }/>) } { getLayout(<Component { ...pageProps }/>) }
<Toaster/> <Toaster/>
{ config.features.rewards.isEnabled && <RewardsLoginModal/> } { config.features.rewards.isEnabled && (
<>
<RewardsLoginModal/>
<RewardsActivityTracker/>
</>
) }
</SettingsContextProvider> </SettingsContextProvider>
</MarketplaceContextProvider> </MarketplaceContextProvider>
</RewardsContextProvider> </RewardsContextProvider>
......
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
import type { GetActivityRewardsResponse } from '@blockscout/points-types';
export const USER_ACTIVITY: GetActivityRewardsResponse = {
items: [
{
date: '2025-03-10',
end_date: '2025-03-16',
activity: 'sent_transactions',
amount: '60',
percentile: 0.5,
is_pending: true,
},
{
date: '2025-03-10',
end_date: '2025-03-16',
activity: 'verified_contracts',
amount: '40',
percentile: 0.3,
is_pending: true,
},
{
date: '2025-03-10',
end_date: '2025-03-16',
activity: 'blockscout_usage',
amount: '80',
percentile: 0.8,
is_pending: true,
},
],
last_week: [
{
date: '2025-03-03',
end_date: '2025-03-09',
activity: 'sent_transactions',
amount: '40',
percentile: 0.25,
is_pending: false,
},
{
date: '2025-03-03',
end_date: '2025-03-09',
activity: 'verified_contracts',
amount: '60',
percentile: 0.6,
is_pending: false,
},
{
date: '2025-03-03',
end_date: '2025-03-09',
activity: 'blockscout_usage',
amount: '100',
percentile: 0.95,
is_pending: false,
},
],
};
export type RewardsConfigResponse = {
rewards: {
registration: string;
registration_with_referral: string;
daily_claim: string;
referral_share: string;
};
auth: {
shared_siwe_login: boolean;
};
};
export type RewardsCheckRefCodeResponse = {
valid: boolean;
is_custom: boolean;
reward: string | null;
};
export type RewardsNonceResponse = {
nonce: string;
merits_login_nonce?: string;
};
export type RewardsCheckUserResponse = {
exists: boolean;
};
export type RewardsLoginResponse = {
created: boolean;
token: string;
};
export type RewardsUserBalancesResponse = {
total: string;
staked: string;
unstaked: string;
total_staking_rewards: string;
total_referral_rewards: string;
pending_referral_rewards: string;
};
export type RewardsUserDailyCheckResponse = {
available: boolean;
daily_reward: string;
streak_reward: string;
pending_referral_rewards: string;
total_reward: string;
date: string;
reset_at: string;
streak: string;
};
export type RewardsUserDailyClaimResponse = {
daily_reward: string;
streak_reward: string;
pending_referral_rewards: string;
total_reward: string;
streak: string;
};
export type RewardsUserReferralsResponse = {
code: string;
link: string;
referrals: string;
};
...@@ -68,7 +68,7 @@ const ContractExternalLibraries = ({ className, data, isLoading }: Props) => { ...@@ -68,7 +68,7 @@ const ContractExternalLibraries = ({ className, data, isLoading }: Props) => {
const content = ( const content = (
<> <>
<Heading size="sm">External libraries ({ data.length })</Heading> <Heading size="sm" level="3">External libraries ({ data.length })</Heading>
<Alert status="warning" mt={ 4 }> <Alert status="warning" mt={ 4 }>
The linked library{ apos }s source code may not be the real one. The linked library{ apos }s source code may not be the real one.
Check the source code at the library address (if any) if you want to be sure in case if there is any library linked Check the source code at the library address (if any) if you want to be sure in case if there is any library linked
......
...@@ -5,6 +5,7 @@ import { useAccount, useWalletClient, useSwitchChain } from 'wagmi'; ...@@ -5,6 +5,7 @@ import { useAccount, useWalletClient, useSwitchChain } from 'wagmi';
import type { FormSubmitResult, SmartContractMethod } from './types'; import type { FormSubmitResult, SmartContractMethod } from './types';
import config from 'configs/app'; import config from 'configs/app';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
import { getNativeCoinValue } from './utils'; import { getNativeCoinValue } from './utils';
...@@ -18,6 +19,7 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise ...@@ -18,6 +19,7 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
const { data: walletClient } = useWalletClient(); const { data: walletClient } = useWalletClient();
const { isConnected, chainId, address: account } = useAccount(); const { isConnected, chainId, address: account } = useAccount();
const { switchChainAsync } = useSwitchChain(); const { switchChainAsync } = useSwitchChain();
const { trackTransaction, trackTransactionConfirm } = useRewardsActivity();
return React.useCallback(async({ args, item, addressHash }) => { return React.useCallback(async({ args, item, addressHash }) => {
if (!isConnected) { if (!isConnected) {
...@@ -33,6 +35,7 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise ...@@ -33,6 +35,7 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
} }
const address = getAddress(addressHash); const address = getAddress(addressHash);
const activityResponse = await trackTransaction(account ?? '', address);
if (item.type === 'receive' || item.type === 'fallback') { if (item.type === 'receive' || item.type === 'fallback') {
const value = getNativeCoinValue(args[0]); const value = getNativeCoinValue(args[0]);
...@@ -40,6 +43,11 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise ...@@ -40,6 +43,11 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
to: address, to: address,
value, value,
}); });
if (activityResponse?.token) {
await trackTransactionConfirm(hash, activityResponse.token);
}
return { source: 'wallet_client', data: { hash } }; return { source: 'wallet_client', data: { hash } };
} }
...@@ -68,6 +76,10 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise ...@@ -68,6 +76,10 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
account, account,
}); });
if (activityResponse?.token) {
await trackTransactionConfirm(hash, activityResponse.token);
}
return { source: 'wallet_client', data: { hash } }; return { source: 'wallet_client', data: { hash } };
}, [ chainId, isConnected, switchChainAsync, walletClient, account ]); }, [ chainId, isConnected, switchChainAsync, walletClient, account, trackTransaction, trackTransactionConfirm ]);
} }
...@@ -14,6 +14,7 @@ import useApiFetch from 'lib/api/useApiFetch'; ...@@ -14,6 +14,7 @@ import useApiFetch from 'lib/api/useApiFetch';
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter'; import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
import delay from 'lib/delay'; import delay from 'lib/delay';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -49,9 +50,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -49,9 +50,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
const { handleSubmit, watch, formState, setError, reset, getFieldState, getValues, clearErrors } = formApi; const { handleSubmit, watch, formState, setError, reset, getFieldState, getValues, clearErrors } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>(); const methodNameRef = React.useRef<string>();
const activityToken = React.useRef<string | undefined>();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { trackContract, trackContractConfirm } = useRewardsActivity();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
const body = prepareRequestBody(data); const body = prepareRequestBody(data);
...@@ -75,6 +77,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -75,6 +77,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
} }
try { try {
const activityResponse = await trackContract(data.address);
activityToken.current = activityResponse?.token;
await apiFetch('contract_verification_via', { await apiFetch('contract_verification_via', {
pathParams: { method: data.method[0], hash: data.address.toLowerCase() }, pathParams: { method: data.method[0], hash: data.address.toLowerCase() },
fetchParams: { fetchParams: {
...@@ -89,7 +93,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -89,7 +93,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
return new Promise((resolve) => { return new Promise((resolve) => {
submitPromiseResolver.current = resolve; submitPromiseResolver.current = resolve;
}); });
}, [ apiFetch, hash, setError ]); }, [ apiFetch, hash, setError, trackContract ]);
const handleFormChange = React.useCallback(() => { const handleFormChange = React.useCallback(() => {
clearErrors('root'); clearErrors('root');
...@@ -127,8 +131,13 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -127,8 +131,13 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
{ send_immediately: true }, { send_immediately: true },
); );
if (activityToken.current) {
await trackContractConfirm(activityToken.current);
activityToken.current = undefined;
}
window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } })); window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } }));
}, [ setError, address, getValues ]); }, [ setError, address, getValues, trackContractConfirm ]);
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) { if (!formState.isSubmitting) {
......
...@@ -4,6 +4,7 @@ import type { Account, SignTypedDataParameters } from 'viem'; ...@@ -4,6 +4,7 @@ import type { Account, SignTypedDataParameters } from 'viem';
import { useAccount, useSendTransaction, useSwitchChain, useSignMessage, useSignTypedData } from 'wagmi'; import { useAccount, useSendTransaction, useSwitchChain, useSignMessage, useSignTypedData } from 'wagmi';
import config from 'configs/app'; import config from 'configs/app';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
type SendTransactionArgs = { type SendTransactionArgs = {
...@@ -27,6 +28,7 @@ export default function useMarketplaceWallet(appId: string) { ...@@ -27,6 +28,7 @@ export default function useMarketplaceWallet(appId: string) {
const { signMessageAsync } = useSignMessage(); const { signMessageAsync } = useSignMessage();
const { signTypedDataAsync } = useSignTypedData(); const { signTypedDataAsync } = useSignTypedData();
const { switchChainAsync } = useSwitchChain(); const { switchChainAsync } = useSwitchChain();
const { trackTransaction, trackTransactionConfirm } = useRewardsActivity();
const logEvent = useCallback((event: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_ACTION>['Action']) => { const logEvent = useCallback((event: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_ACTION>['Action']) => {
mixpanel.logEvent( mixpanel.logEvent(
...@@ -43,10 +45,14 @@ export default function useMarketplaceWallet(appId: string) { ...@@ -43,10 +45,14 @@ export default function useMarketplaceWallet(appId: string) {
const sendTransaction = useCallback(async(transaction: SendTransactionArgs) => { const sendTransaction = useCallback(async(transaction: SendTransactionArgs) => {
await switchNetwork(); await switchNetwork();
const activityResponse = await trackTransaction(address ?? '', transaction.to);
const tx = await sendTransactionAsync(transaction); const tx = await sendTransactionAsync(transaction);
if (activityResponse?.token) {
await trackTransactionConfirm(tx, activityResponse.token);
}
logEvent('Send Transaction'); logEvent('Send Transaction');
return tx; return tx;
}, [ sendTransactionAsync, switchNetwork, logEvent ]); }, [ sendTransactionAsync, switchNetwork, logEvent, trackTransaction, trackTransactionConfirm, address ]);
const signMessage = useCallback(async(message: string) => { const signMessage = useCallback(async(message: string) => {
await switchNetwork(); await switchNetwork();
......
import type { BrowserContext } from '@playwright/test'; import type { BrowserContext } from '@playwright/test';
import React from 'react'; import React from 'react';
import * as activityMock from 'mocks/rewards/activity';
import * as rewardsBalanceMock from 'mocks/rewards/balance'; import * as rewardsBalanceMock from 'mocks/rewards/balance';
import * as dailyRewardMock from 'mocks/rewards/dailyReward'; import * as dailyRewardMock from 'mocks/rewards/dailyReward';
import * as referralsMock from 'mocks/rewards/referrals'; import * as referralsMock from 'mocks/rewards/referrals';
...@@ -25,19 +26,25 @@ testWithAuth.beforeEach(async({ mockEnvs, mockApiResponse }) => { ...@@ -25,19 +26,25 @@ testWithAuth.beforeEach(async({ mockEnvs, mockApiResponse }) => {
await mockApiResponse('user_info', profileMock.withEmailAndWallet); await mockApiResponse('user_info', profileMock.withEmailAndWallet);
}); });
testWithAuth('base view +@dark-mode +@mobile', async({ page, render, mockApiResponse }) => { const testTab = (tab: 'tasks' | 'referrals' | 'resources') =>
await mockApiResponse('rewards_user_balances', rewardsBalanceMock.base); testWithAuth(`${ tab } tab +@dark-mode +@mobile`, async({ page, render, mockApiResponse }, testInfo) => {
await mockApiResponse('rewards_user_daily_check', dailyRewardMock.base); await mockApiResponse('rewards_user_balances', rewardsBalanceMock.base);
await mockApiResponse('rewards_user_referrals', referralsMock.base); await mockApiResponse('rewards_user_daily_check', dailyRewardMock.base);
await mockApiResponse('rewards_config', rewardsConfigMock.base); await mockApiResponse('rewards_user_referrals', referralsMock.base);
await mockApiResponse('rewards_config', rewardsConfigMock.base);
await mockApiResponse('rewards_user_activity', activityMock.base);
const component = await render(<RewardsDashboard/>); const component = await render(<RewardsDashboard/>, { hooksConfig: { router: { query: { tab } } } });
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot(testInfo.project.name === 'mobile' ? {} : {
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor, maskColor: pwConfig.maskColor,
});
}); });
});
testTab('tasks');
testTab('referrals');
testTab('resources');
testWithAuth('with error', async({ page, render }) => { testWithAuth('with error', async({ page, render }) => {
const component = await render(<RewardsDashboard/>); const component = await render(<RewardsDashboard/>);
......
...@@ -6,12 +6,13 @@ import { useRewardsContext } from 'lib/contexts/rewards'; ...@@ -6,12 +6,13 @@ import { useRewardsContext } from 'lib/contexts/rewards';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import { Alert } from 'toolkit/chakra/alert'; import { Alert } from 'toolkit/chakra/alert';
import { Link } from 'toolkit/chakra/link'; import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import DailyRewardClaimButton from 'ui/rewards/dashboard/DailyRewardClaimButton'; import DailyRewardClaimButton from 'ui/rewards/dashboard/DailyRewardClaimButton';
import RewardsDashboardCard from 'ui/rewards/dashboard/RewardsDashboardCard'; import RewardsDashboardCard from 'ui/rewards/dashboard/RewardsDashboardCard';
import RewardsDashboardCardValue from 'ui/rewards/dashboard/RewardsDashboardCardValue'; import RewardsDashboardCardValue from 'ui/rewards/dashboard/RewardsDashboardCardValue';
import RewardsDashboardInfoCard from 'ui/rewards/dashboard/RewardsDashboardInfoCard'; import ReferralsTab from 'ui/rewards/dashboard/tabs/ReferralsTab';
import RewardsReadOnlyInputWithCopy from 'ui/rewards/RewardsReadOnlyInputWithCopy'; import ResourcesTab from 'ui/rewards/dashboard/tabs/ResourcesTab';
import TasksTab from 'ui/rewards/dashboard/tabs/TasksTab';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
...@@ -66,7 +67,8 @@ const RewardsDashboard = () => { ...@@ -66,7 +67,8 @@ const RewardsDashboard = () => {
<RewardsDashboardCard <RewardsDashboardCard
title="All Merits" title="All Merits"
description="Claim your daily Merits and any Merits received from referrals." description="Claim your daily Merits and any Merits received from referrals."
direction="column-reverse" contentDirection="column-reverse"
cardValueStyle={{ minH: { base: '64px', md: '88px' } }}
contentAfter={ <DailyRewardClaimButton/> } contentAfter={ <DailyRewardClaimButton/> }
hint={ ( hint={ (
<> <>
...@@ -86,7 +88,8 @@ const RewardsDashboard = () => { ...@@ -86,7 +88,8 @@ const RewardsDashboard = () => {
<RewardsDashboardCard <RewardsDashboardCard
title="Referrals" title="Referrals"
description="Total number of users who have joined the program using your code or referral link." description="Total number of users who have joined the program using your code or referral link."
direction="column-reverse" contentDirection="column-reverse"
cardValueStyle={{ minH: { base: '64px', md: '88px' } }}
> >
<RewardsDashboardCardValue <RewardsDashboardCardValue
value={ referralsQuery.data?.referrals ? value={ referralsQuery.data?.referrals ?
...@@ -114,7 +117,8 @@ const RewardsDashboard = () => { ...@@ -114,7 +117,8 @@ const RewardsDashboard = () => {
to learn how your streak number affects daily rewards to learn how your streak number affects daily rewards
</> </>
) } ) }
direction="column-reverse" contentDirection="column-reverse"
cardValueStyle={{ minH: { base: '64px', md: '88px' } }}
> >
<RewardsDashboardCardValue <RewardsDashboardCardValue
value={ value={
...@@ -126,94 +130,26 @@ const RewardsDashboard = () => { ...@@ -126,94 +130,26 @@ const RewardsDashboard = () => {
/> />
</RewardsDashboardCard> </RewardsDashboardCard>
</Flex> </Flex>
<Flex w="full" gap={ 6 } flexDirection={{ base: 'column', md: 'row' }}> <RoutedTabs
<RewardsDashboardCard w="full"
title="Referral program" tabs={ [
description={ ( {
<> id: 'tasks',
Refer friends and boost your Merits! You receive a{ ' ' } title: 'Tasks',
<Skeleton as="span" loading={ rewardsConfigQuery.isPending }> component: <TasksTab/>,
{ rewardsConfigQuery.data?.rewards.referral_share ? },
`${ Number(rewardsConfigQuery.data?.rewards.referral_share) * 100 }%` : {
'N/A' id: 'referrals',
} title: 'Referrals',
</Skeleton> component: <ReferralsTab/>,
{ ' ' }bonus on all Merits earned by your referrals. },
</> {
) } id: 'resources',
> title: 'Resources',
<Flex component: <ResourcesTab/>,
flex={ 1 } },
gap={{ base: 2, lg: 6 }} ] }
px={{ base: 4, lg: 6 }} />
py={{ base: 4, lg: 0 }}
flexDirection={{ base: 'column', lg: 'row' }}
>
<RewardsReadOnlyInputWithCopy
label="Referral link"
value={ referralsQuery.data?.link || 'N/A' }
isLoading={ referralsQuery.isPending }
flex={ 2 }
/>
<RewardsReadOnlyInputWithCopy
label="Referral code"
value={ referralsQuery.data?.code || 'N/A' }
isLoading={ referralsQuery.isPending }
flex={ 1 }
/>
</Flex>
</RewardsDashboardCard>
<RewardsDashboardInfoCard
title="Badges"
description={ `Collect limited and legendary badges by completing different Blockscout related tasks.
Go to the badges website to see what${ apos }s available and start your collection today.` }
imageSrc="/static/merits/badges.svg"
imageWidth="260px"
imageHeight="86px"
linkText="View badges"
linkHref={ `https://merits.blockscout.com/?tab=badges&utm_source=${ config.chain.id }&utm_medium=badges` }
/>
</Flex>
<Flex w="full" gap={ 6 } flexDirection={{ base: 'column', md: 'row' }}>
<RewardsDashboardInfoCard
title="Blockscout campaigns"
description="Join Blockscout activities to earn bonus Merits and exclusive rewards from our partners!"
imageSrc="/static/merits/campaigns.svg"
imageWidth="180px"
imageHeight="76px"
linkText="Check campaigns"
linkHref={ `https://merits.blockscout.com/?tab=campaigns&utm_source=${ config.chain.id }&utm_medium=campaigns` }
/>
<RewardsDashboardInfoCard
title="Use your Merits"
description="Spend your Merits to get exclusive discounts and offers across several web3 products!"
imageSrc="/static/merits/offers.svg"
imageWidth="180px"
imageHeight="86px"
linkText="Check offers"
linkHref={ `https://merits.blockscout.com/?tab=redeem&utm_source=${ config.chain.id }&utm_medium=redeem` }
/>
</Flex>
<Flex w="full" gap={ 6 } flexDirection={{ base: 'column', md: 'row' }}>
<RewardsDashboardCard
title="Activity"
description="Earn Merits for your everyday Blockscout activities. You deserve to be rewarded for choosing open-source public goods!"
availableSoon
blurFilter
>
<RewardsDashboardCardValue label="Activity" value="0%"/>
<RewardsDashboardCardValue label="Received" value="0" withIcon/>
</RewardsDashboardCard>
<RewardsDashboardCard
title="Verify contracts"
description="Verified contracts are so important for transparency and interaction. Verify your contracts on Blockscout and receive Merits for your efforts." // eslint-disable-line max-len
availableSoon
blurFilter
>
<RewardsDashboardCardValue label="Activity" value="0%"/>
<RewardsDashboardCardValue label="Received" value="0" withIcon/>
</RewardsDashboardCard>
</Flex>
</Flex> </Flex>
</> </>
); );
......
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
const RewardsActivityTracker = () => {
const router = useRouter();
const { trackUsage } = useRewardsActivity();
useEffect(() => {
trackUsage('explore');
}, [ router.pathname, router.query, trackUsage ]);
return null;
};
export default RewardsActivityTracker;
import { Flex, Text } from '@chakra-ui/react';
import config from 'configs/app';
import { useRewardsContext } from 'lib/contexts/rewards';
import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { Heading } from 'toolkit/chakra/heading';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
const feature = config.features.rewards;
export default function RewardsActivityPassCard() {
const { rewardsConfigQuery } = useRewardsContext();
const backgroundImage = useColorModeValue('/static/merits/cells.svg', '/static/merits/cells_dark.svg');
const activityPassUrl = feature.isEnabled ?
`${ feature.api.endpoint }/?tab=spend&id=${ rewardsConfigQuery.data?.rewards?.blockscout_activity_pass_id }&utm_source=blockscout&utm_medium=tasks` :
undefined;
return (
<Flex
p={{ base: 1.5, md: 2 }}
border="1px solid"
borderColor={{ _light: 'gray.200', _dark: 'whiteAlpha.200' }}
borderRadius="lg"
gap={{ base: 1, md: 10 }}
flexDirection={{ base: 'column', md: 'row' }}
>
<Flex flex={ 1 } flexDirection="column" p={ 3 } gap={ 2 }>
<Heading level="3">
Activity pass
</Heading>
<Text textStyle="sm">
Grab your{ ' ' }
<Link external href={ activityPassUrl } loading={ rewardsConfigQuery.isLoading }>
Activity pass
</Link>{ ' ' }
then engage with various Blockscout products and features to earn Merits every day!{ ' ' }
<Link external href="https://docs.blockscout.com/using-blockscout/merits/activity-pass">
Learn more
</Link>
</Text>
</Flex>
<Flex
flex={{ base: 'none', md: 1 }}
flexDirection={{ base: 'column', md: 'row' }}
justifyContent="space-between"
alignItems="center"
h={{ base: '160px', md: '120px' }}
pr={{ base: 0, md: 8 }}
pl={{ base: 0, md: '86px' }}
pt={{ base: 4, md: 0 }}
pb={{ base: 3, md: 0 }}
borderRadius="base"
backgroundColor={{ _light: '#FFEFCE', _dark: '#E1910E' }}
overflow="hidden"
position="relative"
>
<Image
src={ backgroundImage }
alt="Background"
width="268px"
height="184px"
position="absolute"
top="-20px"
left={{ base: 'calc(50% - 134px)', md: '-8px' }}
/>
<Image
src="/static/merits/activity_pass.svg"
alt="Activity pass"
width="79px"
height="86px"
zIndex={ 1 }
/>
<Link
external
href={ activityPassUrl }
variant="underlaid"
fontWeight="500"
backgroundColor={{ _light: '#FFD57C', _dark: '#FFBA0D' }}
color="#2B1A3F"
iconColor="rgba(43, 26, 63, 0.3)"
_hover={{ color: 'link.primary.hover' }}
flexShrink={ 0 }
zIndex={ 1 }
>
Grab Activity pass
</Link>
</Flex>
</Flex>
);
}
...@@ -2,6 +2,7 @@ import { Flex, Text } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Flex, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { Badge } from 'toolkit/chakra/badge'; import { Badge } from 'toolkit/chakra/badge';
import { Heading } from 'toolkit/chakra/heading';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
type Props = { type Props = {
...@@ -11,38 +12,47 @@ type Props = { ...@@ -11,38 +12,47 @@ type Props = {
availableSoon?: boolean; availableSoon?: boolean;
blurFilter?: boolean; blurFilter?: boolean;
contentAfter?: React.ReactNode; contentAfter?: React.ReactNode;
direction?: 'column' | 'column-reverse' | 'row'; contentDirection?: 'column' | 'column-reverse' | 'row';
reverse?: boolean; reverse?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
label?: string;
isLoading?: boolean;
cardValueStyle?: object;
}; };
const RewardsDashboardCard = ({ const RewardsDashboardCard = ({
title, description, hint, availableSoon, contentAfter, title, description, availableSoon, contentAfter, cardValueStyle, hint,
direction = 'column', children, blurFilter, contentDirection = 'column', children, blurFilter, label, isLoading,
}: Props) => { }: Props) => {
return ( return (
<Flex <Flex
flexDirection={{ base: direction === 'row' ? 'column' : direction, md: direction }} as="section"
justifyContent={ direction === 'column-reverse' ? 'flex-end' : 'flex-start' } flexDirection={{ base: contentDirection === 'row' ? 'column' : contentDirection, md: contentDirection }}
justifyContent={ contentDirection === 'column-reverse' ? 'flex-end' : 'flex-start' }
p={{ base: 1.5, md: 2 }} p={{ base: 1.5, md: 2 }}
border="1px solid" border="1px solid"
borderColor={{ _light: 'gray.200', _dark: 'whiteAlpha.200' }} borderColor={{ _light: 'gray.200', _dark: 'whiteAlpha.200' }}
borderRadius="lg" borderRadius="lg"
gap={{ base: 1, md: direction === 'row' ? 10 : 1 }} gap={{ base: 4, md: contentDirection === 'row' ? 10 : 4 }}
w={ direction === 'row' ? 'full' : 'auto' } w={ contentDirection === 'row' ? 'full' : 'auto' }
flex={ direction !== 'row' ? 1 : '0 1 auto' } flex={ contentDirection !== 'row' ? 1 : '0 1 auto' }
> >
<Flex <Flex
flexDirection="column" flexDirection="column"
gap={ 2 } gap={ 2 }
p={{ base: 1.5, md: 3 }} px={{ base: 1.5, md: 3 }}
w={{ base: 'full', md: direction === 'row' ? '340px' : 'full' }} pb={ contentDirection === 'column-reverse' ? { base: 1.5, md: 3 } : 0 }
pt={ contentDirection === 'column-reverse' ? 0 : { base: 1.5, md: 3 } }
w={{ base: 'full', md: contentDirection === 'row' ? '340px' : 'full' }}
> >
<Flex alignItems="center" gap={ 2 }> { label && <Badge loading={ isLoading }>{ label }</Badge> }
<Text fontSize={{ base: 'md', md: 'lg' }} fontWeight="500">{ title }</Text> { title && (
{ hint && <Hint label={ hint }/> } <Flex alignItems="center" gap={ 2 }>
{ availableSoon && <Badge colorPalette="blue">Available soon</Badge> } <Heading level="3">{ title }</Heading>
</Flex> { hint && <Hint label={ hint }/> }
{ availableSoon && <Badge colorPalette="blue">Available soon</Badge> }
</Flex>
) }
<Text as="div" fontSize="sm"> <Text as="div" fontSize="sm">
{ description } { description }
</Text> </Text>
...@@ -53,10 +63,12 @@ const RewardsDashboardCard = ({ ...@@ -53,10 +63,12 @@ const RewardsDashboardCard = ({
justifyContent="space-around" justifyContent="space-around"
borderRadius={{ base: 'lg', md: '8px' }} borderRadius={{ base: 'lg', md: '8px' }}
backgroundColor={{ _light: 'gray.50', _dark: 'whiteAlpha.50' }} backgroundColor={{ _light: 'gray.50', _dark: 'whiteAlpha.50' }}
minH={{ base: '80px', md: '128px' }} minH={{ base: '104px', md: '128px' }}
mt={ contentDirection === 'column' ? 'auto' : 0 }
filter="auto" filter="auto"
blur={ blurFilter ? '4px' : '0' } blur={ blurFilter ? '4px' : '0' }
flex={ direction === 'row' ? 1 : '0 1 auto' } flex={ contentDirection === 'row' ? 1 : '0 1 auto' }
{ ...cardValueStyle }
> >
{ children } { children }
</Flex> </Flex>
......
import { Flex, Text } from '@chakra-ui/react'; import { Flex, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { Heading } from 'toolkit/chakra/heading';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
...@@ -12,16 +13,15 @@ type Props = { ...@@ -12,16 +13,15 @@ type Props = {
withIcon?: boolean; withIcon?: boolean;
hint?: string | React.ReactNode; hint?: string | React.ReactNode;
isLoading?: boolean; isLoading?: boolean;
bottomText?: string;
}; };
const RewardsDashboardCard = ({ label, value, withIcon, hint, isLoading }: Props) => ( const RewardsDashboardCard = ({ label, value, withIcon, hint, isLoading, bottomText }: Props) => (
<Flex key={ label } flexDirection="column" alignItems="center" gap={ 2 }> <Flex key={ label } flexDirection="column" alignItems="center" gap={ 2 }>
{ label && ( { label && (
<Flex alignItems="center" gap={ 1 }> <Flex alignItems="center" gap={ 1 }>
{ hint && ( { hint && <Hint label={ hint }/> }
<Hint label={ hint }/> <Text textStyle="xs" fontWeight="500" color="text.secondary">
) }
<Text fontSize="xs" fontWeight="500" color="text.secondary">
{ label } { label }
</Text> </Text>
</Flex> </Flex>
...@@ -35,10 +35,17 @@ const RewardsDashboardCard = ({ label, value, withIcon, hint, isLoading }: Props ...@@ -35,10 +35,17 @@ const RewardsDashboardCard = ({ label, value, withIcon, hint, isLoading }: Props
minW="100px" minW="100px"
> >
{ withIcon && <MeritsIcon boxSize={ 8 }/> } { withIcon && <MeritsIcon boxSize={ 8 }/> }
<Text fontSize={{ base: '24px', md: '32px' }} lineHeight={{ base: '24px', md: 1.5 }} fontWeight="500"> <Heading level="1">
{ value } { value }
</Text> </Heading>
</Skeleton> </Skeleton>
{ bottomText && (
<Skeleton loading={ isLoading }>
<Text textStyle="xs" fontWeight="500" color="text.secondary">
{ bottomText }
</Text>
</Skeleton>
) }
</Flex> </Flex>
); );
......
import { Flex, Text } from '@chakra-ui/react';
import React from 'react';
import type { GetInstancesResponse } from '@blockscout/points-types';
import { DialogBody, DialogContent, DialogRoot, DialogHeader } from 'toolkit/chakra/dialog';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
isOpen: boolean;
onClose: () => void;
items: GetInstancesResponse['items'] | undefined;
};
const RewardsInstancesModal = ({ isOpen, onClose, items }: Props) => {
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (!open) {
onClose();
}
}, [ onClose ]);
return (
<DialogRoot
open={ isOpen }
onOpenChange={ handleOpenChange }
size={{ lgDown: 'full', lg: 'sm' }}
>
<DialogContent>
<DialogHeader>
Choose explorer
</DialogHeader>
<DialogBody>
<Flex flexDir="column" gap={ 6 }>
<Text>
Choose Blockscout explorer that you want to interact with and earn
Merits
</Text>
<Flex flexWrap="wrap" gap={ 2 }>
{ items?.map((instance) => (
<Link
external
noIcon
key={ instance.chain_id }
href={ instance.domain }
display="flex"
gap={ 2 }
alignItems="center"
p={ 2 }
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.100' }}
borderRadius="base"
>
<Image
src={ instance.details?.icon_url }
alt={ instance.name }
boxSize={ 5 }
flexShrink={ 0 }
fallback={ (
<IconSvg
name="networks/icon-placeholder"
boxSize={ 5 }
color="text.secondary"
/>
) }
/>
<Text
textStyle="sm"
fontWeight="500"
color="text.primary"
_groupHover={{ color: 'inherit' }}
>
{ instance.name }
</Text>
</Link>
)) }
</Flex>
</Flex>
</DialogBody>
</DialogContent>
</DialogRoot>
);
};
export default RewardsInstancesModal;
import { Text } from '@chakra-ui/react';
import React from 'react';
import { DialogBody, DialogContent, DialogRoot, DialogHeader } from 'toolkit/chakra/dialog';
type Props = {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
};
const RewardsTaskDetailsModal = ({ isOpen, onClose, title, children }: Props) => {
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (!open) {
onClose();
}
}, [ onClose ]);
return (
<DialogRoot
open={ isOpen }
onOpenChange={ handleOpenChange }
size={{ lgDown: 'full', lg: 'sm' }}
>
<DialogContent>
<DialogHeader>
{ title }
</DialogHeader>
<DialogBody>
<Text>{ children }</Text>
<Text textStyle="sm" color="text.secondary" mt={ 3 }>
Note: Merits are only earned on supported networks where the program is active.
</Text>
</DialogBody>
</DialogContent>
</DialogRoot>
);
};
export default RewardsTaskDetailsModal;
import { Flex } from '@chakra-ui/react';
import { useRewardsContext } from 'lib/contexts/rewards';
import { Skeleton } from 'toolkit/chakra/skeleton';
import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy';
import RewardsDashboardCard from '../RewardsDashboardCard';
export default function ReferralsTab() {
const { rewardsConfigQuery, referralsQuery } = useRewardsContext();
return (
<RewardsDashboardCard
title="Referral program"
description={ (
<>
Refer friends and boost your Merits! You receive a{ ' ' }
<Skeleton as="span" loading={ rewardsConfigQuery.isPending }>
{ rewardsConfigQuery.data?.rewards?.referral_share ?
`${ Number(rewardsConfigQuery.data.rewards.referral_share) * 100 }%` :
'N/A'
}
</Skeleton>
{ ' ' }bonus on all Merits earned by your referrals.
</>
) }
contentDirection="row"
>
<Flex
flex={ 1 }
gap={{ base: 2, lg: 6 }}
px={{ base: 4, lg: 6 }}
py={{ base: 4, lg: 0 }}
flexDirection={{ base: 'column', lg: 'row' }}
>
<RewardsReadOnlyInputWithCopy
label="Referral link"
value={ referralsQuery.data?.link || 'N/A' }
isLoading={ referralsQuery.isPending }
flex={ 2 }
/>
<RewardsReadOnlyInputWithCopy
label="Referral code"
value={ referralsQuery.data?.code || 'N/A' }
isLoading={ referralsQuery.isPending }
flex={ 1 }
/>
</Flex>
</RewardsDashboardCard>
);
}
import { Grid } from '@chakra-ui/react';
import config from 'configs/app';
import { apos } from 'lib/html-entities';
import RewardsDashboardInfoCard from '../RewardsDashboardInfoCard';
export default function ResourcesTab() {
return (
<Grid
w="full"
gap={ 6 }
templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }}
>
<RewardsDashboardInfoCard
title="Badges"
description={ `Collect limited and legendary badges by completing different Blockscout related tasks.
Go to the badges website to see what${ apos }s available and start your collection today.` }
imageSrc="/static/merits/badges.svg"
imageWidth="180px"
imageHeight="86px"
linkText="View badges"
linkHref={ `https://merits.blockscout.com/?tab=badges&utm_source=${ config.chain.id }&utm_medium=badges` }
/>
<RewardsDashboardInfoCard
title="Blockscout campaigns"
description="Join Blockscout activities to earn bonus Merits and exclusive rewards from our partners!"
imageSrc="/static/merits/campaigns.svg"
imageWidth="180px"
imageHeight="76px"
linkText="Check campaigns"
linkHref={ `https://merits.blockscout.com/?tab=campaigns&utm_source=${ config.chain.id }&utm_medium=campaigns` }
/>
<RewardsDashboardInfoCard
title="Use your Merits"
description="Spend your Merits to get exclusive discounts and offers across several web3 products!"
imageSrc="/static/merits/offers.svg"
imageWidth="180px"
imageHeight="86px"
linkText="Check offers"
linkHref={ `https://merits.blockscout.com/?tab=redeem&utm_source=${ config.chain.id }&utm_medium=redeem` }
/>
</Grid>
);
}
This diff is collapsed.
...@@ -26,7 +26,7 @@ const RewardsLoginModal = () => { ...@@ -26,7 +26,7 @@ const RewardsLoginModal = () => {
const [ isLoginStep, setIsLoginStep ] = React.useState(true); const [ isLoginStep, setIsLoginStep ] = React.useState(true);
const [ isReferral, setIsReferral ] = React.useState(false); const [ isReferral, setIsReferral ] = React.useState(false);
const [ customReferralReward, setCustomReferralReward ] = React.useState<string | null>(null); const [ customReferralReward, setCustomReferralReward ] = React.useState<string | undefined>();
const [ authModalInitialScreen, setAuthModalInitialScreen ] = React.useState<Screen>(); const [ authModalInitialScreen, setAuthModalInitialScreen ] = React.useState<Screen>();
const authModal = useDisclosure(); const authModal = useDisclosure();
...@@ -34,11 +34,11 @@ const RewardsLoginModal = () => { ...@@ -34,11 +34,11 @@ const RewardsLoginModal = () => {
if (!isLoginModalOpen) { if (!isLoginModalOpen) {
setIsLoginStep(true); setIsLoginStep(true);
setIsReferral(false); setIsReferral(false);
setCustomReferralReward(null); setCustomReferralReward(undefined);
} }
}, [ isLoginModalOpen ]); }, [ isLoginModalOpen ]);
const goNext = useCallback((isReferral: boolean, reward: string | null) => { const goNext = useCallback((isReferral: boolean, reward: string | undefined) => {
setIsReferral(isReferral); setIsReferral(isReferral);
setCustomReferralReward(reward); setCustomReferralReward(reward);
setIsLoginStep(false); setIsLoginStep(false);
......
...@@ -13,16 +13,16 @@ import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy'; ...@@ -13,16 +13,16 @@ import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy';
type Props = { type Props = {
isReferral: boolean; isReferral: boolean;
customReferralReward: string | null; customReferralReward: string | undefined;
}; };
const CongratsStepContent = ({ isReferral, customReferralReward }: Props) => { const CongratsStepContent = ({ isReferral, customReferralReward }: Props) => {
const { referralsQuery, rewardsConfigQuery } = useRewardsContext(); const { referralsQuery, rewardsConfigQuery } = useRewardsContext();
const registrationReward = Number(rewardsConfigQuery.data?.rewards.registration); const registrationReward = Number(rewardsConfigQuery.data?.rewards?.registration);
const registrationWithReferralReward = customReferralReward ? const registrationWithReferralReward = customReferralReward ?
Number(customReferralReward) + registrationReward : Number(customReferralReward) + registrationReward :
Number(rewardsConfigQuery.data?.rewards.registration_with_referral); Number(rewardsConfigQuery.data?.rewards?.registration_with_referral);
const referralReward = registrationWithReferralReward - registrationReward; const referralReward = registrationWithReferralReward - registrationReward;
const refLink = referralsQuery.data?.link || 'N/A'; const refLink = referralsQuery.data?.link || 'N/A';
...@@ -94,8 +94,8 @@ const CongratsStepContent = ({ isReferral, customReferralReward }: Props) => { ...@@ -94,8 +94,8 @@ const CongratsStepContent = ({ isReferral, customReferralReward }: Props) => {
<Text fontSize="md" mt={ 2 }> <Text fontSize="md" mt={ 2 }>
Receive a{ ' ' } Receive a{ ' ' }
<Skeleton as="span" loading={ rewardsConfigQuery.isLoading }> <Skeleton as="span" loading={ rewardsConfigQuery.isLoading }>
{ rewardsConfigQuery.data?.rewards.referral_share ? { rewardsConfigQuery.data?.rewards?.referral_share ?
`${ Number(rewardsConfigQuery.data?.rewards.referral_share) * 100 }%` : `${ Number(rewardsConfigQuery.data.rewards.referral_share) * 100 }%` :
'N/A' 'N/A'
} }
</Skeleton> </Skeleton>
......
...@@ -18,7 +18,7 @@ import { Switch } from 'toolkit/chakra/switch'; ...@@ -18,7 +18,7 @@ import { Switch } from 'toolkit/chakra/switch';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
type Props = { type Props = {
goNext: (isReferral: boolean, reward: string | null) => void; goNext: (isReferral: boolean, reward: string | undefined) => void;
closeModal: () => void; closeModal: () => void;
openAuthModal: (isAuth: boolean, trySharedLogin?: boolean) => void; openAuthModal: (isAuth: boolean, trySharedLogin?: boolean) => void;
}; };
...@@ -48,7 +48,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -48,7 +48,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists, isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists,
[ isConnected, isAddressMismatch, checkUserQuery ]); [ isConnected, isAddressMismatch, checkUserQuery ]);
const canTrySharedLogin = rewardsConfigQuery.data?.auth.shared_siwe_login && checkUserQuery.data?.exists !== false && !isLoggedIntoAccountWithWallet; const canTrySharedLogin = rewardsConfigQuery.data?.auth?.shared_siwe_login && checkUserQuery.data?.exists !== false && !isLoggedIntoAccountWithWallet;
const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setRefCode(event.target.value); setRefCode(event.target.value);
......
...@@ -6,6 +6,7 @@ import type { TokenInfo } from 'types/api/token'; ...@@ -6,6 +6,7 @@ import type { TokenInfo } from 'types/api/token';
import config from 'configs/app'; import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import useProvider from 'lib/web3/useProvider'; import useProvider from 'lib/web3/useProvider';
import useSwitchOrAddChain from 'lib/web3/useSwitchOrAddChain'; import useSwitchOrAddChain from 'lib/web3/useSwitchOrAddChain';
...@@ -62,6 +63,7 @@ const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'i ...@@ -62,6 +63,7 @@ const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'i
const { provider, wallet } = useProvider(); const { provider, wallet } = useProvider();
const switchOrAddChain = useSwitchOrAddChain(); const switchOrAddChain = useSwitchOrAddChain();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { trackUsage } = useRewardsActivity();
const handleClick = React.useCallback(async() => { const handleClick = React.useCallback(async() => {
if (!wallet) { if (!wallet) {
...@@ -89,6 +91,8 @@ const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'i ...@@ -89,6 +91,8 @@ const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'i
description: 'Successfully added token to your wallet', description: 'Successfully added token to your wallet',
}); });
await trackUsage('add_token');
mixpanel.logEvent(mixpanel.EventTypes.ADD_TO_WALLET, { mixpanel.logEvent(mixpanel.EventTypes.ADD_TO_WALLET, {
Target: 'token', Target: 'token',
Wallet: wallet, Wallet: wallet,
...@@ -101,7 +105,7 @@ const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'i ...@@ -101,7 +105,7 @@ const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'i
description: (error as Error)?.message || 'Something went wrong', description: (error as Error)?.message || 'Something went wrong',
}); });
} }
}, [ wallet, token, tokenId, switchOrAddChain, provider ]); }, [ wallet, token, tokenId, switchOrAddChain, provider, trackUsage ]);
if (!provider || !wallet) { if (!provider || !wallet) {
return null; return null;
......
import React from 'react'; import React from 'react';
import { useSignMessage, useSwitchChain } from 'wagmi'; import { useSignMessage, useSwitchChain } from 'wagmi';
import type * as rewards from '@blockscout/points-types';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import type { RewardsCheckUserResponse, RewardsConfigResponse, RewardsLoginResponse, RewardsNonceResponse } from 'types/api/rewards';
import config from 'configs/app'; import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
...@@ -65,12 +65,12 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log ...@@ -65,12 +65,12 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log
throw new Error('User already has logged in to rewards'); throw new Error('User already has logged in to rewards');
} }
const rewardsConfig = await apiFetch('rewards_config') as RewardsConfigResponse; const rewardsConfig = await apiFetch('rewards_config') as rewards.GetConfigResponse;
if (!rewardsConfig.auth.shared_siwe_login) { if (!rewardsConfig.auth?.shared_siwe_login) {
throw new Error('Shared SIWE login is not enabled'); throw new Error('Shared SIWE login is not enabled');
} }
const rewardsCheckUser = await apiFetch('rewards_check_user', { pathParams: { address } }) as RewardsCheckUserResponse; const rewardsCheckUser = await apiFetch('rewards_check_user', { pathParams: { address } }) as rewards.AuthUserResponse;
if (!rewardsCheckUser.exists) { if (!rewardsCheckUser.exists) {
throw new Error('Rewards user does not exist'); throw new Error('Rewards user does not exist');
} }
...@@ -78,7 +78,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log ...@@ -78,7 +78,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log
const nonceConfig = await apiFetch( const nonceConfig = await apiFetch(
'rewards_nonce', 'rewards_nonce',
{ queryParams: { blockscout_login_address: address, blockscout_login_chain_id: config.chain.id } }, { queryParams: { blockscout_login_address: address, blockscout_login_chain_id: config.chain.id } },
) as RewardsNonceResponse; ) as rewards.AuthNonceResponse;
if (!nonceConfig.merits_login_nonce || !nonceConfig.nonce) { if (!nonceConfig.merits_login_nonce || !nonceConfig.nonce) {
throw new Error('Cannot get merits login nonce'); throw new Error('Cannot get merits login nonce');
} }
...@@ -127,7 +127,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log ...@@ -127,7 +127,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log
signature, signature,
}, },
}, },
}) as RewardsLoginResponse : undefined; }) as rewards.AuthLoginResponse : undefined;
if (!('name' in authResponse)) { if (!('name' in authResponse)) {
throw Error('Something went wrong'); throw Error('Something went wrong');
......
...@@ -1536,6 +1536,11 @@ ...@@ -1536,6 +1536,11 @@
resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66" resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66"
integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ== integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ==
"@blockscout/points-types@1.3.0-alpha.1":
version "1.3.0-alpha.1"
resolved "https://registry.yarnpkg.com/@blockscout/points-types/-/points-types-1.3.0-alpha.1.tgz#d1f255de6ccfa09b8a938ffe17f6aedd559273a3"
integrity sha512-yZcxvPpS1JT79dZrzSeP4r3BM5cqSnsVnclCIpJMUO3qBRWEytVfDGXcqNacwqp3342Im8RB/YPLKAuJGc+CrA==
"@blockscout/stats-types@2.5.0-alpha": "@blockscout/stats-types@2.5.0-alpha":
version "2.5.0-alpha" version "2.5.0-alpha"
resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-2.5.0-alpha.tgz#e34698577a337ce08b176d8709f89f185d9d9359" resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-2.5.0-alpha.tgz#e34698577a337ce08b176d8709f89f185d9d9359"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment