import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; import { View, ScrollView, Dimensions, StyleSheet, NativeSyntheticEvent, NativeScrollEvent, TouchableOpacity, } from 'react-native'; import { usePaywallContext } from '../../context/PaywallContext'; import { applyStyles, parseColor, parseSize } from '../../utils/styles'; import { TemplateRenderer } from '../TemplateRenderer'; import type { TComponent, TCarouselContainer, TCarouselSlide } from '@namiml/sdk-core'; import { skuItems, toNamiSKU } from '@namiml/sdk-core'; import { buildSmartTextReplacements, interpolate } from '../../utils/smartText'; interface Props { component: TCarouselContainer; scaleFactor: number; onClose?: () => void; parentDirection?: string; } export const NamiCarousel: React.FC = ({ component, scaleFactor, onClose, parentDirection }) => { const ctx = usePaywallContext(); const scrollRef = useRef(null); const { width: screenWidth } = Dimensions.get('window'); const [activeIndex, setActiveIndex] = useState(0); const containerStyle = useMemo( () => applyStyles(component, scaleFactor, false, parentDirection), [component, scaleFactor, parentDirection] ); const direction = component.direction ?? 'horizontal'; const loopSourceKey = typeof component.loopSource === 'string' ? component.loopSource : ''; const slides = useMemo((): TCarouselSlide[] => { if (Array.isArray(component.loopSource)) { return component.loopSource; } const stateSlides = (ctx.state as any).slides; const fromState = loopSourceKey ? stateSlides?.[loopSourceKey]?.carouselName : undefined; if (Array.isArray(fromState)) { const productGroupIds = (ctx.filteredSkuMenus ?? []).map((menu: any) => menu.id); return fromState.filter((slide: any) => ( !slide?.productGroupId || productGroupIds.includes(slide.productGroupId) )); } if (component.components?.length) { return component.components.map((c, i) => ({ id: c.id ?? String(i), components: [c] })) as any; } return []; }, [component.loopSource, component.components, ctx.state, loopSourceKey, ctx.filteredSkuMenus]); const slideSkuPool = useMemo(() => { if (!loopSourceKey) return []; const skuMenus = ctx.state.selectedPaywall?.sku_menus ?? []; return skuItems(ctx.productDetails, skuMenus, loopSourceKey); }, [ctx.productDetails, ctx.state.selectedPaywall, loopSourceKey]); const nextPad = parseSize(component.nextSlidePadding, scaleFactor) ?? 0; const prevPad = parseSize(component.previousSlidePadding, scaleFactor) ?? 0; const slideSpacing = parseSize(component.slideSpacing, scaleFactor) ?? 0; const rawCarouselWidth = component.width ?? component.fixedWidth; const viewportWidth = useMemo(() => { if (rawCarouselWidth == null) return screenWidth; if (typeof rawCarouselWidth === 'string' && rawCarouselWidth.trim().endsWith('%')) { const pct = parseFloat(rawCarouselWidth); return Number.isFinite(pct) ? Math.max(1, (screenWidth * pct) / 100) : screenWidth; } const resolved = parseSize(rawCarouselWidth, scaleFactor); return resolved && resolved > 0 ? resolved : screenWidth; }, [rawCarouselWidth, scaleFactor, screenWidth]); const slideWidth = Math.max(1, viewportWidth - nextPad - prevPad); const applySlideChange = useCallback((index: number) => { setActiveIndex(index); ctx.setCurrentSlideIndex(index); const slide = slides[index]; const selectedProducts = ctx.state.selectedProducts ?? {}; const selectedSkuId = slide?.productGroupId ? selectedProducts[slide.productGroupId] : undefined; const selectedSku = selectedSkuId ? slideSkuPool.find((sku: any) => sku.id === selectedSkuId) : undefined; const skuVars = selectedSku ? { ...toNamiSKU(selectedSku), ...(selectedSku.variables ?? {}) } : ctx.state.sku; const replacements = buildSmartTextReplacements(ctx.state, ctx.flow, skuVars); const resolvedOnChange = component.onChange ? interpolate(component.onChange as any, { ...replacements, slide, sku: skuVars }) : undefined; const targetGroupId = (resolvedOnChange as any)?.parameters?.partialState?.currentGroupId; if (targetGroupId) { ctx.setCurrentGroupData(targetGroupId, ''); } }, [ctx, component.onChange, slides, slideSkuPool]); const getSlideComponents = useCallback((slide: any): TComponent[] => { if (Array.isArray(slide?.components) && slide.components.length > 0) { return slide.components as TComponent[]; } if (!component.components?.length) return []; const selectedProducts = ctx.state.selectedProducts ?? {}; const selectedSkuId = slide?.productGroupId ? selectedProducts[slide.productGroupId] : undefined; const selectedSku = selectedSkuId ? slideSkuPool.find((sku: any) => sku.id === selectedSkuId) : undefined; const skuVars = selectedSku ? { ...toNamiSKU(selectedSku), ...(selectedSku.variables ?? {}) } : ctx.state.sku; const replacements = buildSmartTextReplacements(ctx.state, ctx.flow, skuVars); return interpolate(component.components as any, { ...replacements, slide, sku: skuVars }) as TComponent[]; }, [component.components, ctx.state, ctx.flow, slideSkuPool]); const onScroll = useCallback((e: NativeSyntheticEvent) => { const offset = e.nativeEvent.contentOffset.x; const idx = Math.round(offset / (slideWidth + slideSpacing)); if (idx !== activeIndex && idx >= 0 && idx < slides.length) { applySlideChange(idx); } }, [slideWidth, slideSpacing, activeIndex, slides.length, applySlideChange]); useEffect(() => { if (!component.autoplay || !component.autoplaySeconds || slides.length <= 1) return; const interval = setInterval(() => { const next = (activeIndex + 1) % slides.length; scrollRef.current?.scrollTo({ x: next * (slideWidth + slideSpacing), animated: true }); applySlideChange(next); }, component.autoplaySeconds * 1000); return () => clearInterval(interval); }, [component.autoplay, component.autoplaySeconds, slides.length, slideWidth, slideSpacing, applySlideChange, activeIndex]); useEffect(() => { if (slides.length === 0) return; if (activeIndex < slides.length) return; applySlideChange(0); scrollRef.current?.scrollTo({ x: 0, animated: false }); }, [slides.length, activeIndex, applySlideChange]); const showIndicators = component.showIndicators !== false && slides.length > 1; const activeIndicatorColor = parseColor(component.activeIndicatorColor) ?? '#007AFF'; const indicatorColor = parseColor(component.indicatorColor) ?? '#ccc'; const indicator = component.indicator; return ( {slides.map((slide: any, i: number) => ( {getSlideComponents(slide).map((child: TComponent, j: number) => ( ))} ))} {showIndicators && ( {slides.map((_: any, i: number) => ( { scrollRef.current?.scrollTo({ x: i * (slideWidth + slideSpacing), animated: true }); applySlideChange(i); }} > ))} )} ); }; const styles = StyleSheet.create({ container: { flexDirection: 'column' }, pagination: { flexDirection: 'row', justifyContent: 'center', paddingVertical: 8 }, dotSpacing: { marginLeft: 6 }, dot: {}, });