Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { appConfig } from '@/lib/config'
import { useConsent, useFlagSubscription } from '@/lib/hooks'
import {
useLiveUpdates,
useOptimization,
useOptimizationActions,
useOptimizationContext,
useProfileState,
useSelectedOptimizationsState,
} from '@contentful/optimization-nextjs/client'
import { type JSX } from 'react'

export function ControlPanel({ demoCTA }: { readonly demoCTA?: boolean } = {}): JSX.Element {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()
const { identify, reset } = useOptimizationActions()
const { consent, setConsent } = useConsent()
const profile = useProfileState()
Expand Down Expand Up @@ -164,6 +164,7 @@ export function ControlPanel({ demoCTA }: { readonly demoCTA?: boolean } = {}):
className="btn btn--secondary btn--sm"
data-testid="track-conversion-button"
onClick={() => {
if (!sdk) return
void sdk.trackView({
componentId: 'page-two-demo-cta',
viewId: crypto.randomUUID(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use client'

import { useOptimization } from '@contentful/optimization-nextjs/client'
import { useOptimizationContext } from '@contentful/optimization-nextjs/client'
import { useEffect } from 'react'

export function CustomViewTracker({ componentId }: { readonly componentId: string }): null {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()

useEffect(() => {
if (!sdk) return
void sdk.trackView({ componentId, viewId: crypto.randomUUID(), viewDurationMs: 0 })
}, [sdk, componentId])

Expand Down
8 changes: 4 additions & 4 deletions implementations/nextjs-sdk_hybrid/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import {
useConsentState,
useOptimization,
useOptimizationActions,
useOptimizationContext,
} from '@contentful/optimization-nextjs/client'
Expand Down Expand Up @@ -92,18 +91,19 @@ export function useFlagSubscription(flagName: string): unknown {
export function useManualViewTracking(
manualTracking: boolean | undefined,
): (element: HTMLDivElement | null, entryId: string) => void {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()
const trackedElement = useRef<HTMLDivElement | null>(null)

useEffect(
() => () => {
const { current } = trackedElement
if (current) sdk.tracking.clearElement('views', current)
if (current) sdk?.tracking.clearElement('views', current)
},
[sdk.tracking],
[sdk?.tracking],
)

return (element: HTMLDivElement | null, entryId: string): void => {
if (!sdk) return
const { current: previous } = trackedElement
if (previous && previous !== element) sdk.tracking.clearElement('views', previous)
trackedElement.current = element
Expand Down
5 changes: 3 additions & 2 deletions implementations/nextjs-sdk_ssr/components/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { appConfig } from '@/lib/config'
import { useConsent, useFlagSubscription } from '@/lib/hooks'
import {
useLiveUpdates,
useOptimization,
useOptimizationActions,
useOptimizationContext,
useProfileState,
useSelectedOptimizationsState,
} from '@contentful/optimization-nextjs/client'
import type { JSX } from 'react'

export function ControlPanel({ demoCTA }: { readonly demoCTA?: boolean } = {}): JSX.Element {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()
const { identify, reset } = useOptimizationActions()
const { consent, setConsent } = useConsent()
const profile = useProfileState()
Expand Down Expand Up @@ -164,6 +164,7 @@ export function ControlPanel({ demoCTA }: { readonly demoCTA?: boolean } = {}):
className="btn btn--secondary btn--sm"
data-testid="track-conversion-button"
onClick={() => {
if (!sdk) return
void sdk.trackView({
componentId: 'page-two-demo-cta',
viewId: crypto.randomUUID(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use client'

import { useOptimization } from '@contentful/optimization-nextjs/client'
import { useOptimizationContext } from '@contentful/optimization-nextjs/client'
import { useEffect } from 'react'

export function CustomViewTracker({ componentId }: { readonly componentId: string }): null {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()

useEffect(() => {
if (!sdk) return
void sdk.trackView({ componentId, viewId: crypto.randomUUID(), viewDurationMs: 0 })
}, [sdk, componentId])

Expand Down
8 changes: 4 additions & 4 deletions implementations/nextjs-sdk_ssr/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import {
useConsentState,
useOptimization,
useOptimizationActions,
useOptimizationContext,
} from '@contentful/optimization-nextjs/client'
Expand Down Expand Up @@ -92,18 +91,19 @@ export function useFlagSubscription(flagName: string): unknown {
export function useManualViewTracking(
manualTracking: boolean | undefined,
): (element: HTMLDivElement | null, entryId: string) => void {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()
const trackedElement = useRef<HTMLDivElement | null>(null)

useEffect(
() => () => {
const { current } = trackedElement
if (current) sdk.tracking.clearElement('views', current)
if (current) sdk?.tracking.clearElement('views', current)
},
[sdk.tracking],
[sdk?.tracking],
)

return (element: HTMLDivElement | null, entryId: string): void => {
if (!sdk) return
const { current: previous } = trackedElement
if (previous && previous !== element) sdk.tracking.clearElement('views', previous)
trackedElement.current = element
Expand Down
5 changes: 3 additions & 2 deletions packages/web/frameworks/nextjs-sdk/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useOptimization } from '@contentful/optimization-react-web'
import { useOptimizationContext } from '@contentful/optimization-react-web'
import type { OptimizationData } from '@contentful/optimization-web/api-schemas'
import { hydrateOptimizationData } from '@contentful/optimization-web/bridge-support'
import { useLayoutEffect } from 'react'
Expand All @@ -22,9 +22,10 @@ export interface NextjsOptimizationStateProps {
}

export function NextjsOptimizationState({ data }: NextjsOptimizationStateProps): null {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()

useLayoutEffect(() => {
if (!sdk) return
void hydrateOptimizationData(sdk, data)
}, [data, sdk])

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { useOptimization } from '../hooks/useOptimization'
import { useOptimizationContext } from '../hooks/useOptimization'
import { useConsentState } from '../hooks/useOptimizationState'
import type { AutoPagePayload } from './types'

Expand Down Expand Up @@ -51,11 +51,11 @@ export function useAutoPageEmitter({
routeKey,
buildPayload,
}: UseAutoPageEmitterArgs): void {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()
const consent = useConsentState()

useEffect(() => {
if (!enabled) {
if (!enabled || !sdk) {
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ResolvedData } from '@contentful/optimization-web/core-sdk'
import type { Entry, EntrySkeletonType } from 'contentful'
import { useMemo } from 'react'

import { useOptimization } from './useOptimization'
import { useOptimizationContext } from './useOptimization'

/**
* Helper methods for resolving Contentful entries against selected optimizations.
Expand Down Expand Up @@ -31,13 +31,21 @@ export interface UseEntryResolverResult {
) => ResolvedData<EntrySkeletonType>
}

function toBaselineResolvedData(entry: Entry): ResolvedData<EntrySkeletonType> {
return { entry, selectedOptimization: undefined }
}

/**
* Returns entry-resolution helpers for React components.
*
* @remarks
* When `selectedOptimizations` is omitted, helpers use the current SDK
* `states.selectedOptimizations` value.
*
* SSR-safe: when the SDK is not yet ready the helpers return the unmodified
* baseline entry so server-rendered content matches the client-hydrated
* baseline before optimizations resolve.
*
* @example
* ```tsx
* const { resolveEntry } = useEntryResolver()
Expand All @@ -47,16 +55,16 @@ export interface UseEntryResolverResult {
* @public
*/
export function useEntryResolver(): UseEntryResolverResult {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()

return useMemo<UseEntryResolverResult>(
() => ({
resolveOptimizedEntry: (entry: Entry, selectedOptimizations?: SelectedOptimizationArray) =>
sdk.resolveOptimizedEntry(entry, selectedOptimizations),
sdk?.resolveOptimizedEntry(entry, selectedOptimizations) ?? toBaselineResolvedData(entry),
resolveEntry: (entry: Entry, selectedOptimizations?: SelectedOptimizationArray) =>
sdk.resolveOptimizedEntry(entry, selectedOptimizations).entry,
sdk?.resolveOptimizedEntry(entry, selectedOptimizations).entry ?? entry,
resolveEntryData: (entry: Entry, selectedOptimizations?: SelectedOptimizationArray) =>
sdk.resolveOptimizedEntry(entry, selectedOptimizations),
sdk?.resolveOptimizedEntry(entry, selectedOptimizations) ?? toBaselineResolvedData(entry),
}),
[sdk],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react'

import type { OptimizationSdk } from '../context/OptimizationContext'
import { useOptimization } from './useOptimization'
import { useOptimizationContext } from './useOptimization'

/**
* Helper methods for resolving Contentful merge tag entries against the current visitor profile.
Expand All @@ -18,6 +18,9 @@ export interface UseMergeTagResolverResult {
/**
* Returns merge-tag resolution helpers for React components.
*
* @remarks
* SSR-safe: when the SDK is not yet ready the resolver returns `undefined`.
*
* @example
* ```tsx
* const { getMergeTagValue } = useMergeTagResolver()
Expand All @@ -27,12 +30,12 @@ export interface UseMergeTagResolverResult {
* @public
*/
export function useMergeTagResolver(): UseMergeTagResolverResult {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()

return useMemo<UseMergeTagResolverResult>(
() => ({
getMergeTagValue: (embeddedEntryNodeTarget, profile) =>
sdk.getMergeTagValue(embeddedEntryNodeTarget, profile),
sdk?.getMergeTagValue(embeddedEntryNodeTarget, profile),
}),
[sdk],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react'

import type { OptimizationSdk } from '../context/OptimizationContext'
import { useOptimization } from './useOptimization'
import { useOptimizationContext } from './useOptimization'

/**
* Bound Optimization SDK actions safe to destructure in React components.
Expand All @@ -21,40 +21,38 @@ export interface UseOptimizationActionsResult {
/**
* Returns bound Optimization SDK actions that are safe to destructure.
*
* @remarks
* SSR-safe: when the SDK is not yet ready (server render or initial
* synchronous client render) the returned actions no-op and event-emitting
* actions resolve to `{ accepted: false }`. Once the SDK is ready subsequent
* calls invoke the real methods.
*
* @example
* ```tsx
* const { track, screen, flush, consent, reset } = useOptimizationActions()
* await track({ event: 'purchase' })
* await screen({ name: 'Cart' })
* await flush()
* consent(true)
* reset()
* ```
*
* @remarks
* This hook does not create a new SDK instance. It binds the most common
* actions from the existing SDK instance returned by `useOptimization()`.
*
* @public
*/
export function useOptimizationActions(): UseOptimizationActionsResult {
const sdk = useOptimization()
const { sdk } = useOptimizationContext()

return useMemo<UseOptimizationActionsResult>(
() => ({
consent: (value) => {
sdk.consent(value)
sdk?.consent(value)
},
flush: async () => {
await sdk.flush()
await sdk?.flush()
},
identify: async (payload) => await sdk.identify(payload),
page: async (payload) => await sdk.page(payload),
identify: async (payload) => (await sdk?.identify(payload)) ?? { accepted: false },
page: async (payload) => (await sdk?.page(payload)) ?? { accepted: false },
reset: () => {
sdk.reset()
sdk?.reset()
},
screen: async (payload) => await sdk.screen(payload),
track: async (payload) => await sdk.track(payload),
screen: async (payload) => (await sdk?.screen(payload)) ?? { accepted: false },
track: async (payload) => (await sdk?.track(payload)) ?? { accepted: false },
}),
[sdk],
)
Expand Down
Loading