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 {
if (typeof window === 'undefined' || typeof document === 'undefined') {
// SSR: do not bind browser events
return;
}
window.addEventListener("scroll", (e: Event) => this.scrollYHandler());
window.addEventListener("resize", (e: Event) => this.resizeHandler());
document.addEventListener("click", (e: MouseEvent) => this.clickHandler(e), true);
@ -46,10 +50,11 @@ export default class WindowStore {
}
private scrollYHandler = (() => {
let previousY: number = window.scrollY;
let previousY: number = (typeof window !== 'undefined' ? window.scrollY : 0);
let snapShotY: number = previousY;
let previousYDirection: number = 1;
return (): void => {
if (typeof window === 'undefined') return;
const scrollYDirection = window.scrollY - previousY > 0 ? 1 : -1;
if (previousYDirection !== scrollYDirection) {
snapShotY = window.scrollY;
@ -62,6 +67,7 @@ export default class WindowStore {
})();
private resizeHandler() {
if (typeof window === 'undefined') return;
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 getConfig from "next/config";
import { GoogleTagManager } from "@next/third-parties/google";
import dynamic from 'next/dynamic';
import Loader from "src/common/Api/LeCoffreApi/sdk/Loader";
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 User from "src/sdk/User";
@ -80,26 +82,31 @@ const MyApp = (({
const [isConnected, setIsConnected] = useState(false);
const [isReady, setIsReady] = useState(false);
const [mounted, setMounted] = useState(false);
const targetOrigin = (() => {
try {
return new URL(_4nkUrl).origin;
} catch {
return _4nkUrl;
}
})();
IframeReference.setTargetOrigin(targetOrigin);
// Configure iframe target and URL on client-side only
useEffect(() => {
setMounted(true);
try {
if (_4nkUrl && typeof _4nkUrl === 'string' && _4nkUrl.trim().length > 0) {
const origin = (() => {
try { return new URL(_4nkUrl).origin; } catch { return _4nkUrl; }
})();
IframeReference.setTargetOrigin(origin);
// Configure full iframe URL if provided (falls back to origin)
const iframeUrl = (() => {
const candidate = (publicRuntimeConfig as any).NEXT_PUBLIC_4NK_IFRAME_URL ?? _4nkUrl;
try {
return new URL(candidate).toString();
} catch {
return targetOrigin;
}
})();
IframeReference.setIframeUrl(iframeUrl);
const candidate = (publicRuntimeConfig as any).NEXT_PUBLIC_4NK_IFRAME_URL ?? _4nkUrl;
const iframe = (() => {
try { return new URL(candidate).toString(); } catch { return origin; }
})();
IframeReference.setIframeUrl(iframe);
} else {
console.warn('[MyApp] NEXT_PUBLIC_4NK_URL is missing; skipping iframe setup');
}
} catch (e) {
console.error('[MyApp] Failed to initialize IframeReference', e);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const isAuthenticated = User.getInstance().isAuthenticated();
@ -117,7 +124,12 @@ const MyApp = (({
return () => { };
}, []);
// Hotjar supprimé
// Empêcher le rendu SSR du contenu qui dépend du navigateur
if (!mounted) {
return <Loader />;
}
// Hotjar supprimé
return getLayout(
<>
@ -126,7 +138,7 @@ const MyApp = (({
<GoogleTagManager gtmId="GTM-5GLJN86P" />
</Component>
}
{isConnected && <Iframe />}
{isConnected && <IframeNoSSR />}
<Loader />
</>
);
@ -147,7 +159,7 @@ MyApp.getInitialProps = async () => {
idNotRedirectUri: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_REDIRECT_URI,
fcAuthorizeEndpoint: publicRuntimeConfig.NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT,
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,
_4nkIframeUrl: publicRuntimeConfig.NEXT_PUBLIC_4NK_IFRAME_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 [showIframe, setShowIframe] = useState(false);
const [authSuccess, setAuthSuccess] = useState(false);
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
// Initialize iframe URL client-side only
useEffect(() => {
try {
const url = IframeReference.getIframeUrl();
setIframeSrc(url);
} catch {
setIframeSrc(null);
}
}, []);
useEffect(() => {
if (typeof window === 'undefined') return;
const handleMessage = (event: MessageEvent) => {
if (!event.data || event.data.type === 'PassClientScriptReady') {
return;
@ -31,15 +44,20 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
return;
}
const targetOrigin = IframeReference.getTargetOrigin();
if (!targetOrigin) {
let targetOrigin: string | null = null;
try {
targetOrigin = IframeReference.getTargetOrigin();
} catch {
console.error('[AuthModal] handleMessage: targetOrigin not found');
return;
}
if (!targetOrigin) {
return;
}
// Accepter seulement les messages de l'iframe (même origine)
if (event.origin !== targetOrigin) {
// Ignorer silencieusement les messages des DevTools et autres sources
return;
}
@ -58,7 +76,6 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
setIsIframeReady(true);
break;
}
case 'LINK_ACCEPTED': {
setShowIframe(false);
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);
break;
}
case 'PAIRING_CREATED': {
console.log('[AuthModal] PAIRING_CREATED:', message);
User.getInstance().setPairingId(message.userPairingId);
setAuthSuccess(true);
setTimeout(() => {
setShowIframe(false);
setIsIframeReady(false);
@ -80,11 +95,9 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
}, 500);
break;
}
case 'GET_PAIRING_ID': {
User.getInstance().setPairingId(message.userPairingId);
setAuthSuccess(true);
setTimeout(() => {
setShowIframe(false);
setIsIframeReady(false);
@ -93,28 +106,21 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
}, 500);
break;
}
case 'ERROR': {
console.error('[AuthModal] handleMessage: error', message);
if (message.messageId.includes('GET_PAIRING_ID')) {
// We are not paired yet
if (message.messageId?.includes('GET_PAIRING_ID')) {
const accessToken = User.getInstance().getAccessToken();
if (accessToken) {
// create a new pairing
const messageId = `CREATE_PAIRING_${uuidv4()}`;
iframeRef.current.contentWindow!.postMessage({ type: 'CREATE_PAIRING', accessToken, messageId }, targetOrigin);
} else {
// We don't have an access token
// Shouldn't happen
console.error('[AuthModal] handleMessage: error: we don\'t have an access token');
console.error("[AuthModal] handleMessage: error: we don't have an access token");
setShowIframe(false);
setIsIframeReady(false);
setAuthSuccess(false);
onClose();
}
} else if (message.messageId.includes('CREATE_PAIRING')) {
// Something went wrong while creating a pairing
// show stopper for now
} else if (message.messageId?.includes('CREATE_PAIRING')) {
console.error('[AuthModal] CREATE_PAIRING error:', message.error);
setShowIframe(false);
setIsIframeReady(false);
@ -125,8 +131,8 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
}
}
};
window.addEventListener('message', handleMessage);
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
@ -178,17 +184,19 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
alignItems: 'center',
width: '100%'
}}>
<iframe
ref={iframeRef}
src={IframeReference.getIframeUrl()}
style={{
display: showIframe ? 'block' : 'none',
width: '400px',
height: '400px',
border: 'none',
overflow: 'hidden'
}}
/>
{iframeSrc && (
<iframe
ref={iframeRef}
src={iframeSrc}
style={{
display: showIframe ? 'block' : 'none',
width: '400px',
height: '400px',
border: 'none',
overflow: 'hidden'
}}
/>
)}
</div>
)}
</Modal>

View File

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

View File

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