import React, { useMemo, useRef, useEffect } from 'react'; import { View, StyleSheet, ImageBackground, Animated } from 'react-native'; import { usePaywallContext } from '../../context/PaywallContext'; import { applyStyles, childSpacingStyle, parseSizeOrPercent, resolveFillImageUrl } from '../../utils/styles'; import { TemplateRenderer } from '../TemplateRenderer'; import { getProductDetail, getSkuProductDetailKeys, skuItems, toNamiSKU } from '@namiml/sdk-core'; import type { TComponent, TProductContainer, PaywallSKU } from '@namiml/sdk-core'; import { buildSmartTextReplacements, interpolate } from '../../utils/smartText'; import { FocusedStyleProvider } from '../../context/FocusContext'; interface Props { component: TProductContainer; scaleFactor: number; onClose?: () => void; parentDirection?: string; } type InjectedSkuContext = { skuActionPayload: any; skuVars: Record; replacements: ReturnType; }; const AnimatedImageBackground = Animated.createAnimatedComponent(ImageBackground); export const NamiProductContainer: React.FC = ({ component, scaleFactor, onClose, parentDirection }) => { const ctx = usePaywallContext(); const containerStyle = useMemo(() => applyStyles(component, scaleFactor, false, parentDirection), [component, scaleFactor, parentDirection]); const opacity = useRef(new Animated.Value(1)).current; const prevGroupId = useRef(undefined); const skus = useMemo(() => { const groupRef = component.subsetGroup ?? ctx.state.currentGroupId; const skuMenus = ctx.state.selectedPaywall?.sku_menus ?? []; return skuItems(ctx.productDetails, skuMenus, groupRef); }, [ctx.productDetails, ctx.state.selectedPaywall, ctx.state.currentGroupId, component.subsetGroup]); const currentGroupId = component.subsetGroup ?? ctx.state.currentGroupId; const selectedId = (ctx.state.selectedProducts ?? {})[currentGroupId]; const fillImageUrl = resolveFillImageUrl(component.fillImage); const sizeKey = component.direction === 'vertical' ? 'width' : 'height'; const childSizeStyle = { [sizeKey]: '100%' as const }; const direction = component.direction ?? 'vertical'; useEffect(() => { if (prevGroupId.current != null && prevGroupId.current !== currentGroupId) { opacity.setValue(0); Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true, }).start(); } prevGroupId.current = currentGroupId; }, [currentGroupId, opacity]); const content = useMemo(() => ( <> {skus.map((sku: PaywallSKU, i: number) => { const isFeatured = sku.featured ?? false; const isSelected = sku.id === selectedId; const useFeaturedFocus = ctx.state.formFactor === 'television' && isFeatured; const injectedSkuContext = buildInjectedSkuContext(sku, isSelected, ctx); const template = isFeatured ? (component.productFeaturedComponents ?? component.components) : (component.productBaseComponents ?? component.components); if (!template?.length) return null; const spacingStyle = childSpacingStyle(i, { spacing: component.spacing, direction }, scaleFactor); const templatePrimary = template[0] as any; const primaryRawWidth = templatePrimary?.width ?? templatePrimary?.fixedWidth; const wrapperWidth = parseSizeOrPercent(primaryRawWidth, scaleFactor); const shouldShareHorizontal = direction === 'horizontal' && primaryRawWidth == null; const shouldShrinkHorizontal = direction === 'horizontal' && ( primaryRawWidth == null || (typeof primaryRawWidth === 'string' && primaryRawWidth.trim().endsWith('%')) ); return ( {template.map((child: TComponent, j: number) => { const enriched = injectSkuIntoComponent(child, injectedSkuContext, useFeaturedFocus); return ( ); })} ); })} ), [ childSizeStyle, component.components, component.productBaseComponents, component.productFeaturedComponents, component.spacing, ctx, direction, onClose, scaleFactor, selectedId, skus, ]); const radiusStyles = pickBorderRadius(containerStyle); const shouldClip = fillImageUrl && Object.keys(radiusStyles).length > 0; if (fillImageUrl) { return ( {content} ); } return ( {content} ); }; const styles = StyleSheet.create({ relative: { position: 'relative' }, clip: { overflow: 'hidden' }, flexShare: { flex: 1 }, horizontalShrink: { minWidth: 0, flexShrink: 1 }, }); function pickBorderRadius(style: any): Record { if (!style) return {}; const keys = [ 'borderRadius', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius', ]; return keys.reduce((acc, key) => { const value = style[key]; if (typeof value === 'number') (acc as any)[key] = value; return acc; }, {} as Record); } function injectSkuIntoComponent( component: TComponent, injectedSkuContext: InjectedSkuContext, useFeaturedFocus: boolean, ): any { const nextComponent: any = { ...component, sku: injectedSkuContext.skuActionPayload, smartTextSku: injectedSkuContext.skuVars, ...(useFeaturedFocus && component.component === 'button' ? { focused: true } : {}), }; const childComponents = nextComponent.components; const collapseHeader = nextComponent.collapseHeader; const result: any = interpolate( { ...nextComponent, components: undefined, collapseHeader: collapseHeader ? { ...collapseHeader, components: undefined } : undefined, }, injectedSkuContext.replacements, ); result.__namiSmartTextResolved = true; if (childComponents) { result.components = childComponents.map((c: TComponent) => injectSkuIntoComponent(c, injectedSkuContext, useFeaturedFocus) ); } if (collapseHeader?.components) { result.collapseHeader = { ...(result.collapseHeader ?? collapseHeader), components: collapseHeader.components.map((c: TComponent) => injectSkuIntoComponent(c, injectedSkuContext, useFeaturedFocus) ), }; } return result; } function buildInjectedSkuContext( sku: PaywallSKU, isSelected: boolean, ctx: any, ): InjectedSkuContext { const entitlements = (sku.entitlements ?? []).map((e: any) => e.entitlement_ref_id); const skuActionPayload = toNamiSKU(sku); const detail = (sku as any).product_details ?? (sku as any).productDetails ?? null; const recomputedDetailValues = getSkuProductDetailKeys().reduce((output: Record, key: string) => { const value = getProductDetail(key, detail, undefined, [sku]); if (value !== null && value !== undefined) { output[key] = value; } return output; }, {}); const skuVars = { ...sku, ...recomputedDetailValues, ...(sku.variables ?? {}), freeTrialEligible: (sku as any).freeTrialEligible ?? false, introEligible: (sku as any).introEligible ?? false, promoEligible: (sku as any).promoEligible ?? false, invalid: (sku as any).invalid ?? false, unavailable: (sku as any).unavailable ?? false, entitlements, selected: isSelected, }; return { skuActionPayload, skuVars, replacements: buildSmartTextReplacements(ctx.state, ctx.flow, skuVars), }; }