mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat: auto-complete iframe challenges
This commit is contained in:
@@ -325,6 +325,70 @@ describe('ChallengeModal', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('auto-submits iframe challenges when the iframe posts a completion message', async () => {
|
||||
const publication = createPublication();
|
||||
testState.challenges = [
|
||||
createStoredChallenge(
|
||||
{
|
||||
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
||||
type: 'url/iframe',
|
||||
},
|
||||
publication,
|
||||
),
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'challengeAnswer',
|
||||
challengeAnswers: [''],
|
||||
sessionId: 'session-123',
|
||||
},
|
||||
origin: 'https://spamblocker.bitsocial.net',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['']);
|
||||
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('ignores iframe completion messages with the wrong session id', async () => {
|
||||
const publication = createPublication();
|
||||
testState.challenges = [
|
||||
createStoredChallenge(
|
||||
{
|
||||
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
||||
type: 'url/iframe',
|
||||
},
|
||||
publication,
|
||||
),
|
||||
];
|
||||
|
||||
await renderModal();
|
||||
await clickButton('Open');
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'challengeAnswer',
|
||||
challengeAnswers: [''],
|
||||
sessionId: 'session-999',
|
||||
},
|
||||
origin: 'https://spamblocker.bitsocial.net',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(publication.publishChallengeAnswers).not.toHaveBeenCalled();
|
||||
expect(testState.removeChallengeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the shortened community address when shortCommunityAddress is unavailable', async () => {
|
||||
const longCommunityAddress = '12D3KooWS6yKc5N7o6JcAYHZpaQwAwyh1VddYatarU75Se3HXEeD';
|
||||
const publication = {
|
||||
|
||||
@@ -37,6 +37,16 @@ const getReadableIframeUrl = (challengeUrl: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getIframeSessionId = (challengeUrl: string) => {
|
||||
try {
|
||||
const url = new URL(challengeUrl);
|
||||
const match = url.pathname.match(/\/iframe\/([^/?#]+)/);
|
||||
return match?.[1] ?? '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
interface IframeChallengeProps {
|
||||
challenge: string;
|
||||
shortCommunityAddress?: string;
|
||||
@@ -44,17 +54,29 @@ interface IframeChallengeProps {
|
||||
readableUrl: string;
|
||||
onCancel: () => void;
|
||||
onDone: () => void;
|
||||
onAutoComplete: (challengeAnswers: string[]) => void;
|
||||
publicationDetails: React.ReactNode;
|
||||
}
|
||||
|
||||
const IframeChallenge = ({ challenge, shortCommunityAddress, communityAddress, readableUrl, onCancel, onDone, publicationDetails }: IframeChallengeProps) => {
|
||||
const IframeChallenge = ({
|
||||
challenge,
|
||||
shortCommunityAddress,
|
||||
communityAddress,
|
||||
readableUrl,
|
||||
onCancel,
|
||||
onDone,
|
||||
onAutoComplete,
|
||||
publicationDetails,
|
||||
}: IframeChallengeProps) => {
|
||||
const account = useAccount();
|
||||
const [theme] = useTheme();
|
||||
const [showIframeConfirmation, setShowIframeConfirmation] = useState(true);
|
||||
const [iframeUrlState, setIframeUrl] = useState('');
|
||||
const [iframeOrigin, setIframeOrigin] = useState('');
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const handledAutoCompleteRef = useRef(false);
|
||||
const displayCommunityAddress = shortCommunityAddress || (communityAddress ? getShortAddress(communityAddress) : '') || communityAddress || 'unknown board';
|
||||
const expectedSessionId = getIframeSessionId(challenge);
|
||||
|
||||
const handleLoadIframe = useCallback(() => {
|
||||
const iframeUrl = challenge;
|
||||
@@ -110,6 +132,30 @@ const IframeChallenge = ({ challenge, shortCommunityAddress, communityAddress, r
|
||||
}
|
||||
}, [iframeOrigin, iframeUrlState, sendThemeToIframe, showIframeConfirmation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showIframeConfirmation || !iframeOrigin || !expectedSessionId) {
|
||||
handledAutoCompleteRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== iframeOrigin) return;
|
||||
if (handledAutoCompleteRef.current) return;
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if ((data as { type?: string }).type !== 'challengeAnswer') return;
|
||||
const challengeAnswers = (data as { challengeAnswers?: unknown }).challengeAnswers;
|
||||
if (!Array.isArray(challengeAnswers)) return;
|
||||
const sessionId = (data as { sessionId?: unknown }).sessionId;
|
||||
if (sessionId !== expectedSessionId) return;
|
||||
handledAutoCompleteRef.current = true;
|
||||
onAutoComplete(challengeAnswers.filter((answer): answer is string => typeof answer === 'string'));
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [expectedSessionId, iframeOrigin, onAutoComplete, showIframeConfirmation]);
|
||||
|
||||
if (showIframeConfirmation) {
|
||||
return (
|
||||
<>
|
||||
@@ -229,6 +275,15 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
closeModal();
|
||||
}, [closeModal, publication]);
|
||||
|
||||
const onIframeAutoComplete = useCallback(
|
||||
(challengeAnswers: string[]) => {
|
||||
if (!publication) return;
|
||||
publication.publishChallengeAnswers(challengeAnswers);
|
||||
closeModal();
|
||||
},
|
||||
[closeModal, publication],
|
||||
);
|
||||
|
||||
const onEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
if (!isValidAnswer(currentChallengeIndex)) return;
|
||||
@@ -323,6 +378,7 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
|
||||
readableUrl={readableUrl}
|
||||
onCancel={abandonModal}
|
||||
onDone={onIframeDone}
|
||||
onAutoComplete={onIframeAutoComplete}
|
||||
publicationDetails={publicationDetails}
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user