Skip to content

Commit

Permalink
feat(evm): add turnstile
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsimao committed Jan 28, 2025
1 parent e62cdc5 commit 97d2e12
Show file tree
Hide file tree
Showing 16 changed files with 225 additions and 70 deletions.
3 changes: 3 additions & 0 deletions apps/evm/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ SENTRY_AUTH_TOKEN=
# REDIS CACHE
KV_REST_API_URL="https://communal-hen-23354.upstash.io"
KV_REST_API_TOKEN=


NEXT_PUBLIC_TURNSTILE_SITE_KEY=
1 change: 1 addition & 0 deletions apps/evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@gobob/utils": "workspace:^",
"@lingui/core": "catalog:",
"@lingui/react": "catalog:",
"@marsidev/react-turnstile": "^1.1.0",
"@next/third-parties": "catalog:",
"@react-aria/button": "catalog:",
"@react-aria/focus": "catalog:",
Expand Down
3 changes: 2 additions & 1 deletion apps/evm/src/app/[lang]/nested-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useLocalStorage } from 'usehooks-ts';
import { isAddressEqual } from 'viem';
import { useAccount, useAccountEffect, useChainId, useConfig, useSwitchChain } from 'wagmi';

import { Header, Layout, Sidebar } from '@/components';
import { Header, Layout, Sidebar, TurnstileModal } from '@/components';
import { ConnectProvider } from '@/connect-ui';
import { isClient, L2_CHAIN, LocalStorageKey } from '@/constants';
import { useBalances, useGetUser, useLogout, useTokens } from '@/hooks';
Expand Down Expand Up @@ -147,6 +147,7 @@ export function NestedProviders({ children }: PropsWithChildren) {
<Header />
{children}
</Layout>
<TurnstileModal />
</ConnectProvider>
</BOBUIProvider>
</StyledComponentsRegistry>
Expand Down
39 changes: 36 additions & 3 deletions apps/evm/src/app/[lang]/sign-up/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useMutation } from '@tanstack/react-query';
import { useParams, useRouter } from 'next/navigation';
import { FormEventHandler, Suspense, useEffect, useState } from 'react';
import { useAccount, useSwitchChain } from 'wagmi';
import { useStore } from '@tanstack/react-store';

import { Auditors, HighlightText, ReferralInput } from './components';
import { StyledAuthCard, StyledH1 } from './SignUp.style';
Expand All @@ -18,6 +19,7 @@ import { L1_CHAIN, L2_CHAIN, RoutesPath, isValidChain } from '@/constants';
import { useGetUser, useSignUp } from '@/hooks';
import { fusionKeys } from '@/lib/react-query';
import { apiClient } from '@/utils';
import { store } from '@/lib/store';

const SignUp = (): JSX.Element | null => {
const { address, chain } = useAccount();
Expand All @@ -29,9 +31,20 @@ const SignUp = (): JSX.Element | null => {
const params = useParams();
const { data: user } = useGetUser();

const { token: turnstileToken } = useStore(store, (state) => state.shared.turnstile);

const [referralCode, setReferralCode] = useState('');

const { mutate: signUp, isPending: isLoadingSignUp } = useSignUp();
const { mutate: signUp, isPending: isLoadingSignUp } = useSignUp({
onSuccess: () =>
store.setState((s) => ({
...s,
shared: {
...s.shared,
turnstile: { isOpen: false }
}
}))
});

const {
mutateAsync: validateReferralCodeAsync,
Expand Down Expand Up @@ -73,7 +86,17 @@ const SignUp = (): JSX.Element | null => {
}
}

return signUp({ address });
if (!turnstileToken) {
return store.setState((s) => ({
...s,
shared: {
...s.shared,
turnstile: { isOpen: true, onSuccess: (token) => signUp({ address, turnstileToken: token }) }
}
}));
}

return signUp({ address, turnstileToken });
}
});
}
Expand All @@ -82,7 +105,17 @@ const SignUp = (): JSX.Element | null => {
await switchChainAsync({ chainId: L1_CHAIN });
}

return signUp({ address, referralCode });
if (!turnstileToken) {
return store.setState((s) => ({
...s,
shared: {
...s.shared,
turnstile: { isOpen: true, onSuccess: (token) => signUp({ address, turnstileToken: token }) }
}
}));
}

return signUp({ address, turnstileToken });
};

return (
Expand Down
39 changes: 36 additions & 3 deletions apps/evm/src/components/SignUpButton/SignUpButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Button, ButtonProps, toast } from '@gobob/ui';
import { Trans } from '@lingui/macro';
import { mergeProps } from '@react-aria/utils';
import { useAccount, useSwitchChain } from 'wagmi';
import { useStore } from '@tanstack/react-store';

import { useConnectModal } from '@/connect-ui';
import { L2_CHAIN, isValidChain } from '@/constants';
import { useSignUp } from '@/hooks';
import { store } from '@/lib/store';

type SignUpButtonProps = ButtonProps;

Expand All @@ -15,7 +17,18 @@ const SignUpButton = (props: SignUpButtonProps): JSX.Element => {
const { open } = useConnectModal();
const { address, chain } = useAccount();

const { mutate: signUp, isPending: isSigningUp } = useSignUp();
const { token: turnstileToken } = useStore(store, (state) => state.shared.turnstile);

const { mutate: signUp, isPending: isSigningUp } = useSignUp({
onSuccess: () =>
store.setState((s) => ({
...s,
shared: {
...s.shared,
turnstile: { isOpen: false }
}
}))
});

const handlePress = async () => {
if (!address) {
Expand All @@ -30,7 +43,17 @@ const SignUpButton = (props: SignUpButtonProps): JSX.Element => {
}
}

return signUp({ address });
if (!turnstileToken) {
return store.setState((s) => ({
...s,
shared: {
...s.shared,
turnstile: { isOpen: true, onSuccess: (token) => signUp({ address, turnstileToken: token }) }
}
}));
}

return signUp({ address, turnstileToken });
}
});
}
Expand All @@ -39,7 +62,17 @@ const SignUpButton = (props: SignUpButtonProps): JSX.Element => {
await switchChainAsync({ chainId: L2_CHAIN });
}

return signUp({ address });
if (!turnstileToken) {
return store.setState((s) => ({
...s,
shared: {
...s.shared,
turnstile: { isOpen: true, onSuccess: (token) => signUp({ address, turnstileToken: token }) }
}
}));
}

return signUp({ address, turnstileToken });
};

return <Button loading={isSigningUp} {...mergeProps(props, { onPress: handlePress })} />;
Expand Down
32 changes: 32 additions & 0 deletions apps/evm/src/components/TurnstileModal/TurnstileModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Modal, ModalBody, ModalHeader } from '@gobob/ui';
import { useStore } from '@tanstack/react-store';
import { Turnstile } from '@marsidev/react-turnstile';
import { Trans } from '@lingui/macro';

import { store } from '@/lib/store';

const TurnstileModal = () => {
const { isOpen, onSuccess } = useStore(store, (state) => state.shared.turnstile);

const handleSuccess = (token: string) => {
onSuccess?.(token);

store.setState((s) => ({ ...s, shared: { ...s.shared, turnstile: { isOpen: false, token } } }));
};

return (
<Modal
isOpen={isOpen}
onClose={() => store.setState((s) => ({ ...s, shared: { ...s.shared, turnstile: { isOpen: false } } }))}
>
<ModalHeader>
<Trans>Verify You&apos;re Human</Trans>
</ModalHeader>
<ModalBody padding='even'>
<Turnstile siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY} onSuccess={handleSuccess} />
</ModalBody>
</Modal>
);
};

export { TurnstileModal };
1 change: 1 addition & 0 deletions apps/evm/src/components/TurnstileModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TurnstileModal } from './TurnstileModal';
1 change: 1 addition & 0 deletions apps/evm/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './SignUpButton';
export * from './SpiceAmount';
export * from './Trapezoid';
export * from './WithdrawAlert';
export * from './TurnstileModal';
9 changes: 5 additions & 4 deletions apps/evm/src/hooks/tests/useSignUp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('useSignUp', () => {
it('calls refetchUser on successful sign-up', async () => {
vi.useFakeTimers();
const mockAddress = '0x123';
const mockTurnstileToken = '0x12345';
const mockChainId = 1;
const mockNonce = 'mock-nonce';
const mockRefetchUser = vi.fn();
Expand All @@ -69,13 +70,13 @@ describe('useSignUp', () => {
wrapper
});

await act(() => result.current.mutate({ address: mockAddress }));
await act(() => result.current.mutate({ address: mockAddress, turnstileToken: mockTurnstileToken }));

vi.runAllTimers();

expect(apiClient.getNonce).toHaveBeenCalled();
expect(mockSignMessageAsync).toHaveBeenCalledWith({ message: 'Message for 0x123' });
expect(apiClient.signUp).toHaveBeenCalledWith(expect.any(Object), 'mock-signature');
expect(apiClient.signUp).toHaveBeenCalledWith(expect.any(Object), mockTurnstileToken, 'mock-signature');
expect(mockRefetchUser).toHaveBeenCalled();
});

Expand All @@ -91,7 +92,7 @@ describe('useSignUp', () => {
wrapper
});

await act(() => result.current.mutate({ address: mockAddress }));
await act(() => result.current.mutate({ address: mockAddress, turnstileToken: '' }));

expect(toast.error).toHaveBeenCalledWith('User rejected the request');
});
Expand All @@ -108,7 +109,7 @@ describe('useSignUp', () => {
wrapper
});

await act(() => result.current.mutate({ address: mockAddress }));
await act(() => result.current.mutate({ address: mockAddress, turnstileToken: '' }));

expect(toast.error).toHaveBeenCalledWith('Network error');
});
Expand Down
18 changes: 15 additions & 3 deletions apps/evm/src/hooks/useSignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { useGetUser } from './useGetUser';
import { fusionKeys } from '@/lib/react-query';
import { apiClient } from '@/utils';

const useSignUp = () => {
type UseSignUpProps = {
onSuccess?: () => void;
};

const useSignUp = ({ onSuccess }: UseSignUpProps = {}) => {
const { signMessageAsync } = useSignMessage();
const { chain } = useAccount();

Expand All @@ -22,7 +26,14 @@ const useSignUp = () => {

return useMutation({
mutationKey: fusionKeys.signUp(),
mutationFn: async ({ address }: { address: Address; referralCode?: string }) => {
mutationFn: async ({
address,
turnstileToken
}: {
address: Address;
turnstileToken: string;
referralCode?: string;
}) => {
const nonce = await apiClient.getNonce();

const message = new SiweMessage({
Expand All @@ -39,10 +50,11 @@ const useSignUp = () => {
message: message.prepareMessage()
});

await apiClient.signUp(message, signature);
await apiClient.signUp(message, turnstileToken, signature);
},
onSuccess: (_, { address, referralCode }) => {
sendGAEvent('event', 'signup', { evm_address: JSON.stringify(address), referral_code: referralCode });
onSuccess?.();
setTimeout(() => refetchUser(), 100);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
14 changes: 14 additions & 0 deletions apps/evm/src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { Store as StoreLib } from '@tanstack/react-store';

import { BridgeTransaction } from '../types';

type SharedStore = {
turnstile: {
isOpen: boolean;
token?: string;
onSuccess?: (token: string) => void;
};
};

type BridgeStore = {
transactions: {
isInitialLoading: boolean;
Expand All @@ -18,11 +26,17 @@ type StrategiesStore = {
};

type Store = {
shared: SharedStore;
bridge: BridgeStore;
strategies: StrategiesStore;
};

const store = new StoreLib<Store>({
shared: {
turnstile: {
isOpen: false
}
},
bridge: {
transactions: {
isInitialLoading: true,
Expand Down
Loading

0 comments on commit 97d2e12

Please sign in to comment.