From e2b94ed6f870be55940d21447cb8999ed8a9a8ea Mon Sep 17 00:00:00 2001 From: Jared Piedt Date: Wed, 2 Jul 2025 15:52:14 -0400 Subject: [PATCH 1/2] fix(monocle-types,monocle-react): Fix issue where assessment wasn't set in MonocleProvider --- .changeset/pink-friends-invite.md | 8 +++ .../src/contexts/MonocleProvider.tsx | 54 ++++++++++++++++--- packages/monocle-react/src/types.ts | 2 + .../src/utils/useMaxAllowedInstancesGuard.tsx | 3 +- packages/types/src/index.ts | 4 +- 5 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 .changeset/pink-friends-invite.md diff --git a/.changeset/pink-friends-invite.md b/.changeset/pink-friends-invite.md new file mode 100644 index 0000000..a54d172 --- /dev/null +++ b/.changeset/pink-friends-invite.md @@ -0,0 +1,8 @@ +--- +'@spur.us/monocle-react': patch +'@spur.us/types': patch +--- + +Fix `onAssessment` function signature in `MonocleConfig` type. + +Fix `MonocleProvider` issue where assessment isn't set. diff --git a/packages/monocle-react/src/contexts/MonocleProvider.tsx b/packages/monocle-react/src/contexts/MonocleProvider.tsx index 08990ab..97c411b 100644 --- a/packages/monocle-react/src/contexts/MonocleProvider.tsx +++ b/packages/monocle-react/src/contexts/MonocleProvider.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; +import React, { + createContext, + useContext, + useEffect, + useState, + useMemo, +} from 'react'; import { withMaxAllowedInstancesGuard } from '../utils'; import { MonocleProviderProps } from '../types'; import { DOMAIN } from '../constants'; @@ -56,8 +62,22 @@ const MonocleProviderComponent: React.FC = ({ setError(null); await loadScript(); if (window.MCL) { - const newAssessment = window.MCL.getAssessment(); - setAssessment(newAssessment); + // Configure MCL with our callback to receive assessment updates + await window.MCL.configure({ + onAssessment: (assessment: string) => { + setAssessment(assessment); + setIsLoading(false); + }, + }); + + // Check if assessment is already available + const existingAssessment = window.MCL.getAssessment(); + if (existingAssessment) { + setAssessment(existingAssessment); + setIsLoading(false); + } + // If no existing assessment, the onAssessment callback will be called + // when MCL completes its initialization } else { throw new Error('MCL object not found on window'); } @@ -65,7 +85,6 @@ const MonocleProviderComponent: React.FC = ({ setError( err instanceof Error ? err : new Error('Unknown error occurred') ); - } finally { setIsLoading(false); } }; @@ -75,10 +94,15 @@ const MonocleProviderComponent: React.FC = ({ if (!assessment) { refresh(); } - }, [publishableKey]); + }, [publishableKey, assessment]); + + const contextValue = useMemo( + () => ({ assessment, refresh, isLoading, error }), + [assessment, isLoading, error] + ); return ( - + {children} ); @@ -90,6 +114,24 @@ export const MonocleProvider = withMaxAllowedInstancesGuard( 'Only one instance of MonocleProvider is allowed' ); +/** + * Hook to access the Monocle context. + * + * @returns {MonocleContextType} The Monocle context containing assessment data, loading state, and error information + * @throws {Error} When used outside of a MonocleProvider + * + * @example + * ```tsx + * function MyComponent() { + * const { assessment, isLoading, error, refresh } = useMonocle(); + * + * if (isLoading) return
Loading...
; + * if (error) return
Error: {error.message}
; + * + * return
Assessment: {assessment}
; + * } + * ``` + */ export const useMonocle = () => { const context = useContext(MonocleContext); if (!context) { diff --git a/packages/monocle-react/src/types.ts b/packages/monocle-react/src/types.ts index 042bfae..37e9c06 100644 --- a/packages/monocle-react/src/types.ts +++ b/packages/monocle-react/src/types.ts @@ -1,3 +1,5 @@ +import React from 'react'; + /** * Props for the MonocleProvider component. * @interface MonocleProviderProps diff --git a/packages/monocle-react/src/utils/useMaxAllowedInstancesGuard.tsx b/packages/monocle-react/src/utils/useMaxAllowedInstancesGuard.tsx index 00e541c..c65ab90 100644 --- a/packages/monocle-react/src/utils/useMaxAllowedInstancesGuard.tsx +++ b/packages/monocle-react/src/utils/useMaxAllowedInstancesGuard.tsx @@ -32,7 +32,8 @@ export function useMaxAllowedInstancesGuard( instanceCounter.set(name, count + 1); return () => { - instanceCounter.set(name, (instanceCounter.get(name) || 1) - 1); + const currentCount = instanceCounter.get(name) || 0; + instanceCounter.set(name, Math.max(0, currentCount - 1)); }; }, []); } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f15fc2f..84a81b4 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -64,10 +64,10 @@ export interface MonocleLoaderConfig extends MonocleConfig { export interface MonocleConfig { /** * Configure a function handler to be called when a new assessment is received for monocle. - * @param assessment + * @param assessment The raw assessment string (encrypted bundle) * @returns */ - onAssessment?: (assessment: MonocleAssessment) => void; + onAssessment?: (assessment: string) => void; /** * @deprecated Use onAssessment instead * Configure a function handler to be called when a new bundle is received for monocle. From 35132682b60e54202891724b938e7d0ce0697a62 Mon Sep 17 00:00:00 2001 From: Jared Piedt Date: Wed, 2 Jul 2025 16:08:10 -0400 Subject: [PATCH 2/2] add more improvements --- .../src/contexts/MonocleProvider.tsx | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/monocle-react/src/contexts/MonocleProvider.tsx b/packages/monocle-react/src/contexts/MonocleProvider.tsx index 97c411b..0860fbc 100644 --- a/packages/monocle-react/src/contexts/MonocleProvider.tsx +++ b/packages/monocle-react/src/contexts/MonocleProvider.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState, useMemo, + useCallback, } from 'react'; import { withMaxAllowedInstancesGuard } from '../utils'; import { MonocleProviderProps } from '../types'; @@ -56,15 +57,20 @@ const MonocleProviderComponent: React.FC = ({ }); }; - const refresh = async () => { + const refresh = useCallback(async () => { try { setIsLoading(true); setError(null); await loadScript(); if (window.MCL) { + let timeoutId: NodeJS.Timeout | null = null; + // Configure MCL with our callback to receive assessment updates await window.MCL.configure({ onAssessment: (assessment: string) => { + if (timeoutId) { + clearTimeout(timeoutId); + } setAssessment(assessment); setIsLoading(false); }, @@ -75,9 +81,13 @@ const MonocleProviderComponent: React.FC = ({ if (existingAssessment) { setAssessment(existingAssessment); setIsLoading(false); + } else { + // Set a timeout in case the assessment never comes + timeoutId = setTimeout(() => { + setError(new Error('Assessment timeout - MCL did not respond within 30 seconds')); + setIsLoading(false); + }, 30000); } - // If no existing assessment, the onAssessment callback will be called - // when MCL completes its initialization } else { throw new Error('MCL object not found on window'); } @@ -87,18 +97,25 @@ const MonocleProviderComponent: React.FC = ({ ); setIsLoading(false); } - }; + }, [publishableKey, domain]); useEffect(() => { // Only refresh if the publishableKey changes and we don't already have an assessment if (!assessment) { refresh(); } - }, [publishableKey, assessment]); + + // Cleanup function to reset callback on unmount + return () => { + if (window.MCL) { + window.MCL.configure({ onAssessment: undefined }); + } + }; + }, [publishableKey, domain, assessment, refresh]); const contextValue = useMemo( () => ({ assessment, refresh, isLoading, error }), - [assessment, isLoading, error] + [assessment, refresh, isLoading, error] ); return (