Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/pink-friends-invite.md
Original file line number Diff line number Diff line change
@@ -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.
75 changes: 67 additions & 8 deletions packages/monocle-react/src/contexts/MonocleProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import React, {
createContext,
useContext,
useEffect,
useState,
useMemo,
useCallback,
} from 'react';
import { withMaxAllowedInstancesGuard } from '../utils';
import { MonocleProviderProps } from '../types';
import { DOMAIN } from '../constants';
Expand Down Expand Up @@ -50,35 +57,69 @@ const MonocleProviderComponent: React.FC<MonocleProviderProps> = ({
});
};

const refresh = async () => {
const refresh = useCallback(async () => {
try {
setIsLoading(true);
setError(null);
await loadScript();
if (window.MCL) {
const newAssessment = window.MCL.getAssessment();
setAssessment(newAssessment);
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);
},
});

// Check if assessment is already available
const existingAssessment = window.MCL.getAssessment();
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);
}
} else {
throw new Error('MCL object not found on window');
}
} catch (err) {
setError(
err instanceof Error ? err : new Error('Unknown error occurred')
);
} finally {
setIsLoading(false);
}
};
}, [publishableKey, domain]);

useEffect(() => {
// Only refresh if the publishableKey changes and we don't already have an assessment
if (!assessment) {
refresh();
}
}, [publishableKey]);

// 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, refresh, isLoading, error]
);

return (
<MonocleContext.Provider value={{ assessment, refresh, isLoading, error }}>
<MonocleContext.Provider value={contextValue}>
{children}
</MonocleContext.Provider>
);
Expand All @@ -90,6 +131,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 <div>Loading...</div>;
* if (error) return <div>Error: {error.message}</div>;
*
* return <div>Assessment: {assessment}</div>;
* }
* ```
*/
export const useMonocle = () => {
const context = useContext(MonocleContext);
if (!context) {
Expand Down
2 changes: 2 additions & 0 deletions packages/monocle-react/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

/**
* Props for the MonocleProvider component.
* @interface MonocleProviderProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};
}, []);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down