diff --git a/package.json b/package.json index 6b60960b..d15d1376 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "license": "GPL-2.0-only", "private": true, "dependencies": { - "@bitsocialnet/bitsocial-react-hooks": "https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/95b46a8350ed5a2f96721f0dc14f2aaf26d410bf", + "@bitsocialnet/bitsocial-react-hooks": "https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/648d789ff2c42feec208156a6c03af6e3825f45d", "@capacitor/app": "7.0.1", "@capacitor/status-bar": "7.0.1", "@capawesome/capacitor-android-edge-to-edge-support": "7.2.2", diff --git a/src/components/challenge-modal/__tests__/challenge-modal.test.tsx b/src/components/challenge-modal/__tests__/challenge-modal.test.tsx index 60592804..3d77b060 100644 --- a/src/components/challenge-modal/__tests__/challenge-modal.test.tsx +++ b/src/components/challenge-modal/__tests__/challenge-modal.test.tsx @@ -3,6 +3,7 @@ import { createElement } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import ChallengeModal from '../challenge-modal'; +import getShortAddress from '../../../lib/get-short-address'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; @@ -88,7 +89,7 @@ let consoleErrorSpy: ReturnType; let postMessageMock: ReturnType; let root: Root; -const createPublication = () => ({ +const createPublication = (): Record => ({ author: { displayName: 'Alice' }, content: 'Publication content', link: 'https://example.com/link', @@ -285,6 +286,63 @@ describe('ChallengeModal', () => { expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); }); + it('allows localhost http iframe challenges for local spam blocker testing', async () => { + const publication = createPublication(); + testState.challenges = [ + createStoredChallenge( + { + challenge: 'http://localhost:3000/api/v1/iframe/session-123?foo=bar', + type: 'url/iframe', + }, + publication, + ), + ]; + + await renderModal(); + expect(container.textContent).toContain('mu wants to open localhost:3000'); + + await clickButton('Open'); + + const iframe = container.querySelector('iframe'); + expect(iframe).not.toBeNull(); + expect(iframe?.getAttribute('src')).toContain('http://localhost:3000/api/v1/iframe/session-123?foo=bar&theme=dark'); + + await act(async () => { + iframe?.dispatchEvent(new Event('load', { bubbles: true })); + }); + + expect(postMessageMock).toHaveBeenCalledWith( + { + source: 'plebbit-5chan', + theme: 'dark', + type: 'plebbit-theme', + }, + 'http://localhost:3000', + ); + }); + + it('uses the shortened community address when shortCommunityAddress is unavailable', async () => { + const longCommunityAddress = '12D3KooWS6yKc5N7o6JcAYHZpaQwAwyh1VddYatarU75Se3HXEeD'; + const publication = { + ...createPublication(), + shortCommunityAddress: undefined, + communityAddress: longCommunityAddress, + }; + testState.challenges = [ + createStoredChallenge( + { + challenge: 'http://localhost:3000/api/v1/iframe/session-123?foo=bar', + type: 'url/iframe', + }, + publication, + ), + ]; + + await renderModal(); + expect(container.textContent).toContain(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000`); + expect(container.textContent).not.toContain(`${longCommunityAddress} wants to open localhost:3000`); + }); + it('alerts when iframe challenges need a signer address and the account is missing one', async () => { testState.account = { author: { address: '' } }; testState.challenges = [ diff --git a/src/components/challenge-modal/challenge-modal.tsx b/src/components/challenge-modal/challenge-modal.tsx index f5178e66..7c96da91 100644 --- a/src/components/challenge-modal/challenge-modal.tsx +++ b/src/components/challenge-modal/challenge-modal.tsx @@ -9,6 +9,7 @@ import styles from './challenge-modal.module.css'; import capitalize from 'lodash/capitalize'; import { useSpring, animated } from '@react-spring/web'; import { useDrag } from '@use-gesture/react'; +import getShortAddress from '../../lib/get-short-address'; const useParentAddress = (parentCid?: string) => { const parentComment = useComment({ commentCid: parentCid, onlyIfCached: true }); @@ -25,6 +26,17 @@ const TextChallenge = ({ challenge }: { challenge: string }) =>
; +const isLocalIframeHostname = (hostname: string) => hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname.endsWith('.localhost'); + +const getReadableIframeUrl = (challengeUrl: string) => { + try { + const url = new URL(challengeUrl); + return url.host || url.hostname; + } catch { + return ''; + } +}; + interface IframeChallengeProps { challenge: string; shortCommunityAddress?: string; @@ -42,6 +54,7 @@ const IframeChallenge = ({ challenge, shortCommunityAddress, communityAddress, r const [iframeUrlState, setIframeUrl] = useState(''); const [iframeOrigin, setIframeOrigin] = useState(''); const iframeRef = useRef(null); + const displayCommunityAddress = shortCommunityAddress || (communityAddress ? getShortAddress(communityAddress) : '') || communityAddress || 'unknown board'; const handleLoadIframe = useCallback(() => { const iframeUrl = challenge; @@ -60,8 +73,10 @@ const IframeChallenge = ({ challenge, shortCommunityAddress, communityAddress, r try { const validatedUrl = new URL(replacedUrl); - if (validatedUrl.protocol !== 'https:') { - throw new Error('Only HTTPS iframe challenges are supported'); + const isHttps = validatedUrl.protocol === 'https:'; + const isLocalHttp = validatedUrl.protocol === 'http:' && isLocalIframeHostname(validatedUrl.hostname); + if (!isHttps && !isLocalHttp) { + throw new Error('Only HTTPS iframe challenges or localhost HTTP challenges are supported'); } validatedUrl.pathname = validatedUrl.pathname.replace(/\/{2,}/g, '/'); validatedUrl.searchParams.set('theme', theme); @@ -101,7 +116,7 @@ const IframeChallenge = ({ challenge, shortCommunityAddress, communityAddress, r {publicationDetails}
- {shortCommunityAddress || communityAddress || 'unknown board'} wants to open {readableUrl || 'an external site'} + {displayCommunityAddress} wants to open {readableUrl || 'an external site'}
@@ -235,26 +250,11 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => { }, [abandonModal]); const getChallengeUrl = useCallback(() => { - try { - const iframeUrl = currentChallenge?.challenge; - if (!iframeUrl) return ''; - const url = new URL(iframeUrl); - if (url.hostname === 'mintpass.org') return url.hostname; - return url.href; - } catch { - return ''; - } + const iframeUrl = currentChallenge?.challenge; + if (!iframeUrl) return ''; + return getReadableIframeUrl(iframeUrl); }, [currentChallenge]); - - const readableUrl = (() => { - const url = getChallengeUrl(); - if (!url) return ''; - try { - return decodeURIComponent(url); - } catch { - return url; - } - })(); + const readableUrl = getChallengeUrl(); if (!challenges?.length || !publication || !currentChallenge) { return null; diff --git a/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx b/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx index eb5400ea..2a44f0e7 100644 --- a/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx +++ b/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx @@ -9,11 +9,11 @@ const act = (React as { act?: (cb: () => void | Promise) => void | Promise const testState = vi.hoisted(() => ({ account: { mediaIpfsGatewayUrl: 'https://media.old.example', + chainProviders: { + eth: { chainId: 1, urls: ['https://eth.old.example'] }, + sol: { chainId: 101, urls: ['https://sol.old.example'] }, + }, pkcOptions: { - chainProviders: { - eth: { chainId: 1, urls: ['https://eth.old.example'] }, - sol: { chainId: 101, urls: ['https://sol.old.example'] }, - }, httpRoutersOptions: ['https://router.old.example'], ipfsGatewayUrls: ['https://ipfs.old.example'], pkcRpcClientsOptions: ['ws://old.example/key'], @@ -86,11 +86,11 @@ describe('AdvancedSettings', () => { vi.clearAllMocks(); testState.account = { mediaIpfsGatewayUrl: 'https://media.old.example', + chainProviders: { + eth: { chainId: 1, urls: ['https://eth.old.example'] }, + sol: { chainId: 101, urls: ['https://sol.old.example'] }, + }, pkcOptions: { - chainProviders: { - eth: { chainId: 1, urls: ['https://eth.old.example'] }, - sol: { chainId: 101, urls: ['https://sol.old.example'] }, - }, httpRoutersOptions: ['https://router.old.example'], ipfsGatewayUrls: ['https://ipfs.old.example'], pkcRpcClientsOptions: ['ws://old.example/key'], @@ -154,11 +154,11 @@ describe('AdvancedSettings', () => { expect(testState.setAccountMock).toHaveBeenCalledWith({ mediaIpfsGatewayUrl: 'https://media.new.example', + chainProviders: { + eth: { chainId: 1, urls: ['https://eth.one.example'] }, + sol: { chainId: 101, urls: ['https://sol.one.example'] }, + }, pkcOptions: { - chainProviders: { - eth: { chainId: 1, urls: ['https://eth.one.example'] }, - sol: { chainId: 101, urls: ['https://sol.one.example'] }, - }, dataPath: '/tmp/next-plebbit', httpRoutersOptions: ['https://router.one.example'], ipfsGatewayUrls: ['https://ipfs.one.example', 'https://ipfs.two.example'], diff --git a/src/components/settings-modal/advanced-settings/advanced-settings.tsx b/src/components/settings-modal/advanced-settings/advanced-settings.tsx index 0cc81647..f7930274 100644 --- a/src/components/settings-modal/advanced-settings/advanced-settings.tsx +++ b/src/components/settings-modal/advanced-settings/advanced-settings.tsx @@ -237,7 +237,7 @@ const AdvancedSettings = () => { const mediaIpfsGatewayUrl = mediaIpfsGatewayUrlRef.current?.value.trim(); - const pubsubHttpClientsOptions = pubsubProvidersRef.current?.value + const pubsubKuboRpcClientsOptions = pubsubProvidersRef.current?.value .split('\n') .map((url) => url.trim()) .filter((url) => url !== ''); @@ -272,11 +272,11 @@ const AdvancedSettings = () => { await setAccount({ ...account, mediaIpfsGatewayUrl, + chainProviders, pkcOptions: { ...protocolOptions, ipfsGatewayUrls, - pubsubKuboRpcClientsOptions: pubsubHttpClientsOptions, - chainProviders, + pubsubKuboRpcClientsOptions, httpRoutersOptions, pkcRpcClientsOptions, dataPath, diff --git a/src/hooks/__tests__/use-publish-post.test.tsx b/src/hooks/__tests__/use-publish-post.test.tsx index 520efe75..1feaf992 100644 --- a/src/hooks/__tests__/use-publish-post.test.tsx +++ b/src/hooks/__tests__/use-publish-post.test.tsx @@ -82,6 +82,7 @@ describe('usePublishPost', () => { spoiler: true, title: 'Hello world', }); + expect('subplebbitAddress' in latestValue.publishPostOptions).toBe(false); expect(typeof latestValue.publishPostOptions.onChallengeVerification).toBe('function'); expect(typeof latestValue.publishPostOptions.onError).toBe('function'); }); diff --git a/src/hooks/__tests__/use-publish-reply.test.tsx b/src/hooks/__tests__/use-publish-reply.test.tsx index fea99018..affda3c4 100644 --- a/src/hooks/__tests__/use-publish-reply.test.tsx +++ b/src/hooks/__tests__/use-publish-reply.test.tsx @@ -112,6 +112,7 @@ describe('usePublishReply', () => { quotedCids: ['quoted-cid'], spoiler: true, }); + expect('subplebbitAddress' in (testState.lastPublishOptions || {})).toBe(false); }); it('resolves same-board external quote references before triggering publish', async () => { diff --git a/src/stores/__tests__/publish-stores.test.ts b/src/stores/__tests__/publish-stores.test.ts index 0a33cfaf..7d126fe5 100644 --- a/src/stores/__tests__/publish-stores.test.ts +++ b/src/stores/__tests__/publish-stores.test.ts @@ -52,6 +52,7 @@ describe('publish stores', () => { expect(state.author).toEqual({ address: '0x123', role: 'mod', displayName: 'Poster Alias' }); expect(state.publishCommentOptions.author).toEqual({ address: '0x123', role: 'mod', displayName: 'Poster Alias' }); expect(state.publishCommentOptions.communityAddress).toBe('music-posting.eth'); + expect('subplebbitAddress' in state.publishCommentOptions).toBe(false); expect(state.publishCommentOptions.title).toBe('Hello'); state.publishCommentOptions.onChallengeVerification?.({} as never, comment); @@ -85,6 +86,7 @@ describe('publish stores', () => { expect(state.publishCommentOptions['parent-1']?.communityAddress).toBe('music-posting.eth'); expect(state.publishCommentOptions['parent-1']?.parentCid).toBe('parent-1'); expect(state.publishCommentOptions['parent-1']?.postCid).toBe('parent-1'); + expect('subplebbitAddress' in (state.publishCommentOptions['parent-1'] || {})).toBe(false); state.publishCommentOptions['parent-1']?.onChallengeVerification?.({ token: 'challenge' } as never, comment); expect(testState.alertChallengeVerificationFailedMock).toHaveBeenCalledWith({ token: 'challenge' }, comment); diff --git a/yarn.lock b/yarn.lock index 152fa11f..a71c9aa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ __metadata: version: 0.0.0-use.local resolution: "5chan@workspace:." dependencies: - "@bitsocialnet/bitsocial-react-hooks": "https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/95b46a8350ed5a2f96721f0dc14f2aaf26d410bf" + "@bitsocialnet/bitsocial-react-hooks": "https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/648d789ff2c42feec208156a6c03af6e3825f45d" "@capacitor/android": "npm:7.4.5" "@capacitor/app": "npm:7.0.1" "@capacitor/cli": "npm:7.4.5" @@ -1519,22 +1519,22 @@ __metadata: languageName: node linkType: hard -"@bitsocial/bso-resolver@https://github.com/bitsocialnet/bso-resolver.git#a005e4b507435c0f399328800d6c421bf62b14ed": - version: 0.0.2 - resolution: "@bitsocial/bso-resolver@https://github.com/bitsocialnet/bso-resolver.git#commit=a005e4b507435c0f399328800d6c421bf62b14ed" +"@bitsocial/bso-resolver@npm:0.0.4": + version: 0.0.4 + resolution: "@bitsocial/bso-resolver@npm:0.0.4" dependencies: "@pkc/pkc-logger": "github:pkcprotocol/pkc-logger#cb494fbaf332ed5b2cfd9d9d0f90ca7fd0058b4a" better-sqlite3: "npm:12.6.2" viem: "npm:2.47.0" - checksum: 10c0/64f86a126ba3c3f4c9516bafa895dfbb4f18f72c9e9cd0e1f9584755291e768f8ff33b08988ce01fa0fcce0cafd987b79547e31770d5e4f038b7b8b74422e778 + checksum: 10c0/39cf1f3381343591e98ae149c682acd245403bde5608135d849dd31904c6b539f3accdc46e1e6a3978ddf6dea71f85814be8792ce5f5e0d7526399f1eb23ac48 languageName: node linkType: hard -"@bitsocialnet/bitsocial-react-hooks@https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/95b46a8350ed5a2f96721f0dc14f2aaf26d410bf": +"@bitsocialnet/bitsocial-react-hooks@https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/648d789ff2c42feec208156a6c03af6e3825f45d": version: 0.1.0 - resolution: "@bitsocialnet/bitsocial-react-hooks@https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/95b46a8350ed5a2f96721f0dc14f2aaf26d410bf" + resolution: "@bitsocialnet/bitsocial-react-hooks@https://codeload.github.com/bitsocialnet/bitsocial-react-hooks/tar.gz/648d789ff2c42feec208156a6c03af6e3825f45d" dependencies: - "@bitsocial/bso-resolver": "https://github.com/bitsocialnet/bso-resolver.git#a005e4b507435c0f399328800d6c421bf62b14ed" + "@bitsocial/bso-resolver": "npm:0.0.4" "@pkc/pkc-logger": "https://github.com/pkcprotocol/pkc-logger.git" "@pkcprotocol/pkc-js": "https://github.com/pkcprotocol/pkc-js.git#e63026e6cd33df5af3a3b5e6473b1e364d89fbdb" assert: "npm:2.0.0" @@ -1551,7 +1551,7 @@ __metadata: zustand: "npm:4.0.0" peerDependencies: react: ">=16.8" - checksum: 10c0/48a658a76f77d9febd6a11b6c8cb027574999acd6189beee5b121cae81653f123b06cebad134b10a36ca70ec3d5adbbf80c4c3e5de8e81d745d0430c95dfcf08 + checksum: 10c0/e4acb68b8b1cc50e8fffaa9245cb47f7496164b98c38ec7891f7cedaac7d80e8b58e341cef1e591995e70854ed32a7320f55d2bd3cb94064df165a24f4f1aba0 languageName: node linkType: hard