From e003ce972769060c8abca630230d89d33f0f194a Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 9 Oct 2024 20:03:42 -0300 Subject: [PATCH 01/12] Created captcha schema --- src/schemas/captcha.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/schemas/captcha.ts diff --git a/src/schemas/captcha.ts b/src/schemas/captcha.ts new file mode 100644 index 000000000..3b6174d28 --- /dev/null +++ b/src/schemas/captcha.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +const captchaSchema = z.object({ + bg: z.string().catch(''), + created_at: z.string().catch(''), + expires_at: z.string().catch(''), + id: z.string().catch(''), + puzzle: z.string().catch(''), + type: z.string().catch(''), +}); + +type CaptchaData = z.infer; + +export { captchaSchema, type CaptchaData }; \ No newline at end of file From 3076a711d117832ff1081213a9b81d72f4c9fde6 Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 9 Oct 2024 20:03:53 -0300 Subject: [PATCH 02/12] Add new messages --- src/locales/en.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/locales/en.json b/src/locales/en.json index 0accc3497..deeb4c619 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1176,6 +1176,16 @@ "nostr_relays.title": "Relays", "nostr_relays.write_only": "Write-only", "nostr_signin.siwe.welcome": "Welcome to {site_title}", + "nostr_signup.captcha_check_button": "Check", + "nostr_signup.captcha_check_button.checking": "Checking…", + "nostr_signup.captcha_instruction": "Complete the puzzle by dragging the puzzle piece to the correct position.", + "nostr_signup.captcha_message.error": "It seems an error has occurred. Please try again. If the problem persists, please contact us.", + "nostr_signup.captcha_message.misbehaving": "It looks like we're experiencing issues with the {instance}. Please try again. If the error persists, try again later.", + "nostr_signup.captcha_message.sucess": "Incredible! You've successfully completed the captcha. Let's move on to the next step!", + "nostr_signup.captcha_message.wrong": "Oops! It looks like your captcha response was incorrect. Please try again.", + "nostr_signup.captcha_reset_button": "Reset puzzle", + "nostr_signup.captcha_title": "Human Verification", + "nostr_signup.captcha_try_again_button": "Try again", "nostr_signup.has_key": "I already have a key", "nostr_signup.key-add.key_button": "Add Key", "nostr_signup.key-add.title": "Import Key", From e787da4e02b17e523ada85ec079df49a325b9cb3 Mon Sep 17 00:00:00 2001 From: danidfra Date: Wed, 9 Oct 2024 20:04:18 -0300 Subject: [PATCH 03/12] Implement Captcha --- src/features/ui/components/modal-root.tsx | 2 ++ .../modals/nostr-signup-modal/steps/keygen-step.tsx | 5 ++--- src/features/ui/util/async-components.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/ui/components/modal-root.tsx b/src/features/ui/components/modal-root.tsx index 890ad9c08..b7d27472d 100644 --- a/src/features/ui/components/modal-root.tsx +++ b/src/features/ui/components/modal-root.tsx @@ -46,6 +46,7 @@ import { ZapSplitModal, ZapInvoiceModal, ZapsModal, + CaptchaModal, } from 'soapbox/features/ui/util/async-components'; import ModalLoading from './modal-loading'; @@ -56,6 +57,7 @@ const MODAL_COMPONENTS: Record> = { 'ACTIONS': ActionsModal, 'BIRTHDAYS': BirthdaysModal, 'BOOST': BoostModal, + 'CAPTCHA': CaptchaModal, 'COMPARE_HISTORY': CompareHistoryModal, 'COMPONENT': ComponentModal, 'COMPOSE': ComposeModal, diff --git a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx index f4692f05b..b923c8aaf 100644 --- a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx +++ b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { fetchAccount } from 'soapbox/actions/accounts'; +import { openModal } from 'soapbox/actions/modals'; import { logInNostr } from 'soapbox/actions/nostr'; -import { startOnboarding } from 'soapbox/actions/onboarding'; import CopyableInput from 'soapbox/components/copyable-input'; import EmojiGraphic from 'soapbox/components/emoji-graphic'; import { Button, Stack, Modal, FormGroup, Text, Tooltip, HStack } from 'soapbox/components/ui'; @@ -62,9 +62,9 @@ const KeygenStep: React.FC = ({ onClose }) => { await Promise.all(events.map((event) => relay?.event(event))); await dispatch(logInNostr(pubkey)); - dispatch(startOnboarding()); onClose(); + await dispatch(openModal('CAPTCHA')); }; return ( @@ -82,7 +82,6 @@ const KeygenStep: React.FC = ({ onClose }) => { - + + + + ); +}; + +export default Captcha; \ No newline at end of file diff --git a/src/features/ui/components/modals/captcha-modal/puzzle.tsx b/src/features/ui/components/modals/captcha-modal/puzzle.tsx new file mode 100644 index 000000000..1d44cc9d1 --- /dev/null +++ b/src/features/ui/components/modals/captcha-modal/puzzle.tsx @@ -0,0 +1,109 @@ +import React, { useRef, useState } from 'react'; + +interface IPuzzleCaptcha { + bg: string; + puzzle: string; + position: { x: number; y: number }; + onChange(point: { x: number; y: number }): void; +} + +export const PuzzleCaptcha: React.FC = ({ bg, puzzle, position, onChange }) => { + const ref = useRef(null); + const [touchOffsetX, setTouchOffsetX] = useState(0); + const [touchOffsetY, setTouchOffsetY] = useState(0); + + const handlePointerDown = (e: React.PointerEvent) => { + e.currentTarget.setPointerCapture(e.pointerId); + }; + + const handlePointerMove = (e: React.PointerEvent) => { + if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; + + const rect = ref.current?.getBoundingClientRect(); + if (!rect) return; + + const newPosition = { + x: e.clientX - rect.left - e.currentTarget.width / 2, + y: e.clientY - rect.top - e.currentTarget.height / 2, + }; + + onChange(newPosition); + }; + + const handleTouchStart = (event: React.TouchEvent) => { + const imageElement = event.currentTarget.getBoundingClientRect(); + const touch = event.touches[0]; + + const offsetX = touch.clientX - imageElement.left; + const offsetY = touch.clientY - imageElement.top; + + setTouchOffsetX(offsetX); + setTouchOffsetY(offsetY); + }; + + + const handleTouchMove = (event: React.TouchEvent) => { + const touch = event.touches[0]; + + const dropArea = document.getElementById('drop-area')?.getBoundingClientRect(); + if (!dropArea) return; + + const newLeft = touch.clientX - dropArea.left - touchOffsetX; + const newTop = touch.clientY - dropArea.top - touchOffsetY; + + const imageWidth = 61; + const imageHeight = 61; + const limitedLeft = Math.min(Math.max(newLeft, 0), dropArea.width - imageWidth); + const limitedTop = Math.min(Math.max(newTop, 0), dropArea.height - imageHeight); + + onChange({ x: limitedLeft, y: limitedTop }); + }; + + const handlePointerUp = (e: React.PointerEvent) => { + e.currentTarget.releasePointerCapture(e.pointerId); + }; + + const handleTouchDrop = (event: React.TouchEvent) => { + event.preventDefault(); + + const dropArea = document.getElementById('drop-area')?.getBoundingClientRect(); + if (!dropArea) return; + + const touch = event.changedTouches[0]; + + const newLeft = touch.clientX - dropArea.left - touchOffsetX; + const newTop = touch.clientY - dropArea.top - touchOffsetY; + + const imageWidth = 61; + const imageHeight = 61; + + const limitedLeft = Math.min(Math.max(newLeft, 0), dropArea.width - imageWidth); + const limitedTop = Math.min(Math.max(newTop, 0), dropArea.height - imageHeight); + + onChange({ x: limitedLeft, y: limitedTop }); + + }; + + return ( +
+ + + +
+ ); +}; diff --git a/src/features/ui/components/modals/captcha-modal/useCaptcha.ts b/src/features/ui/components/modals/captcha-modal/useCaptcha.ts new file mode 100644 index 000000000..8f891e6c9 --- /dev/null +++ b/src/features/ui/components/modals/captcha-modal/useCaptcha.ts @@ -0,0 +1,114 @@ +import { AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { closeModal } from 'soapbox/actions/modals'; +import { startOnboarding } from 'soapbox/actions/onboarding'; +import { useApi, useAppDispatch, useInstance } from 'soapbox/hooks'; +import { captchaSchema, type CaptchaData } from 'soapbox/schemas/captcha'; +import toast from 'soapbox/toast'; + + + +const messages = defineMessages({ + sucessMessage: { id: 'nostr_signup.captcha_message.sucess', defaultMessage: 'Incredible! You\'ve successfully completed the captcha. Let\'s move on to the next step!' }, + wrongMessage: { id: 'nostr_signup.captcha_message.wrong', defaultMessage: 'Oops! It looks like your captcha response was incorrect. Please try again.' }, + errorMessage: { id: 'nostr_signup.captcha_message.error', defaultMessage: 'It seems an error has occurred. Please try again. If the problem persists, please contact us.' }, + misbehavingMessage: { id: 'nostr_signup.captcha_message.misbehaving', defaultMessage: 'It looks like we\'re experiencing issues with the {instance}. Please try again. If the error persists, try again later.' }, +}); + +function getRandomNumber(min: number, max: number): number { + return Number((Math.random() * (max - min) + min).toFixed()); +} + +const useCaptcha = () => { + const api = useApi(); + const instance = useInstance(); + const dispatch = useAppDispatch(); + const intl = useIntl(); + const [captcha, setCaptcha] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [tryAgain, setTryAgain] = useState(false); + const [yPosition, setYPosition] = useState(); + const [xPosition, setXPosition] = useState(); + + const loadCaptcha = async () => { + try { + const topI = getRandomNumber(0, (356 - 61)); + const leftI = getRandomNumber(0, (330 - 61)); + const { data } = await api.get('/api/v1/ditto/captcha'); + if (data) { + const normalizedData = captchaSchema.parse(data); + setCaptcha(normalizedData); + setYPosition(topI); + setXPosition(leftI); + } + + } catch (error) { + toast.error('Error loading captcha:'); + } + }; + + useEffect(() => { + loadCaptcha(); + }, []); + + const handleChangePosition = (point: { x: number; y: number }) => { + setXPosition(point.x); + setYPosition(point.y); + }; + + const handleSubmit = async () => { + setIsSubmitting(true); + + if (captcha) { + const result = { + x: xPosition, + y: yPosition, + }; + + try { + await api.post(`/api/v1/ditto/captcha/${captcha.id}/verify`, result).then(() => { + setTryAgain(true); + + dispatch(closeModal('CAPTCHA')); + toast.success(messages.sucessMessage); + dispatch(startOnboarding()); + }); + } catch (e) { + setTryAgain(true); + const error = e as AxiosError; + const status = error.request?.status; + + let message; + + switch (status) { + case 400: + message = intl.formatMessage(messages.wrongMessage); + break; + case 422: + message = intl.formatMessage(messages.misbehavingMessage, { instance: instance.title }); + break; + default: + message = intl.formatMessage(messages.errorMessage); + break; + } + toast.error(message); + } + setIsSubmitting(false); + } + }; + + return { + captcha, + loadCaptcha, + handleChangePosition, + handleSubmit, + isSubmitting, + tryAgain, + yPosition, + xPosition, + }; +}; + +export default useCaptcha; \ No newline at end of file From e040c9f98174aedff6abfc3c786c76f4dfe7ee64 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 14:13:46 -0300 Subject: [PATCH 05/12] Refactoring the puzzle and organizing files --- .../hooks/captcha}/useCaptcha.ts | 0 .../captcha-modal/components/puzzle.tsx | 64 ++++++++++ .../modals/captcha-modal/puzzle.tsx | 109 ------------------ 3 files changed, 64 insertions(+), 109 deletions(-) rename src/{features/ui/components/modals/captcha-modal => api/hooks/captcha}/useCaptcha.ts (100%) create mode 100644 src/features/ui/components/modals/captcha-modal/components/puzzle.tsx delete mode 100644 src/features/ui/components/modals/captcha-modal/puzzle.tsx diff --git a/src/features/ui/components/modals/captcha-modal/useCaptcha.ts b/src/api/hooks/captcha/useCaptcha.ts similarity index 100% rename from src/features/ui/components/modals/captcha-modal/useCaptcha.ts rename to src/api/hooks/captcha/useCaptcha.ts diff --git a/src/features/ui/components/modals/captcha-modal/components/puzzle.tsx b/src/features/ui/components/modals/captcha-modal/components/puzzle.tsx new file mode 100644 index 000000000..cb7303c6d --- /dev/null +++ b/src/features/ui/components/modals/captcha-modal/components/puzzle.tsx @@ -0,0 +1,64 @@ +import React, { useRef } from 'react'; + +interface IPuzzleCaptcha { + bg: string; + puzzle: string; + position: { x: number; y: number }; + onChange(point: { x: number; y: number }): void; +} + +export const PuzzleCaptcha: React.FC = ({ bg, puzzle, position, onChange }) => { + const ref = useRef(null); + + const calculateNewPosition = ( + clientX: number, + clientY: number, + elementWidth: number, + elementHeight: number, + dropArea: DOMRect, + ) => { + const newX = Math.min(Math.max(clientX - dropArea.left - elementWidth / 2, 0), dropArea.width - elementWidth); + const newY = Math.min(Math.max(clientY - dropArea.top - elementHeight / 2, 0), dropArea.height - elementHeight); + return { x: newX, y: newY }; + }; + + const handlePointerMove = (e: React.PointerEvent) => { + if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; + const dropArea = ref.current?.getBoundingClientRect(); + if (!dropArea) return; + + const newPosition = calculateNewPosition(e.clientX, e.clientY, e.currentTarget.width, e.currentTarget.height, dropArea); + onChange(newPosition); + }; + + const handleTouchMove = (event: React.TouchEvent) => { + const touch = event.touches[0]; + const dropArea = ref.current?.getBoundingClientRect(); + if (!dropArea) return; + + const newPosition = calculateNewPosition(touch.clientX, touch.clientY, 61, 61, dropArea); + onChange(newPosition); + }; + + return ( +
+ e.currentTarget.setPointerCapture(e.pointerId)} + onPointerMove={handlePointerMove} + onPointerUp={(e) => e.currentTarget.releasePointerCapture(e.pointerId)} + onTouchMove={handleTouchMove} + style={{ + filter: 'drop-shadow(2px 0 0 #fff) drop-shadow(0 2px 0 #fff) drop-shadow(-2px 0 0 #fff) drop-shadow(0 -2px 0 #fff)', + left: position.x, + top: position.y, + }} + draggable={false} + /> + + +
+ ); +}; diff --git a/src/features/ui/components/modals/captcha-modal/puzzle.tsx b/src/features/ui/components/modals/captcha-modal/puzzle.tsx deleted file mode 100644 index 1d44cc9d1..000000000 --- a/src/features/ui/components/modals/captcha-modal/puzzle.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useRef, useState } from 'react'; - -interface IPuzzleCaptcha { - bg: string; - puzzle: string; - position: { x: number; y: number }; - onChange(point: { x: number; y: number }): void; -} - -export const PuzzleCaptcha: React.FC = ({ bg, puzzle, position, onChange }) => { - const ref = useRef(null); - const [touchOffsetX, setTouchOffsetX] = useState(0); - const [touchOffsetY, setTouchOffsetY] = useState(0); - - const handlePointerDown = (e: React.PointerEvent) => { - e.currentTarget.setPointerCapture(e.pointerId); - }; - - const handlePointerMove = (e: React.PointerEvent) => { - if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; - - const rect = ref.current?.getBoundingClientRect(); - if (!rect) return; - - const newPosition = { - x: e.clientX - rect.left - e.currentTarget.width / 2, - y: e.clientY - rect.top - e.currentTarget.height / 2, - }; - - onChange(newPosition); - }; - - const handleTouchStart = (event: React.TouchEvent) => { - const imageElement = event.currentTarget.getBoundingClientRect(); - const touch = event.touches[0]; - - const offsetX = touch.clientX - imageElement.left; - const offsetY = touch.clientY - imageElement.top; - - setTouchOffsetX(offsetX); - setTouchOffsetY(offsetY); - }; - - - const handleTouchMove = (event: React.TouchEvent) => { - const touch = event.touches[0]; - - const dropArea = document.getElementById('drop-area')?.getBoundingClientRect(); - if (!dropArea) return; - - const newLeft = touch.clientX - dropArea.left - touchOffsetX; - const newTop = touch.clientY - dropArea.top - touchOffsetY; - - const imageWidth = 61; - const imageHeight = 61; - const limitedLeft = Math.min(Math.max(newLeft, 0), dropArea.width - imageWidth); - const limitedTop = Math.min(Math.max(newTop, 0), dropArea.height - imageHeight); - - onChange({ x: limitedLeft, y: limitedTop }); - }; - - const handlePointerUp = (e: React.PointerEvent) => { - e.currentTarget.releasePointerCapture(e.pointerId); - }; - - const handleTouchDrop = (event: React.TouchEvent) => { - event.preventDefault(); - - const dropArea = document.getElementById('drop-area')?.getBoundingClientRect(); - if (!dropArea) return; - - const touch = event.changedTouches[0]; - - const newLeft = touch.clientX - dropArea.left - touchOffsetX; - const newTop = touch.clientY - dropArea.top - touchOffsetY; - - const imageWidth = 61; - const imageHeight = 61; - - const limitedLeft = Math.min(Math.max(newLeft, 0), dropArea.width - imageWidth); - const limitedTop = Math.min(Math.max(newTop, 0), dropArea.height - imageHeight); - - onChange({ x: limitedLeft, y: limitedTop }); - - }; - - return ( -
- - - -
- ); -}; From 41dde9448f0c686dfaab287aac162db317fce6df Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 14:27:21 -0300 Subject: [PATCH 06/12] Refactoring and improving the onboarding trigger --- src/components/modal-root.tsx | 4 ++++ .../components/modals/captcha-modal/captcha-modal.tsx | 10 +++++++++- .../ui/components/modals/captcha-modal/captcha.tsx | 4 ++-- .../modals/nostr-signup-modal/steps/keygen-step.tsx | 8 ++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/modal-root.tsx b/src/components/modal-root.tsx index 84b7830e1..5afc25d4a 100644 --- a/src/components/modal-root.tsx +++ b/src/components/modal-root.tsx @@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom'; import { cancelReplyCompose } from 'soapbox/actions/compose'; import { cancelEventCompose } from 'soapbox/actions/events'; import { openModal, closeModal } from 'soapbox/actions/modals'; +import { startOnboarding } from 'soapbox/actions/onboarding'; import { useAppDispatch, usePrevious } from 'soapbox/hooks'; import type { ModalType } from 'soapbox/features/ui/components/modal-root'; @@ -111,6 +112,9 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) })); } else if ((hasComposeContent || hasEventComposeContent) && type === 'CONFIRM') { dispatch(closeModal('CONFIRM')); + } else if (type === 'CAPTCHA') { + dispatch(startOnboarding()); + onClose(); } else { onClose(); } diff --git a/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx b/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx index 9b1fcbc63..b6d010ad4 100644 --- a/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx +++ b/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; +import { startOnboarding } from 'soapbox/actions/onboarding'; import { Modal, Stack } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; import Captcha from './captcha'; @@ -10,8 +12,14 @@ interface ICaptchaModal { } const CaptchaModal: React.FC = ({ onClose }) => { + const dispatch = useAppDispatch(); return ( - } onClose={onClose} width='sm'> + } onClose={() => { + onClose(); + dispatch(startOnboarding()); + }} width='sm' + > diff --git a/src/features/ui/components/modals/captcha-modal/captcha.tsx b/src/features/ui/components/modals/captcha-modal/captcha.tsx index bc85d3f88..a11e01431 100644 --- a/src/features/ui/components/modals/captcha-modal/captcha.tsx +++ b/src/features/ui/components/modals/captcha-modal/captcha.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; +import useCaptcha from 'soapbox/api/hooks/captcha/useCaptcha'; import { Button, Spinner, Stack, Text } from 'soapbox/components/ui'; -import { PuzzleCaptcha } from './puzzle'; -import useCaptcha from './useCaptcha'; +import { PuzzleCaptcha } from './components/puzzle'; const Captcha: React.FC = () => { const { diff --git a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx index b923c8aaf..9c90a8a98 100644 --- a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx +++ b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx @@ -5,12 +5,14 @@ import { FormattedMessage } from 'react-intl'; import { fetchAccount } from 'soapbox/actions/accounts'; import { openModal } from 'soapbox/actions/modals'; import { logInNostr } from 'soapbox/actions/nostr'; +import { closeSidebar } from 'soapbox/actions/sidebar'; import CopyableInput from 'soapbox/components/copyable-input'; import EmojiGraphic from 'soapbox/components/emoji-graphic'; import { Button, Stack, Modal, FormGroup, Text, Tooltip, HStack } from 'soapbox/components/ui'; import { useNostr } from 'soapbox/contexts/nostr-context'; import { NKeys } from 'soapbox/features/nostr/keys'; import { useAppDispatch, useInstance } from 'soapbox/hooks'; +import { useIsMobile } from 'soapbox/hooks/useIsMobile'; import { download } from 'soapbox/utils/download'; import { slugify } from 'soapbox/utils/input'; @@ -21,6 +23,7 @@ interface IKeygenStep { const KeygenStep: React.FC = ({ onClose }) => { const instance = useInstance(); const dispatch = useAppDispatch(); + const isMobile = useIsMobile(); const { relay } = useNostr(); const secretKey = useMemo(() => generateSecretKey(), []); @@ -64,6 +67,11 @@ const KeygenStep: React.FC = ({ onClose }) => { await dispatch(logInNostr(pubkey)); onClose(); + + if (isMobile) { + dispatch(closeSidebar()); + } + await dispatch(openModal('CAPTCHA')); }; From 162b0042ed37f41cfbc99e3decdf48260cceed65 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 17:26:15 -0300 Subject: [PATCH 07/12] Update account schema --- src/schemas/account.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/schemas/account.ts b/src/schemas/account.ts index 1a6944386..86e5d7985 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -110,6 +110,8 @@ const baseAccountSchema = z.object({ nostr: z.object({ nip05: z.string().optional().catch(undefined), }).optional().catch(undefined), + ditto: z.object({ + captcha_solved: z.boolean().catch(true) }), }).optional().catch(undefined), statuses_count: z.number().catch(0), suspended: z.boolean().catch(false), From 3f6d589e9319814ded11d23e4fd374b6c766f8f7 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 17:28:56 -0300 Subject: [PATCH 08/12] Using captcha_solved --- src/api/hooks/captcha/useCaptcha.ts | 2 -- .../ui/components/modals/captcha-modal/captcha-modal.tsx | 8 +------- .../modals/nostr-signup-modal/steps/keygen-step.tsx | 2 -- src/init/soapbox-mount.tsx | 7 +++++++ 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/api/hooks/captcha/useCaptcha.ts b/src/api/hooks/captcha/useCaptcha.ts index 8f891e6c9..8085376db 100644 --- a/src/api/hooks/captcha/useCaptcha.ts +++ b/src/api/hooks/captcha/useCaptcha.ts @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { closeModal } from 'soapbox/actions/modals'; -import { startOnboarding } from 'soapbox/actions/onboarding'; import { useApi, useAppDispatch, useInstance } from 'soapbox/hooks'; import { captchaSchema, type CaptchaData } from 'soapbox/schemas/captcha'; import toast from 'soapbox/toast'; @@ -73,7 +72,6 @@ const useCaptcha = () => { dispatch(closeModal('CAPTCHA')); toast.success(messages.sucessMessage); - dispatch(startOnboarding()); }); } catch (e) { setTryAgain(true); diff --git a/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx b/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx index b6d010ad4..b84e78c04 100644 --- a/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx +++ b/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { startOnboarding } from 'soapbox/actions/onboarding'; import { Modal, Stack } from 'soapbox/components/ui'; -import { useAppDispatch } from 'soapbox/hooks'; import Captcha from './captcha'; @@ -12,13 +10,9 @@ interface ICaptchaModal { } const CaptchaModal: React.FC = ({ onClose }) => { - const dispatch = useAppDispatch(); return ( } onClose={() => { - onClose(); - dispatch(startOnboarding()); - }} width='sm' + title={} width='sm' > diff --git a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx index 9c90a8a98..1d6274ce8 100644 --- a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx +++ b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { fetchAccount } from 'soapbox/actions/accounts'; -import { openModal } from 'soapbox/actions/modals'; import { logInNostr } from 'soapbox/actions/nostr'; import { closeSidebar } from 'soapbox/actions/sidebar'; import CopyableInput from 'soapbox/components/copyable-input'; @@ -72,7 +71,6 @@ const KeygenStep: React.FC = ({ onClose }) => { dispatch(closeSidebar()); } - await dispatch(openModal('CAPTCHA')); }; return ( diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx index 1b7ba320e..49bed38cc 100644 --- a/src/init/soapbox-mount.tsx +++ b/src/init/soapbox-mount.tsx @@ -35,11 +35,18 @@ const SoapboxMount = () => { const soapboxConfig = useSoapboxConfig(); + const showCaptcha = account && !account?.source?.ditto.captcha_solved; const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const showOnboarding = account && needsOnboarding; + + if (showCaptcha) { + dispatch(openModal('CAPTCHA')); + } + if (showOnboarding) { dispatch(openModal('ONBOARDING_FLOW')); } + const { redirectRootNoLogin, gdpr } = soapboxConfig; // @ts-ignore: I don't actually know what these should be, lol From ea19016ca3e8de690f6ce67bef9572775625ec8e Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 17:36:10 -0300 Subject: [PATCH 09/12] Merged captcha and captcha-modal --- .../modals/captcha-modal/captcha-modal.tsx | 46 ++++++++++++++-- .../modals/captcha-modal/captcha.tsx | 54 ------------------- 2 files changed, 43 insertions(+), 57 deletions(-) delete mode 100644 src/features/ui/components/modals/captcha-modal/captcha.tsx diff --git a/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx b/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx index b84e78c04..5dac02d2d 100644 --- a/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx +++ b/src/features/ui/components/modals/captcha-modal/captcha-modal.tsx @@ -1,21 +1,61 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Modal, Stack } from 'soapbox/components/ui'; +import useCaptcha from 'soapbox/api/hooks/captcha/useCaptcha'; +import { Modal, Button, Spinner, Stack, Text } from 'soapbox/components/ui'; -import Captcha from './captcha'; +import { PuzzleCaptcha } from './components/puzzle'; interface ICaptchaModal { onClose: (type?: string) => void; } const CaptchaModal: React.FC = ({ onClose }) => { + const { + captcha, + loadCaptcha, + handleChangePosition, + handleSubmit, + isSubmitting, + tryAgain, + yPosition, + xPosition, + } = useCaptcha(); + return ( } width='sm' > - + + + + + +
+ {captcha ? : } +
+ + + + + +
); diff --git a/src/features/ui/components/modals/captcha-modal/captcha.tsx b/src/features/ui/components/modals/captcha-modal/captcha.tsx deleted file mode 100644 index a11e01431..000000000 --- a/src/features/ui/components/modals/captcha-modal/captcha.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -import useCaptcha from 'soapbox/api/hooks/captcha/useCaptcha'; -import { Button, Spinner, Stack, Text } from 'soapbox/components/ui'; - -import { PuzzleCaptcha } from './components/puzzle'; - -const Captcha: React.FC = () => { - const { - captcha, - loadCaptcha, - handleChangePosition, - handleSubmit, - isSubmitting, - tryAgain, - yPosition, - xPosition, - } = useCaptcha(); - - return ( - - - - - -
- {captcha ? : } -
- - - - - -
- ); -}; - -export default Captcha; \ No newline at end of file From bf16faccf099acdcd0c2556deab30cb1ba10183b Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 17:40:54 -0300 Subject: [PATCH 10/12] Update the captcha modal to not close on outside clicks --- src/components/modal-root.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/modal-root.tsx b/src/components/modal-root.tsx index 5afc25d4a..0612b90ad 100644 --- a/src/components/modal-root.tsx +++ b/src/components/modal-root.tsx @@ -6,7 +6,6 @@ import { useHistory } from 'react-router-dom'; import { cancelReplyCompose } from 'soapbox/actions/compose'; import { cancelEventCompose } from 'soapbox/actions/events'; import { openModal, closeModal } from 'soapbox/actions/modals'; -import { startOnboarding } from 'soapbox/actions/onboarding'; import { useAppDispatch, usePrevious } from 'soapbox/hooks'; import type { ModalType } from 'soapbox/features/ui/components/modal-root'; @@ -113,8 +112,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) } else if ((hasComposeContent || hasEventComposeContent) && type === 'CONFIRM') { dispatch(closeModal('CONFIRM')); } else if (type === 'CAPTCHA') { - dispatch(startOnboarding()); - onClose(); + return; } else { onClose(); } From 130d4b149b37eb2a88a2f42c83c46c447820ac3c Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 17:46:38 -0300 Subject: [PATCH 11/12] Add useEffect on showCaptcha --- src/init/soapbox-mount.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx index 49bed38cc..ad187beec 100644 --- a/src/init/soapbox-mount.tsx +++ b/src/init/soapbox-mount.tsx @@ -1,4 +1,4 @@ -import React, { Suspense } from 'react'; +import React, { Suspense, useEffect } from 'react'; import { Toaster } from 'react-hot-toast'; import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom'; import { CompatRouter } from 'react-router-dom-v5-compat'; @@ -39,9 +39,11 @@ const SoapboxMount = () => { const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const showOnboarding = account && needsOnboarding; - if (showCaptcha) { - dispatch(openModal('CAPTCHA')); - } + useEffect(() => { + if (showCaptcha) { + dispatch(openModal('CAPTCHA')); + } + }, [showCaptcha]); if (showOnboarding) { dispatch(openModal('ONBOARDING_FLOW')); From 0d58e68911e42473e715d7375c1e383f4aa74806 Mon Sep 17 00:00:00 2001 From: danidfra Date: Thu, 10 Oct 2024 17:50:01 -0300 Subject: [PATCH 12/12] Last changes --- src/init/soapbox-mount.tsx | 2 +- src/schemas/account.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx index ad187beec..d91c71c3e 100644 --- a/src/init/soapbox-mount.tsx +++ b/src/init/soapbox-mount.tsx @@ -35,7 +35,7 @@ const SoapboxMount = () => { const soapboxConfig = useSoapboxConfig(); - const showCaptcha = account && !account?.source?.ditto.captcha_solved; + const showCaptcha = account && account?.source?.ditto.captcha_solved === false; const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const showOnboarding = account && needsOnboarding; diff --git a/src/schemas/account.ts b/src/schemas/account.ts index 86e5d7985..702b631b0 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -110,8 +110,9 @@ const baseAccountSchema = z.object({ nostr: z.object({ nip05: z.string().optional().catch(undefined), }).optional().catch(undefined), - ditto: z.object({ - captcha_solved: z.boolean().catch(true) }), + ditto: coerceObject({ + captcha_solved: z.boolean().catch(true), + }), }).optional().catch(undefined), statuses_count: z.number().catch(0), suspended: z.boolean().catch(false),