Skip to content

Commit

Permalink
Feature/human app frontend 2nd milestone (#200)
Browse files Browse the repository at this point in the history
* Add lock

* feat(app/worker): add missing hcaptcha

* fix(app/hcaptcha): refresh form captcha after FetchError

* feat(app): add missing captcha

* feat(app): add error to captcha

* feat(app): add gracefull error handling (#172)

* fix(app/worker/profile): fix KYC start process

* feat(app/worker/profile): fix mobile jobs tables

* fix(app): fix copy (#176)

* fix(app/worker/profile): disable job discovery when did not register address (#175)

* fix(app/wallet-connect): improve networks narrowing

* fix(app/operator/set-up/stake): redirect after successfull stake

* fix(app/operator/set-up/stake): remove useless changes

* feat(app/auth): logout user when tokens expire

* fix(app/worker/jobs): extend job routing

* feat(app/operator/set-up): ommit approve if not nesseccary while staking

* feat(app/auth): allow user to switch account on homepage

* fix(app/auth/jwt-expiration): fix naming

* fix(app/worker/profile): remove callbacks from KYC mutation hook

* feat(app/worker/email-verification): redirect unverified user to veri… (#189)

* feat(app/worker/email-verification): redirect unverified user to verify page

* feat(app/worker/email-verification): logout on cancel button

* fix(app/auth): update jwt payload (#194)

* feat(app/worker/email-verification): prevent unathenticated user from resend email verification (#195)

* Ham 20 register address and add kyc on chain should be two different steps (#191)

* feat(app/worker/register-address): separate address registering from KVStore setup

* feat(app/worker/profile): change adding value to KVStore

* feat(app/worker/profile): update KYC on chain feature

* feat(app/worker/profile): update profile actions

* feat(app/worker/profile): handle new KYC responses (#196)

* feat(app/worker/profile): remove oracle address

* fix(app/worker/jobs): change assigment_id type (#193)

* Ham 44 missing oracle address in my jobs table (#197)

* fix(app/worker/jobs): fix my jobs fetching

* fix(app/worker/jobs/resign-job): fix resign jobs dto

* feat(app/layout): close drawer on link click (#198)

* fix(app): add fixes after review

* revert yarn.lock

---------

Co-authored-by: MicDebBlocky <[email protected]>
  • Loading branch information
KacperKoza343 and MicDebBlocky authored Jul 8, 2024
1 parent 3b167d4 commit 916c4a5
Show file tree
Hide file tree
Showing 74 changed files with 1,240 additions and 848 deletions.
3 changes: 3 additions & 0 deletions packages/apps/human-app/frontend/src/api/api-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const apiPaths = {
registerAddress: {
path: '/user/register-address',
},
signedAddress: {
path: '/kyc/on-chain',
},
enableHCaptchaLabeling: '/labeling/h-captcha/enable',
verifyHCaptchaLabeling: '/labeling/h-captcha/verify',
hCaptchaUserStats: '/labeling/h-captcha/user-stats',
Expand Down
16 changes: 14 additions & 2 deletions packages/apps/human-app/frontend/src/api/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import merge from 'lodash/merge';
import type { ZodType, ZodTypeDef } from 'zod';
import { ZodError, type ZodType, type ZodTypeDef } from 'zod';
import type { ResponseError } from '@/shared/types/global.type';
import type { SignInSuccessResponse } from '@/api/servieces/worker/sign-in';
// eslint-disable-next-line import/no-cycle -- cause by refresh token retry
Expand Down Expand Up @@ -203,7 +203,19 @@ export function createFetcher(defaultFetcherConfig?: {
return data;
}

return fetcherOptions.successSchema.parse(data);
try {
return fetcherOptions.successSchema.parse(data);
} catch (error) {
if (error instanceof ZodError) {
// eslint-disable-next-line no-console -- ...
console.error('Invalid response');
error.errors.forEach((e) => {
// eslint-disable-next-line no-console -- ...
console.error(e);
});
}
throw error;
}
}

return fetcher;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import {
useQueryClient,
} from '@tanstack/react-query';
import { t } from 'i18next';
import { useNavigate } from 'react-router-dom';
import { ethers } from 'ethers';
import { stakingStake } from '@/smart-contracts/Staking/staking-stake';
import type { ResponseError } from '@/shared/types/global.type';
import { useConnectedWallet } from '@/auth-web3/use-connected-wallet';
import { getContractAddress } from '@/smart-contracts/get-contract-address';
import { hmTokenApprove } from '@/smart-contracts/HMToken/hm-token-approve';
import type { ContractCallArguments } from '@/smart-contracts/types';
import { routerPaths } from '@/router/router-paths';
import { hmTokenAllowance } from '@/smart-contracts/HMToken/hm-token-allowance';
import { useHMTokenDecimals } from '@/api/servieces/operator/human-token-decimals';

type AmountValidation = z.ZodEffects<
z.ZodEffects<z.ZodString, string, string>,
Expand Down Expand Up @@ -48,6 +53,7 @@ async function addStakeMutationFn(
data: AddStakeCallArguments & {
address: string;
amount: string;
decimals?: number;
} & Omit<ContractCallArguments, 'contractAddress'>
) {
const stakingContractAddress = getContractAddress({
Expand All @@ -60,15 +66,32 @@ async function addStakeMutationFn(
contractName: 'HMToken',
});

await hmTokenApprove({
const allowance = await hmTokenAllowance({
spender: stakingContractAddress,
owner: data.address || '',
contractAddress: hmTokenContractAddress,
chainId: data.chainId,
provider: data.provider,
signer: data.signer,
amount: data.amount,
chainId: data.chainId,
});

const amountBigInt = ethers.parseUnits(data.amount, data.decimals);

if (amountBigInt - allowance > 0) {
await hmTokenApprove({
spender: stakingContractAddress,
contractAddress: hmTokenContractAddress,
amount: amountBigInt.toString(),
provider: data.provider,
signer: data.signer,
chainId: data.chainId,
});
}
await stakingStake({
...data,
amount: amountBigInt.toString(),
contractAddress: stakingContractAddress,
});
await stakingStake({ ...data, contractAddress: stakingContractAddress });
return data;
}

Expand All @@ -78,7 +101,10 @@ export function useAddStakeMutation() {
address,
web3ProviderMutation: { data: web3data },
} = useConnectedWallet();
const { data: HMTDecimals } = useHMTokenDecimals();

const queryClient = useQueryClient();
const navigate = useNavigate();

return useMutation({
mutationFn: (data: AddStakeCallArguments) =>
Expand All @@ -88,8 +114,10 @@ export function useAddStakeMutation() {
provider: web3data?.provider,
signer: web3data?.signer,
chainId,
decimals: HMTDecimals,
}),
onSuccess: async () => {
navigate(routerPaths.operator.addKeys);
await queryClient.invalidateQueries();
},
onError: async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable camelcase -- api response*/
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { z } from 'zod';
import { useParams } from 'react-router-dom';
import { apiClient } from '@/api/api-client';
import { apiPaths } from '@/api/api-paths';
import { stringifyUrlQueryObject } from '@/shared/helpers/stringify-url-query-object';
Expand All @@ -26,7 +27,9 @@ export type AvailableJobsSuccessResponse = z.infer<
typeof availableJobsSuccessResponseSchema
>;

type GetJobTableDataDto = JobsFilterStoreProps['filterParams'];
type GetJobTableDataDto = JobsFilterStoreProps['filterParams'] & {
oracle_address: string;
};

const getAvailableJobsTableData = async (dto: GetJobTableDataDto) => {
return apiClient(
Expand All @@ -43,22 +46,26 @@ const getAvailableJobsTableData = async (dto: GetJobTableDataDto) => {

export function useGetAvailableJobsData() {
const { filterParams } = useJobsFilterStore();
const { address: oracle_address } = useParams<{ address: string }>();
const dto = { ...filterParams, oracle_address: oracle_address || '' };

return useQuery({
queryKey: ['availableJobs', filterParams],
queryFn: () => getAvailableJobsTableData(filterParams),
queryKey: ['availableJobs', dto],
queryFn: () => getAvailableJobsTableData(dto),
});
}

export function useInfiniteGetAvailableJobsData() {
const { filterParams } = useJobsFilterStore();
const { address: oracle_address } = useParams<{ address: string }>();
const dto = { ...filterParams, oracle_address: oracle_address || '' };

return useInfiniteQuery({
initialPageParam: 0,
queryKey: ['availableJobsInfinite', filterParams],
queryFn: () => getAvailableJobsTableData(filterParams),
queryKey: ['availableJobsInfinite', dto],
queryFn: () => getAvailableJobsTableData(dto),
getNextPageParam: (pageParams) => {
return pageParams.total_pages === pageParams.page
return pageParams.total_pages - 1 <= pageParams.page
? undefined
: pageParams.page;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,106 +1,43 @@
/* eslint-disable camelcase -- ...*/
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';
import { jwtDecode } from 'jwt-decode';
import { useAuthenticatedUser } from '@/auth/use-authenticated-user';
import { apiClient } from '@/api/api-client';
import { apiPaths } from '@/api/api-paths';
import { signInSuccessResponseSchema } from '@/api/servieces/worker/sign-in';
import { FetchError } from '@/api/fetcher';
import { browserAuthProvider } from '@/shared/helpers/browser-auth-provider';
import type { ResponseError } from '@/shared/types/global.type';
import { useGetAccessTokenMutation } from '@/api/servieces/common/get-access-token';

const kycSessionIdSchema = z.object({
session_id: z.string(),
});

type KycError = 'emailNotVerified' | 'kycApproved';

export type KycSessionIdSuccessSchema = z.infer<typeof kycSessionIdSchema>;
type KycSessionIdMutationResult =
| (KycSessionIdSuccessSchema & { error?: never })
| { session_id?: never; error: KycError };

export function useKycSessionIdMutation(callbacks: {
onError?: (error: ResponseError) => void;
onSuccess?: () => void;
}) {
export function useKycSessionIdMutation() {
const queryClient = useQueryClient();
const { user } = useAuthenticatedUser();
const { mutateAsync: getAccessTokenMutation } = useGetAccessTokenMutation();
return useMutation({
mutationFn: async (): Promise<KycSessionIdMutationResult> => {
const accessToken = browserAuthProvider.getAccessToken();
if (!accessToken) {
// unauthenticated
browserAuthProvider.signOut();
throw new Error();
}
const tokenPayload = jwtDecode(accessToken);
const tokenExpired =
(tokenPayload.exp || 0) < new Date().getTime() / 1000;

const tokenOrSignInResponseData = tokenExpired
? await apiClient(apiPaths.worker.obtainAccessToken.path, {
successSchema: signInSuccessResponseSchema,
options: {
method: 'POST',
body: JSON.stringify({
refresh_token: browserAuthProvider.getRefreshToken(),
}),
},
})
: accessToken;

if (typeof tokenOrSignInResponseData !== 'string') {
browserAuthProvider.signIn(
tokenOrSignInResponseData,
browserAuthProvider.authType
);
}

mutationFn: async () => {
try {
const response = await apiClient(apiPaths.worker.kycSessionId.path, {
const result = await apiClient(apiPaths.worker.kycSessionId.path, {
successSchema: kycSessionIdSchema,
authenticated: true,
options: {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
Authorization: `Bearer ${browserAuthProvider.getAccessToken()}`,
}),
},
});
return response;
return result;
} catch (error) {
// 401 - unauthenticated also means that email not verified
// 400 - bad request also means that KYC already approved

// normally if app receives 401 status code it tries to obtain
// access token with refresh token, kycSessionIdMutation has to
// implement its own flow to handle that case because 401 that
// can be revived for "kyc/start" doesn't mean that JWT token expired
if (error instanceof FetchError) {
if (error.status === 401) {
return { error: 'emailNotVerified' };
}
if (error.status === 400) {
await getAccessTokenMutation('web2');
return { error: 'kycApproved' };
}
}

await getAccessTokenMutation('web2');
throw error;
}
},

onError: (error) => {
onError: () => {
void queryClient.invalidateQueries();
if (callbacks.onError) callbacks.onError(error);
},
onSuccess: () => {
void queryClient.invalidateQueries();
if (callbacks.onSuccess) callbacks.onSuccess();
},
mutationKey: ['kycSessionId', user.email],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,15 @@ export function useGetOnChainRegisteredAddress() {
chainId,
});

return {
registeredAddressOnChain,
};
return registeredAddressOnChain;
},
retry: 0,
refetchInterval: 0,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
queryKey: [
user.address,
user.wallet_address,
user.reputation_network,
chainId,
address,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useQuery } from '@tanstack/react-query';
import { z } from 'zod';
import { apiClient } from '@/api/api-client';
import { apiPaths } from '@/api/api-paths';

const signedAddressSuccessSchema = z.object({
key: z.string(),
value: z.string(),
});

export type SignedAddressSuccess = z.infer<typeof signedAddressSuccessSchema>;

const getSignedAddress = async () => {
return apiClient(apiPaths.worker.signedAddress.path, {
authenticated: true,
successSchema: signedAddressSuccessSchema,
options: {
method: 'GET',
},
});
};

export function useGetSignedAddress() {
return useQuery({
queryKey: ['getSignedAddress'],
queryFn: getSignedAddress,
});
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable camelcase -- api response*/
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { z } from 'zod';
import { useParams } from 'react-router-dom';
import { apiClient } from '@/api/api-client';
import { apiPaths } from '@/api/api-paths';
import { stringifyUrlQueryObject } from '@/shared/helpers/stringify-url-query-object';
Expand All @@ -9,7 +10,7 @@ import type { MyJobsFilterStoreProps } from '@/hooks/use-my-jobs-filter-store';
import { useMyJobsFilterStore } from '@/hooks/use-my-jobs-filter-store';

const myJobSchema = z.object({
assignment_id: z.number(),
assignment_id: z.string(),
escrow_address: z.string(),
chain_id: z.number(),
job_type: z.string(),
Expand Down Expand Up @@ -41,7 +42,9 @@ export interface MyJobsWithJobTypes {
jobs: MyJobsSuccessResponse;
}

type GetMyJobTableDataDto = MyJobsFilterStoreProps['filterParams'];
type GetMyJobTableDataDto = MyJobsFilterStoreProps['filterParams'] & {
oracle_address: string;
};

const getMyJobsTableData = async (dto: GetMyJobTableDataDto) => {
return apiClient(
Expand All @@ -58,22 +61,26 @@ const getMyJobsTableData = async (dto: GetMyJobTableDataDto) => {

export function useGetMyJobsData() {
const { filterParams } = useMyJobsFilterStore();
const { address } = useParams<{ address: string }>();
const dto = { ...filterParams, oracle_address: address || '' };

return useQuery({
queryKey: ['myJobs', filterParams],
queryFn: () => getMyJobsTableData(filterParams),
queryKey: ['myJobs', dto],
queryFn: () => getMyJobsTableData(dto),
});
}

export function useInfiniteGetMyJobsData() {
const { filterParams } = useMyJobsFilterStore();
const { address } = useParams<{ address: string }>();
const dto = { ...filterParams, oracle_address: address || '' };

return useInfiniteQuery({
initialPageParam: 0,
queryKey: ['myJobsInfinite', filterParams],
queryFn: () => getMyJobsTableData(filterParams),
queryKey: ['myJobsInfinite', dto],
queryFn: () => getMyJobsTableData(dto),
getNextPageParam: (pageParams) => {
return pageParams.total_pages === pageParams.page
return pageParams.total_pages - 1 <= pageParams.page
? undefined
: pageParams.page;
},
Expand Down
Loading

0 comments on commit 916c4a5

Please sign in to comment.