fix(challenge-modal): publish challenge answers with pkc object schema

pkc-js's publishChallengeAnswers destructures { challengeAnswers } from its
argument, so passing a bare string[] left challengeAnswers undefined and
silently broke challenge submission (i.e. posting). Wrap answers in the object
form via a publishPublicationChallengeAnswers helper with error logging, and
align the ChallengePublication type with the real runtime signature.
This commit is contained in:
Tommaso Casaburi
2026-06-13 18:25:04 +07:00
parent ded239bef2
commit 591aaa729f
3 changed files with 31 additions and 14 deletions
@@ -49,11 +49,15 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useComment: ({ commentCid }: { commentCid?: string }) => (commentCid ? testState.commentsByCid[commentCid] : undefined), useComment: ({ commentCid }: { commentCid?: string }) => (commentCid ? testState.commentsByCid[commentCid] : undefined),
})); }));
vi.mock('../../../lib/utils/challenge-utils', () => ({ vi.mock('../../../lib/utils/challenge-utils', async (importOriginal) => {
getPublicationPreview: () => testState.publicationPreview, const actual = await importOriginal<typeof import('../../../lib/utils/challenge-utils')>();
getPublicationType: () => testState.publicationType, return {
getVotePreview: () => testState.votePreview, ...actual,
})); getPublicationPreview: () => testState.publicationPreview,
getPublicationType: () => testState.publicationType,
getVotePreview: () => testState.votePreview,
};
});
vi.mock('../../../hooks/use-is-mobile', () => ({ vi.mock('../../../hooks/use-is-mobile', () => ({
default: () => testState.isMobile, default: () => testState.isMobile,
@@ -224,7 +228,7 @@ describe('ChallengeModal', () => {
input?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' })); input?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }));
}); });
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['4']); expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: ['4'] });
expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
}); });
@@ -310,7 +314,7 @@ describe('ChallengeModal', () => {
await dispatchInput(container.querySelector<HTMLInputElement>('input[placeholder*="TYPE THE ANSWER HERE"]') as HTMLInputElement, 'step two'); await dispatchInput(container.querySelector<HTMLInputElement>('input[placeholder*="TYPE THE ANSWER HERE"]') as HTMLInputElement, 'step two');
await clickButton('submit'); await clickButton('submit');
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['step one', 'step two']); expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: ['step one', 'step two'] });
expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
}); });
@@ -369,7 +373,7 @@ describe('ChallengeModal', () => {
expect(doneButton?.getAttribute('aria-label')).toBe('Finish challenge'); expect(doneButton?.getAttribute('aria-label')).toBe('Finish challenge');
await clickButton('Finish Challenge'); await clickButton('Finish Challenge');
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['']); expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: [''] });
expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
}); });
@@ -481,7 +485,7 @@ describe('ChallengeModal', () => {
); );
}); });
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['']); expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: [''] });
expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
}); });
@@ -538,7 +542,7 @@ describe('ChallengeModal', () => {
); );
}); });
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['', '']); expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: ['', ''] });
expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
}); });
@@ -1,7 +1,7 @@
import { useRef, useState, useEffect, useCallback } from 'react'; import { useRef, useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Challenge as ChallengeType, useAccount, useComment } from '@bitsocial/bitsocial-react-hooks'; import { Challenge as ChallengeType, useAccount, useComment } from '@bitsocial/bitsocial-react-hooks';
import { getPublicationPreview, getPublicationType, getVotePreview } from '../../lib/utils/challenge-utils'; import { getPublicationPreview, getPublicationType, getVotePreview, publishPublicationChallengeAnswers } from '../../lib/utils/challenge-utils';
import { stripGeneratedFortuneMarkup } from '../../lib/utils/post-options-utils'; import { stripGeneratedFortuneMarkup } from '../../lib/utils/post-options-utils';
import useIsMobile from '../../hooks/use-is-mobile'; import useIsMobile from '../../hooks/use-is-mobile';
import useChallengesStore from '../../stores/use-challenges-store'; import useChallengesStore from '../../stores/use-challenges-store';
@@ -40,6 +40,10 @@ const ImageChallenge = ({ challenge }: { challenge: string }) =>
<div className={styles.challengeMedia}>Invalid image challenge</div> <div className={styles.challengeMedia}>Invalid image challenge</div>
); );
const logPublishChallengeAnswersError = (error: unknown) => {
console.error('Failed to publish challenge answers:', error);
};
const isLocalIframeHostname = (hostname: string) => hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname.endsWith('.localhost'); const isLocalIframeHostname = (hostname: string) => hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname.endsWith('.localhost');
type ValidatedIframeUrl = { status: 'valid'; finalUrl: string; origin: string } | { status: 'invalid'; error: unknown } | { status: 'unsupported' }; type ValidatedIframeUrl = { status: 'valid'; finalUrl: string; origin: string } | { status: 'invalid'; error: unknown } | { status: 'unsupported' };
@@ -391,7 +395,7 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
const onSubmit = () => { const onSubmit = () => {
if (!publication) return; if (!publication) return;
publication.publishChallengeAnswers(answers); void publishPublicationChallengeAnswers(publication, answers).catch(logPublishChallengeAnswersError);
setAnswers([]); setAnswers([]);
closeModal(); closeModal();
}; };
@@ -413,7 +417,7 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
return; return;
} }
publication.publishChallengeAnswers(updatedAnswers); void publishPublicationChallengeAnswers(publication, updatedAnswers).catch(logPublishChallengeAnswersError);
setAnswers([]); setAnswers([]);
closeModal(); closeModal();
}, },
+10 -1
View File
@@ -13,19 +13,28 @@ const resolveBoardIdentifier = (communityAddress: unknown): string => {
return boardPath === communityAddress ? communityAddress : `/${boardPath}/`; return boardPath === communityAddress ? communityAddress : `/${boardPath}/`;
}; };
export type ChallengePublication = Partial<Comment> & { type ChallengeAnswersInput = string[] | { challengeAnswers?: string[] };
export type ChallengePublication = Partial<Omit<Comment, 'publishChallengeAnswers'>> & {
author?: unknown; author?: unknown;
commentCid?: string; commentCid?: string;
communityAddress?: string; communityAddress?: string;
content?: string; content?: string;
link?: string; link?: string;
parentCid?: string; parentCid?: string;
publishChallengeAnswers?: (challengeAnswers?: ChallengeAnswersInput) => Promise<void> | void;
shortCommunityAddress?: string; shortCommunityAddress?: string;
subplebbitAddress?: string; subplebbitAddress?: string;
title?: string; title?: string;
vote?: number; vote?: number;
}; };
export const publishPublicationChallengeAnswers = async (publication: ChallengePublication | undefined, challengeAnswers: string[]) => {
const publishChallengeAnswers = publication?.publishChallengeAnswers;
if (typeof publishChallengeAnswers !== 'function') return;
await publishChallengeAnswers.call(publication, { challengeAnswers });
};
export const redactGeneratedFortuneFromPublication = <T>(publication: T): T => { export const redactGeneratedFortuneFromPublication = <T>(publication: T): T => {
if (!publication || typeof publication !== 'object') { if (!publication || typeof publication !== 'object') {
return publication; return publication;