Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 129 additions & 155 deletions src/components/challenge-modal/challenge-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ import useTheme from '../../hooks/use-theme';
import styles from './challenge-modal.module.css';
import { getPublicationPreview, getPublicationType, getVotePreview } from '../../lib/utils/challenge-utils';

interface ChallengeProps {
challenge?: ChallengeType;
iframeUrl?: string | null;
publication?: any;
closeModal: () => void;
}

interface ChallengeHeaderProps {
publicationType: string | null;
votePreview: string;
Expand Down Expand Up @@ -51,6 +44,8 @@ interface RegularChallengeContentProps {

const RegularChallengeContent = ({ challenge, closeModal }: RegularChallengeContentProps) => {
const { t } = useTranslation();
const account = useAccount();
const [theme] = useTheme();
const challenges = challenge?.[0]?.challenges;
const publicationTarget = challenge?.[2];
const publicationType = getPublicationType(challenge?.[1]);
Expand All @@ -62,10 +57,15 @@ const RegularChallengeContent = ({ challenge, closeModal }: RegularChallengeCont

const [currentChallengeIndex, setCurrentChallengeIndex] = useState(0);
const [answers, setAnswers] = useState<string[]>([]);
const [showIframeConfirmation, setShowIframeConfirmation] = useState(true);
const [iframeUrlState, setIframeUrl] = useState<string>('');
const [iframeOrigin, setIframeOrigin] = useState<string>('');
const iframeRef = useRef<HTMLIFrameElement>(null);

const currentChallenge = challenges?.[currentChallengeIndex];
const isTextChallenge = currentChallenge?.type === 'text/plain';
const isImageChallenge = currentChallenge?.type === 'image/png';
const isIframeChallenge = currentChallenge?.type === 'text/url-iframe';

const isValidAnswer = (index: number) => {
return !!answers[index] && answers[index].trim() !== '';
Expand All @@ -79,11 +79,17 @@ const RegularChallengeContent = ({ challenge, closeModal }: RegularChallengeCont
});
};

const onSubmit = () => {
const onSubmit = useCallback(() => {
challenge[1].publishChallengeAnswers(answers);
setAnswers([]);
closeModal();
};
}, [challenge, answers, closeModal]);

const onIframeClose = useCallback(() => {
// Submit empty string as answer for iframe challenges
challenge[1].publishChallengeAnswers(['']);
closeModal();
}, [challenge, closeModal]);

const onEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter') return;
Expand All @@ -95,87 +101,10 @@ const RegularChallengeContent = ({ challenge, closeModal }: RegularChallengeCont
}
};

const subplebbit = shortSubplebbitAddress || subplebbitAddress;

return (
<>
<ChallengeHeader
publicationType={publicationType ?? null}
votePreview={votePreview}
parentCid={parentCid}
parentAddress={parentAddress}
publicationContent={publicationContent}
subplebbit={subplebbit}
/>
<div className={styles.challengeMediaWrapper}>
{isTextChallenge && <div className={styles.challengeMedia}>{currentChallenge?.challenge}</div>}
{isImageChallenge && <img alt={t('loading')} className={styles.challengeMedia} src={`data:image/png;base64,${currentChallenge?.challenge}`} />}
</div>
<div>
<input
onKeyDown={onEnterKey}
onChange={onAnswersChange}
value={answers[currentChallengeIndex] || ''}
className={styles.challengeInput}
autoCorrect='off'
autoComplete='off'
spellCheck='false'
/>
</div>
<div className={styles.challengeFooter}>
<div className={styles.counter}>{t('challenge_counter', { index: currentChallengeIndex + 1, total: challenges?.length })}</div>
<span className={styles.buttons}>
{!challenges?.[currentChallengeIndex + 1] && (
<button onClick={onSubmit} disabled={!isValidAnswer(currentChallengeIndex)}>
{t('submit')}
</button>
)}
<button onClick={closeModal}>{t('cancel')}</button>
{challenges && challenges.length > 1 && (
<button disabled={!challenges[currentChallengeIndex - 1]} onClick={() => setCurrentChallengeIndex((prev) => prev - 1)}>
{t('previous')}
</button>
)}
{challenges?.[currentChallengeIndex + 1] && <button onClick={() => setCurrentChallengeIndex((prev) => prev + 1)}>{t('next')}</button>}
</span>
</div>
</>
);
};

interface IframeChallengeContentProps {
iframeUrl: string;
publication: any;
closeModal: () => void;
}

const IframeChallengeContent = ({ iframeUrl, publication, closeModal }: IframeChallengeContentProps) => {
const { t } = useTranslation();
const account = useAccount();
const [theme] = useTheme();
const [showConfirmation, setShowConfirmation] = useState(true);
const [iframeUrlState, setIframeUrl] = useState<string>('');
const [iframeOrigin, setIframeOrigin] = useState<string>('');
const iframeRef = useRef<HTMLIFrameElement>(null);

const publicationType = getPublicationType(publication);
const publicationContent = getPublicationPreview(publication);
const votePreview = getVotePreview(publication);
const { shortSubplebbitAddress, subplebbitAddress, parentCid } = publication || {};
const parentComment = useComment({ commentCid: parentCid, onlyIfCached: true });
const parentAddress = parentComment?.author?.shortAddress || '';

useEffect(() => {
const onEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
document.addEventListener('keydown', onEscapeKey);
return () => document.removeEventListener('keydown', onEscapeKey);
}, [closeModal]);

const handleLoadIframe = () => {
const iframeUrl = currentChallenge?.challenge;
if (!iframeUrl) return;

const rawUserAddress = account?.author?.address?.trim();
const requiresUserAddress = iframeUrl.includes('{userAddress}');

Expand Down Expand Up @@ -204,7 +133,7 @@ const IframeChallengeContent = ({ iframeUrl, publication, closeModal }: IframeCh
const finalUrl = validatedUrl.toString();
setIframeUrl(finalUrl);
setIframeOrigin(validatedUrl.origin);
setShowConfirmation(false);
setShowIframeConfirmation(false);
} catch (error) {
console.error('Invalid iframe challenge URL', { error });
alert('Error: Invalid URL for authentication challenge');
Expand Down Expand Up @@ -236,13 +165,83 @@ const IframeChallengeContent = ({ iframeUrl, publication, closeModal }: IframeCh
};

useEffect(() => {
if (iframeRef.current && iframeUrlState && iframeOrigin && !showConfirmation) {
if (iframeRef.current && iframeUrlState && iframeOrigin && !showIframeConfirmation) {
sendThemeToIframe();
}
}, [iframeOrigin, iframeUrlState, sendThemeToIframe, showConfirmation]);
}, [iframeOrigin, iframeUrlState, sendThemeToIframe, showIframeConfirmation]);

useEffect(() => {
const onEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (isIframeChallenge) {
onIframeClose();
} else {
closeModal();
}
}
};
document.addEventListener('keydown', onEscapeKey);
return () => document.removeEventListener('keydown', onEscapeKey);
}, [isIframeChallenge, onIframeClose, closeModal]);

const subplebbit = shortSubplebbitAddress || subplebbitAddress;

// Render iframe challenge
if (isIframeChallenge) {
return (
<>
<ChallengeHeader
publicationType={publicationType ?? null}
votePreview={votePreview}
parentCid={parentCid}
parentAddress={parentAddress}
publicationContent={publicationContent}
subplebbit={subplebbit}
/>

{showIframeConfirmation ? (
<>
<div className={styles.challengeMediaWrapper}>
<div className={`${styles.challengeMedia} ${styles.iframeChallengeWarning}`}>
{t('iframe_challenge_warning', {
defaultValue: 'This challenge requires loading an external website. Loading it will reveal your IP address to that website. Do you want to continue?',
})}
</div>
</div>
<div className={styles.challengeFooter}>
<span className={styles.buttons}>
<button onClick={handleLoadIframe}>{t('load_external_resource', { defaultValue: 'load' })}</button>
<button onClick={closeModal}>{t('cancel')}</button>
</span>
</div>
</>
) : (
<>
<div className={`${styles.challengeMediaWrapper} ${styles.iframeWrapper}`}>
<iframe
ref={iframeRef}
src={iframeUrlState}
sandbox='allow-scripts allow-forms allow-popups allow-top-navigation-by-user-activation'
onLoad={handleIframeLoad}
className={styles.iframe}
title={t('challenge_iframe', { defaultValue: 'Challenge authentication' })}
/>
</div>
<div className={`${styles.challengeFooter} ${styles.iframeFooter}`}>
<div className={styles.iframeInstruction}>
{t('iframe_challenge_keep_open', { defaultValue: 'Complete the challenge in the box above. Keep this window open until done.' })}
</div>
<div className={styles.iframeCloseButton}>
<button onClick={onIframeClose}>{t('done', { defaultValue: 'done' })}</button>
</div>
</div>
</>
)}
</>
);
}

// Render regular text/image challenges
return (
<>
<ChallengeHeader
Expand All @@ -253,76 +252,56 @@ const IframeChallengeContent = ({ iframeUrl, publication, closeModal }: IframeCh
publicationContent={publicationContent}
subplebbit={subplebbit}
/>

{showConfirmation ? (
<>
<div className={styles.challengeMediaWrapper}>
<div className={`${styles.challengeMedia} ${styles.iframeChallengeWarning}`}>
{t('iframe_challenge_warning', {
defaultValue: 'This challenge requires loading an external website. Loading it will reveal your IP address to that website. Do you want to continue?',
})}
</div>
</div>
<div className={styles.challengeFooter}>
<span className={styles.buttons}>
<button onClick={handleLoadIframe}>{t('load_external_resource', { defaultValue: 'load' })}</button>
<button onClick={closeModal}>{t('cancel')}</button>
</span>
</div>
</>
) : (
<>
<div className={`${styles.challengeMediaWrapper} ${styles.iframeWrapper}`}>
<iframe
ref={iframeRef}
src={iframeUrlState}
sandbox='allow-scripts allow-forms allow-popups allow-top-navigation-by-user-activation'
onLoad={handleIframeLoad}
className={styles.iframe}
title={t('challenge_iframe', { defaultValue: 'Challenge authentication' })}
/>
</div>
<div className={`${styles.challengeFooter} ${styles.iframeFooter}`}>
<div className={styles.iframeInstruction}>
{t('iframe_challenge_keep_open', { defaultValue: 'Complete the challenge in the box above. Keep this window open until done.' })}
</div>
<div className={styles.iframeCloseButton}>
<button onClick={closeModal}>{t('close', { defaultValue: 'close' })}</button>
</div>
</div>
</>
)}
<div className={styles.challengeMediaWrapper}>
{isTextChallenge && <div className={styles.challengeMedia}>{currentChallenge?.challenge}</div>}
{isImageChallenge && <img alt={t('loading')} className={styles.challengeMedia} src={`data:image/png;base64,${currentChallenge?.challenge}`} />}
</div>
<div>
<input
onKeyDown={onEnterKey}
onChange={onAnswersChange}
value={answers[currentChallengeIndex] || ''}
className={styles.challengeInput}
autoCorrect='off'
autoComplete='off'
spellCheck='false'
/>
</div>
<div className={styles.challengeFooter}>
<div className={styles.counter}>{t('challenge_counter', { index: currentChallengeIndex + 1, total: challenges?.length })}</div>
<span className={styles.buttons}>
{!challenges?.[currentChallengeIndex + 1] && (
<button onClick={onSubmit} disabled={!isValidAnswer(currentChallengeIndex)}>
{t('submit')}
</button>
)}
<button onClick={closeModal}>{t('cancel')}</button>
{challenges && challenges.length > 1 && (
<button disabled={!challenges[currentChallengeIndex - 1]} onClick={() => setCurrentChallengeIndex((prev) => prev - 1)}>
{t('previous')}
</button>
)}
{challenges?.[currentChallengeIndex + 1] && <button onClick={() => setCurrentChallengeIndex((prev) => prev + 1)}>{t('next')}</button>}
</span>
</div>
</>
);
};

const ChallengeContent = ({ challenge, iframeUrl, publication, closeModal }: ChallengeProps) => {
if (challenge && !iframeUrl) {
const ChallengeContent = ({ challenge, closeModal }: { challenge?: ChallengeType; closeModal: () => void }) => {
if (challenge) {
return <RegularChallengeContent challenge={challenge} closeModal={closeModal} />;
}

if (iframeUrl && publication) {
return <IframeChallengeContent iframeUrl={iframeUrl} publication={publication} closeModal={closeModal} />;
}

return null;
};

const ChallengeModal = () => {
const { challenges, removeChallenge, iframeModalOpen, iframeModalUrl, iframeModalPublication, closeIframeModal } = useChallengesStore();
const { challenges, removeChallenge } = useChallengesStore();

// Determine which modal should be open
const isRegularChallengeOpen = !!challenges.length;
const isIframeChallengeOpen = iframeModalOpen && !!iframeModalUrl;

const isOpen = isRegularChallengeOpen || isIframeChallengeOpen;
const isOpen = !!challenges.length;
const closeModal = () => {
if (isRegularChallengeOpen) {
removeChallenge();
}
if (isIframeChallengeOpen) {
closeIframeModal();
}
removeChallenge();
};

const { refs, context } = useFloating({
Expand All @@ -341,12 +320,7 @@ const ChallengeModal = () => {
<FloatingFocusManager context={context} modal={false}>
<div className={styles.modal} ref={refs.setFloating} aria-labelledby={headingId} {...getFloatingProps()}>
<div className={styles.container}>
<ChallengeContent
challenge={isRegularChallengeOpen ? challenges[0] : undefined}
iframeUrl={iframeModalUrl}
publication={iframeModalPublication}
closeModal={closeModal}
/>
<ChallengeContent challenge={challenges[0]} closeModal={closeModal} />
</div>
</div>
</FloatingFocusManager>
Expand Down
Loading