ci: docker_tag=ext fix(front): rendre SSR-safe Iframe/MessageBus/AuthModal/_app (basePath /lecoffre)

This commit is contained in:
Debian Dev4 2025-09-22 10:50:20 +00:00
parent 22bcc727c9
commit 5284d9be04
5 changed files with 100 additions and 57 deletions

View File

@ -36,6 +36,10 @@ export default class WindowStore {
} }
private iniEvents(): void { private iniEvents(): void {
if (typeof window === 'undefined' || typeof document === 'undefined') {
// SSR: do not bind browser events
return;
}
window.addEventListener("scroll", (e: Event) => this.scrollYHandler()); window.addEventListener("scroll", (e: Event) => this.scrollYHandler());
window.addEventListener("resize", (e: Event) => this.resizeHandler()); window.addEventListener("resize", (e: Event) => this.resizeHandler());
document.addEventListener("click", (e: MouseEvent) => this.clickHandler(e), true); document.addEventListener("click", (e: MouseEvent) => this.clickHandler(e), true);
@ -46,10 +50,11 @@ export default class WindowStore {
} }
private scrollYHandler = (() => { private scrollYHandler = (() => {
let previousY: number = window.scrollY; let previousY: number = (typeof window !== 'undefined' ? window.scrollY : 0);
let snapShotY: number = previousY; let snapShotY: number = previousY;
let previousYDirection: number = 1; let previousYDirection: number = 1;
return (): void => { return (): void => {
if (typeof window === 'undefined') return;
const scrollYDirection = window.scrollY - previousY > 0 ? 1 : -1; const scrollYDirection = window.scrollY - previousY > 0 ? 1 : -1;
if (previousYDirection !== scrollYDirection) { if (previousYDirection !== scrollYDirection) {
snapShotY = window.scrollY; snapShotY = window.scrollY;
@ -62,6 +67,7 @@ export default class WindowStore {
})(); })();
private resizeHandler() { private resizeHandler() {
if (typeof window === 'undefined') return;
this.event.emit("resize", window); this.event.emit("resize", window);
} }
} }

View File

@ -6,11 +6,13 @@ import type { AppType, AppProps } from "next/app";
import { useEffect, useState, type ReactElement, type ReactNode } from "react"; import { useEffect, useState, type ReactElement, type ReactNode } from "react";
import getConfig from "next/config"; import getConfig from "next/config";
import { GoogleTagManager } from "@next/third-parties/google"; import { GoogleTagManager } from "@next/third-parties/google";
import dynamic from 'next/dynamic';
import Loader from "src/common/Api/LeCoffreApi/sdk/Loader"; import Loader from "src/common/Api/LeCoffreApi/sdk/Loader";
import IframeReference from "src/sdk/IframeReference"; import IframeReference from "src/sdk/IframeReference";
import Iframe from "src/sdk/Iframe"; // import Iframe from "src/sdk/Iframe";
const IframeNoSSR = dynamic(() => import('src/sdk/Iframe'), { ssr: false });
import MessageBus from "src/sdk/MessageBus"; import MessageBus from "src/sdk/MessageBus";
import User from "src/sdk/User"; import User from "src/sdk/User";
@ -80,26 +82,31 @@ const MyApp = (({
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const [mounted, setMounted] = useState(false);
const targetOrigin = (() => { // Configure iframe target and URL on client-side only
try { useEffect(() => {
return new URL(_4nkUrl).origin; setMounted(true);
} catch { try {
return _4nkUrl; if (_4nkUrl && typeof _4nkUrl === 'string' && _4nkUrl.trim().length > 0) {
} const origin = (() => {
})(); try { return new URL(_4nkUrl).origin; } catch { return _4nkUrl; }
IframeReference.setTargetOrigin(targetOrigin); })();
IframeReference.setTargetOrigin(origin);
// Configure full iframe URL if provided (falls back to origin) const candidate = (publicRuntimeConfig as any).NEXT_PUBLIC_4NK_IFRAME_URL ?? _4nkUrl;
const iframeUrl = (() => { const iframe = (() => {
const candidate = (publicRuntimeConfig as any).NEXT_PUBLIC_4NK_IFRAME_URL ?? _4nkUrl; try { return new URL(candidate).toString(); } catch { return origin; }
try { })();
return new URL(candidate).toString(); IframeReference.setIframeUrl(iframe);
} catch { } else {
return targetOrigin; console.warn('[MyApp] NEXT_PUBLIC_4NK_URL is missing; skipping iframe setup');
} }
})(); } catch (e) {
IframeReference.setIframeUrl(iframeUrl); console.error('[MyApp] Failed to initialize IframeReference', e);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
const isAuthenticated = User.getInstance().isAuthenticated(); const isAuthenticated = User.getInstance().isAuthenticated();
@ -117,7 +124,12 @@ const MyApp = (({
return () => { }; return () => { };
}, []); }, []);
// Hotjar supprimé // Empêcher le rendu SSR du contenu qui dépend du navigateur
if (!mounted) {
return <Loader />;
}
// Hotjar supprimé
return getLayout( return getLayout(
<> <>
@ -126,7 +138,7 @@ const MyApp = (({
<GoogleTagManager gtmId="GTM-5GLJN86P" /> <GoogleTagManager gtmId="GTM-5GLJN86P" />
</Component> </Component>
} }
{isConnected && <Iframe />} {isConnected && <IframeNoSSR />}
<Loader /> <Loader />
</> </>
); );
@ -147,7 +159,7 @@ MyApp.getInitialProps = async () => {
idNotRedirectUri: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_REDIRECT_URI, idNotRedirectUri: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_REDIRECT_URI,
fcAuthorizeEndpoint: publicRuntimeConfig.NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT, fcAuthorizeEndpoint: publicRuntimeConfig.NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT,
fcClientId: publicRuntimeConfig.NEXT_PUBLIC_FC_CLIENT_ID, fcClientId: publicRuntimeConfig.NEXT_PUBLIC_FC_CLIENT_ID,
docaposteApiUrl: publicRuntimeConfig.NEXT_PUBLIC_DOCAPOST_API_URL, docaposteApiUrl: publicRuntimeConfig.NEXT_PUBLIC_DOCAPOSTE_API_URL,
_4nkUrl: publicRuntimeConfig.NEXT_PUBLIC_4NK_URL, _4nkUrl: publicRuntimeConfig.NEXT_PUBLIC_4NK_URL,
_4nkIframeUrl: publicRuntimeConfig.NEXT_PUBLIC_4NK_IFRAME_URL, _4nkIframeUrl: publicRuntimeConfig.NEXT_PUBLIC_4NK_IFRAME_URL,
apiUrl: publicRuntimeConfig.NEXT_PUBLIC_API_URL, apiUrl: publicRuntimeConfig.NEXT_PUBLIC_API_URL,

View File

@ -17,10 +17,23 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
const [isIframeReady, setIsIframeReady] = useState(false); const [isIframeReady, setIsIframeReady] = useState(false);
const [showIframe, setShowIframe] = useState(false); const [showIframe, setShowIframe] = useState(false);
const [authSuccess, setAuthSuccess] = useState(false); const [authSuccess, setAuthSuccess] = useState(false);
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
// Initialize iframe URL client-side only
useEffect(() => { useEffect(() => {
try {
const url = IframeReference.getIframeUrl();
setIframeSrc(url);
} catch {
setIframeSrc(null);
}
}, []);
useEffect(() => {
if (typeof window === 'undefined') return;
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
if (!event.data || event.data.type === 'PassClientScriptReady') { if (!event.data || event.data.type === 'PassClientScriptReady') {
return; return;
@ -31,15 +44,20 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
return; return;
} }
const targetOrigin = IframeReference.getTargetOrigin(); let targetOrigin: string | null = null;
if (!targetOrigin) { try {
targetOrigin = IframeReference.getTargetOrigin();
} catch {
console.error('[AuthModal] handleMessage: targetOrigin not found'); console.error('[AuthModal] handleMessage: targetOrigin not found');
return; return;
} }
if (!targetOrigin) {
return;
}
// Accepter seulement les messages de l'iframe (même origine) // Accepter seulement les messages de l'iframe (même origine)
if (event.origin !== targetOrigin) { if (event.origin !== targetOrigin) {
// Ignorer silencieusement les messages des DevTools et autres sources
return; return;
} }
@ -58,7 +76,6 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
setIsIframeReady(true); setIsIframeReady(true);
break; break;
} }
case 'LINK_ACCEPTED': { case 'LINK_ACCEPTED': {
setShowIframe(false); setShowIframe(false);
User.getInstance().setTokens(message.accessToken, message.refreshToken); User.getInstance().setTokens(message.accessToken, message.refreshToken);
@ -66,12 +83,10 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
iframeRef.current.contentWindow!.postMessage({ type: 'GET_PAIRING_ID', accessToken: message.accessToken, messageId }, targetOrigin); iframeRef.current.contentWindow!.postMessage({ type: 'GET_PAIRING_ID', accessToken: message.accessToken, messageId }, targetOrigin);
break; break;
} }
case 'PAIRING_CREATED': { case 'PAIRING_CREATED': {
console.log('[AuthModal] PAIRING_CREATED:', message); console.log('[AuthModal] PAIRING_CREATED:', message);
User.getInstance().setPairingId(message.userPairingId); User.getInstance().setPairingId(message.userPairingId);
setAuthSuccess(true); setAuthSuccess(true);
setTimeout(() => { setTimeout(() => {
setShowIframe(false); setShowIframe(false);
setIsIframeReady(false); setIsIframeReady(false);
@ -80,11 +95,9 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
}, 500); }, 500);
break; break;
} }
case 'GET_PAIRING_ID': { case 'GET_PAIRING_ID': {
User.getInstance().setPairingId(message.userPairingId); User.getInstance().setPairingId(message.userPairingId);
setAuthSuccess(true); setAuthSuccess(true);
setTimeout(() => { setTimeout(() => {
setShowIframe(false); setShowIframe(false);
setIsIframeReady(false); setIsIframeReady(false);
@ -93,28 +106,21 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
}, 500); }, 500);
break; break;
} }
case 'ERROR': { case 'ERROR': {
console.error('[AuthModal] handleMessage: error', message); console.error('[AuthModal] handleMessage: error', message);
if (message.messageId.includes('GET_PAIRING_ID')) { if (message.messageId?.includes('GET_PAIRING_ID')) {
// We are not paired yet
const accessToken = User.getInstance().getAccessToken(); const accessToken = User.getInstance().getAccessToken();
if (accessToken) { if (accessToken) {
// create a new pairing
const messageId = `CREATE_PAIRING_${uuidv4()}`; const messageId = `CREATE_PAIRING_${uuidv4()}`;
iframeRef.current.contentWindow!.postMessage({ type: 'CREATE_PAIRING', accessToken, messageId }, targetOrigin); iframeRef.current.contentWindow!.postMessage({ type: 'CREATE_PAIRING', accessToken, messageId }, targetOrigin);
} else { } else {
// We don't have an access token console.error("[AuthModal] handleMessage: error: we don't have an access token");
// Shouldn't happen
console.error('[AuthModal] handleMessage: error: we don\'t have an access token');
setShowIframe(false); setShowIframe(false);
setIsIframeReady(false); setIsIframeReady(false);
setAuthSuccess(false); setAuthSuccess(false);
onClose(); onClose();
} }
} else if (message.messageId.includes('CREATE_PAIRING')) { } else if (message.messageId?.includes('CREATE_PAIRING')) {
// Something went wrong while creating a pairing
// show stopper for now
console.error('[AuthModal] CREATE_PAIRING error:', message.error); console.error('[AuthModal] CREATE_PAIRING error:', message.error);
setShowIframe(false); setShowIframe(false);
setIsIframeReady(false); setIsIframeReady(false);
@ -125,8 +131,8 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
} }
} }
}; };
window.addEventListener('message', handleMessage);
window.addEventListener('message', handleMessage);
return () => { return () => {
window.removeEventListener('message', handleMessage); window.removeEventListener('message', handleMessage);
}; };
@ -178,17 +184,19 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
alignItems: 'center', alignItems: 'center',
width: '100%' width: '100%'
}}> }}>
<iframe {iframeSrc && (
ref={iframeRef} <iframe
src={IframeReference.getIframeUrl()} ref={iframeRef}
style={{ src={iframeSrc}
display: showIframe ? 'block' : 'none', style={{
width: '400px', display: showIframe ? 'block' : 'none',
height: '400px', width: '400px',
border: 'none', height: '400px',
overflow: 'hidden' border: 'none',
}} overflow: 'hidden'
/> }}
/>
)}
</div> </div>
)} )}
</Modal> </Modal>

View File

@ -1,4 +1,4 @@
import { useRef, useEffect, memo } from 'react'; import { useRef, useEffect, memo, useState } from 'react';
import IframeReference from './IframeReference'; import IframeReference from './IframeReference';
interface IframeProps { interface IframeProps {
@ -7,17 +7,30 @@ interface IframeProps {
function Iframe({ showIframe = false }: IframeProps) { function Iframe({ showIframe = false }: IframeProps) {
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
// Client-side only: resolve iframe URL and set reference
try {
const url = IframeReference.getIframeUrl();
setIframeSrc(url);
} catch {
setIframeSrc(null);
}
if (iframeRef.current) { if (iframeRef.current) {
IframeReference.setIframe(iframeRef.current); IframeReference.setIframe(iframeRef.current);
} }
}, [iframeRef.current]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!iframeSrc) {
return null;
}
return ( return (
<iframe <iframe
ref={iframeRef} ref={iframeRef}
src={IframeReference.getIframeUrl()} src={iframeSrc}
style={{ style={{
display: showIframe ? 'block' : 'none', display: showIframe ? 'block' : 'none',
width: '400px', width: '400px',

View File

@ -22,10 +22,12 @@ export default class MessageBus {
} }
public initMessageListener(): void { public initMessageListener(): void {
if (typeof window === 'undefined') return;
window.addEventListener('message', this.handleMessage.bind(this)); window.addEventListener('message', this.handleMessage.bind(this));
} }
public destroyMessageListener(): void { public destroyMessageListener(): void {
if (typeof window === 'undefined') return;
window.removeEventListener('message', this.handleMessage.bind(this)); window.removeEventListener('message', this.handleMessage.bind(this));
this.isListening = false; // Reset the flag when destroying listener this.isListening = false; // Reset the flag when destroying listener
} }
@ -1003,7 +1005,9 @@ export default class MessageBus {
try { try {
const targetOrigin = IframeReference.getTargetOrigin(); const targetOrigin = IframeReference.getTargetOrigin();
const iframe = IframeReference.getIframe(); const iframe = IframeReference.getIframe();
iframe.contentWindow?.postMessage(message, targetOrigin); if (typeof window !== 'undefined') {
iframe.contentWindow?.postMessage(message, targetOrigin);
}
this.messagesSent.add(message.messageId); this.messagesSent.add(message.messageId);
} catch (error) { } catch (error) {
console.error('[MessageBus] sendMessage: error', error); console.error('[MessageBus] sendMessage: error', error);