1import{useState,useMemo,useEffect,useRef}from"preact/hooks";import{html}from"htm/preact";import{useStorage}from"onboarding-use-storage";import{dispatchStepEvent}from"onboarding-dispatch-event";import{storeOnboardingData}from"onboarding-storage-utils";import{ONBOARDING_EVENTS,ONBOARDING_STORAGE_KEYS,CTA_GROUPS}from"onboarding-step-constants";import{interpolateAllPlaceholders}from"onboarding-utils";export function App({blockSettings:e,stepId:t,sectionId:i,namespace:n="onboarding",onSelectionChange:o,selectedDropdownBlock:l,isLoading:s=!1}){const[r]=useStorage(n,ONBOARDING_STORAGE_KEYS.STEP_DATA),[a]=useStorage(n,ONBOARDING_STORAGE_KEYS.CHECKOUT_CONTEXT_BASE),[c]=useStorage(n,ONBOARDING_STORAGE_KEYS.ELIGIBLE_DISCOUNTS),[d]=useStorage(n,ONBOARDING_STORAGE_KEYS.ONBOARDING_DISCOUNTS),[p]=useStorage(n,ONBOARDING_STORAGE_KEYS.CURRENT_STEP_STATE),[u,m]=useState(null),f=useRef(null),g=p?.currentStepId===t,x=a?.products,h=useMemo((()=>{const t=e?.filter((e=>"cta"===e._type))||[],i=t.filter((e=>e.cta_group===CTA_GROUPS.PRIMARY));return i.length>0?i:t.slice(0,3)}),[e]);if(0===h.length)throw new Error("Plan selector template requires at least one CTA.");useEffect((()=>{if(void 0!==p&&!g)return;if(0===h.length)return;const e=`${t}_${i}`;if(f.current===e)return;let n=-1;if(r&&r.length>0){let e=0;h.forEach(((t,i)=>{if(!t.recommendations||0===t.recommendations.length)return;const o=t.recommendations.filter((e=>r.some((t=>t.field===e.field&&t.value===e.value)))).length;t.recommendations.every((e=>r.some((t=>t.field===e.field&&t.value===e.value))))&&o>e&&(e=o,n=i)}))}const s=-1===n;s&&(n=h.findIndex((e=>e.is_selected)));const a=-1===n?null:n,c=null!==a?h[a]:null;if(m(a),o?.(a,c?.recommendations||[]),s&&c){const e=[c._id];l?._id&&e.push(l._id),dispatchStepEvent({eventType:ONBOARDING_EVENTS.CTA_SELECTED,selectedBlockIds:e,stepId:t,stepDataKey:ONBOARDING_STORAGE_KEYS.STEP_DATA,sectionId:i})}f.current=e}),[p,g,r,t,i,h,o]),useEffect((()=>{g||(f.current=null)}),[g]);const v=e=>"number"==typeof e?e.toFixed(2):"0.00",_=useMemo((()=>{const e=r?.find((e=>"tier"===e.field));return e?.value?.replace("plan-","")||"good"}),[r]),b=useMemo((()=>e?.find((e=>"description_below_plan_selector"===e.key))),[e]),y=useMemo((()=>e?.find((e=>"text"===e._type&&"bestValueIndex"===e.key))),[e]),S=useMemo((()=>e?.find((e=>"text"===e._type&&"bestValueLabel"===e.key))),[e]),E=useMemo((()=>e?.find((e=>"image_picker"===e._type&&"bestValueImage"===e.key))),[e]),w=useMemo((()=>h.map(((e,t)=>{const i=e.title||`option-${t}`;let o=null,l=null,s=0,c=null;const p=e.recommendations?.find((e=>"quantity"===e.field))?.value||"1",u=e.recommendations?.find((e=>"bag_size"===e.field))?.value,m=u||(i.includes("Large")?"Big Bag":"Standard Bag"),f=x?.find((e=>{if(a?.tiers&&e.id){const t=a.tiers[e.id];if(t)return t===_}const t=e.handle?.toLowerCase();if(t?.includes("premium"))return"better"===_;if(t?.includes("classic"))return"good"===_;throw new Error(`[plan-selector] Unable to determine tier for product ${e.id} (${e.handle}). Handle must contain 'premium' or 'classic'. Checkout context tiers: ${JSON.stringify(a?.tiers)}`)})),g=f?.variants?.find((e=>e.title?.includes(m)||e.bagSize===m));let h=null;if(g?.price){const e=("Big Bag"===m?907:310)/28.3495;e>0&&(h=g.price/100/e)}if(!o){let t=null;if("undefined"!=typeof window&&window.onboardingDiscountSettings&&window.onboardingDiscountSettings[n||"onboarding"]){const i=window.onboardingDiscountSettings[n||"onboarding"];Object.values(i).forEach((i=>{!t&&i.blocks&&i.blocks.forEach((i=>{if(!t&&"plan_discount"===i.blockType){const n=new Set((e.recommendations||[]).map((e=>e.field))),o=[...(r||[]).filter((e=>!n.has(e.field))),...e.recommendations||[]],l=i.recommendations?.every((e=>{const t=o.find((t=>t.field===e.field));return t?.value===e.value}));l&&(t={type:i.discountType,value:parseFloat(i.discountValue),message:i.discountMessage})}}))}))}if(!t&&d&&d.length>0){const i=new Set((e.recommendations||[]).map((e=>e.field))),n=d.filter((t=>{if("plan"!==t.category)return!1;const n=[...(r||[]).filter((e=>!i.has(e.field))),...e.recommendations||[]];return t.requiredRecommendations?.every((e=>{const t=n.find((t=>t.field===e.field));return t?.value===e.value}))})).reduce(((e,t)=>e?"percentage"===t.type&&"percentage"===e.type?t.value>e.value?t:e:"percentage"===t.type&&"percentage"!==e.type?t:"percentage"!==t.type&&"percentage"===e.type?e:t.value>e.value?t:e:t),null);n&&(t={type:n.type,value:n.value,message:n.message||"Discount Applied"})}if(t){let i=e.price;if(!i){const e=x?.find((e=>{if(a?.tiers&&e.id){const t=a.tiers[e.id];if(t)return t===_}const t=e.handle?.toLowerCase();if(t?.includes("premium"))return"better"===_;if(t?.includes("classic"))return"good"===_;throw new Error(`[plan-selector] Unable to determine tier for product ${e.id} (${e.handle}). Handle must contain 'premium' or 'classic'. Checkout context tiers: ${JSON.stringify(a?.tiers)}`)}));if(e?.variants){const t=e.variants.find((e=>e.title?.includes(m)||e.bagSize===m));t?.price&&(i=t.price/100)}}if(i&&t.value){const e=parseInt(p)||1,n=i*e;l=n,"percentage"===t.type?(c=n*(t.value/100),o=n-c):"fixedAmount"===t.type&&(c=t.value*e,o=Math.max(0,n-c)),s=l>0&&null!==c?Math.round(c/l*100):0}
1}}if(!o){let t=null;const i=x?.find((e=>{if(a?.tiers&&e.id){const t=a.tiers[e.id];if(t)return t===_}const t=e.handle?.toLowerCase();if(t?.includes("premium"))return"better"===_;if(t?.includes("classic"))return"good"===_;throw new Error(`[plan-selector] Unable to determine tier for product ${e.id} (${e.handle}). Handle must contain 'premium' or 'classic'. Checkout context tiers: ${JSON.stringify(a?.tiers)}`)}));if(i?.variants){const e=i.variants.find((e=>e.title?.includes(m)||e.bagSize===m));e?.price&&(t=e.price/100)}if(!t&&e.price&&(t=e.price),t){o=t*(parseInt(p)||1)}if(!l&&e.original_price){const t=parseInt(p)||1;l=e.original_price*t}}const v={stepData:r,pricePerOunce:h,pricePerOrder:o};return{title:e.title||i,description:interpolateAllPlaceholders(e.description,v),perUnitPrice:interpolateAllPlaceholders(e.subDescription,v),price:o,originalPrice:l,discountAmount:c,discountPercentage:s,discountMessage:s>0?"Sale Price":null,badge:e.badge,bagType:i,pricingSubtext:interpolateAllPlaceholders(e.pricing_subtext,v)}}))),[h,_,c,x,d,r]),A=useMemo((()=>{const e=y?.value?.toLowerCase().trim();if(e){if("none"===e)return-1;if("auto"===e){let e=0,t=-1;return w.forEach(((i,n)=>{const o=i.discountAmount||0;o>e&&(e=o,t=n)})),t}const t=parseInt(e);if(!isNaN(t)&&t>=1&&t<=h.length)return t-1}let t=0,i=-1;return w.forEach(((e,n)=>{const o=e.discountAmount||0;o>t&&(t=o,i=n)})),i}),[w,y,h.length]),O=useMemo((()=>e?.find((e=>"text"===e._type&&"prepaid_3_banner_message"===e.key))),[e]),T=useMemo((()=>e?.find((e=>"text"===e._type&&"prepaid_6_banner_message"===e.key))),[e]),$=useMemo((()=>e?.find((e=>"text"===e._type&&"banner_background_color"===e.key))),[e]),D=useMemo((()=>e?.find((e=>"text"===e._type&&"banner_text_color"===e.key))),[e]);return useEffect((()=>{if(!O?.value&&!T?.value)return;const e=r?.find((e=>"plan"===e.field)),t=e?.value;let i="",o=!0;if(t){const e=String(t).toLowerCase();e.includes("prepaid-3")&&O?.value?(i=O.value,o=!1):e.includes("prepaid-6")&&T?.value&&(i=T.value,o=!1)}const l={title:i,backgroundColor:$?.value||"#F53C32",textColor:D?.value||"#FFF9F1",hidden:o,isDynamic:!0,hasConfiguration:!0};storeOnboardingData(n,"bannerState",l);const s=new CustomEvent(`${n}:bannerState`,{detail:{bannerState:l}});document.dispatchEvent(s)}),[r,n,O,T,$,D]),html` 2 <!-- Bag Size Options - Responsive: Stacked on Mobile, Horizontal on Desktop --> 3 <div 4 class="w-full flex flex-col md:flex-row gap-4 md:gap-6 mb-6 justify-center md:px-0 justify-self-center" 5 > 6 ${h.map(((e,n)=>{const r=u===n,a=((e,t)=>w[t])(0,n);if(!a)return"";return html` 7 <div class="relative w-full md:w-auto"> 8 <!-- Best Value Badge --> 9 ${n===A?html` 10 <div 11 class="absolute flex items-center justify-center top-[8px] md:top-[-8px] right-[-2px] w-[100px] h-[24px] z-[2]" 12 > 13 ${E?.value?html`<img 14 src="${E.value}" 15 alt="" 16 class="absolute w-[100px] h-[24px]" 17 />`:html`<svg 18 width="100" 19 height="24"
20 viewBox="0 0 100 24" 21 fill="none" 22 xmlns="http://www.w3.org/2000/svg" 23 style="position: absolute;" 24 > 25 <path 26 d="M0.463047 3.94313C-0.707533 2.28726 0.476616 0 2.50446 0H97.2148C98.5955 0 99.7148 1.11929 99.7148 2.5V21.5C99.7148 22.8807 98.5955 24 97.2148 24H2.50446C0.476614 24 -0.707531 21.7127 0.463048 20.0569L5.13848 13.4431C5.74993 12.5782 5.74993 11.4218 5.13848 10.5569L0.463047 3.94313Z" 27 fill="#F45048" 28 /> 29 </svg>`} 30 <div 31 class="font-mono uppercase whitespace-nowrap text-white text-eyebrow-xs relative z-[1]" 32 > 33 ${S?.value} 34 </div> 35 </div> 36 `:""} 37 38 <!-- Card - Horizontal on mobile, vertical on desktop --> 39 <div 40 class="${s?"cursor-default":"cursor-pointer"} transition-all flex flex-row md:flex-col items-stretch md:items-stretch w-full md:w-[204px] gap-0 h-[124px] md:h-[300px] md:max-w-none p-[20px_24px_20px_12px] md:p-6" 41 style="background-color: #FFFFFF; border-radius: 16px; border: 1px solid ${r?"#252525":"rgba(37, 37, 37, 0.25)"}; box-shadow: ${r?"inset 0 0 0 1px #252525":"none"};" 42 onClick=${()=>(e=>{if(s)return;const n=h[e];if(!n)throw new Error(`Plan selector is missing a CTA at index ${e}.`);if(n.advance_on_click){m(e);const o=[n._id];return l?._id&&o.push(l._id),void setTimeout((()=>{dispatchStepEvent({eventType:ONBOARDING_EVENTS.CTA_NAVIGATE,selectedBlockIds:o,triggeringBlockId:n._id,stepId:t,stepDataKey:ONBOARDING_STORAGE_KEYS.STEP_DATA,sectionId:i,nextStepId:n["next-step-id"]})}),250)}const r=!1!==n.allow_toggle;if(r&&u===e)return m(null),dispatchStepEvent({eventType:ONBOARDING_EVENTS.CTA_UNSELECTED,selectedBlockIds:l?._id?[l._id]:[],stepId:t,stepDataKey:ONBOARDING_STORAGE_KEYS.STEP_DATA,sectionId:i}),void(o&&o(null,[]));if(!r&&u===e)return;m(e);const a=[n._id];l?._id&&a.push(l._id),dispatchStepEvent({eventType:ONBOARDING_EVENTS.CTA_SELECTED,selectedBlockIds:a,stepId:t,stepDataKey:ONBOARDING_STORAGE_KEYS.STEP_DATA,sectionId:i}),o&&o(e,n?.recommendations||[])})(n)} 43 > 44 <!-- Mobile: 3-column layout, Desktop: vertical layout --> 45 46 <!-- Column 1: Image (mobile only) --> 47 <div 48 class="relative shrink-0 flex items-center md:hidden mr-[14px]" 49 > 50 ${e.image?html` 51 <img 52 src="${e.image}" 53 alt="${a.title}" 54 class="object-contain w-[68px] h-[68px]" 55 /> 56 `:""} 57 </div> 58 59 <!-- Column 2: Title/Description/Discount (mobile), Full content (desktop) --> 60 <div 61 class="flex flex-col flex-1 max-md:min-w-0 md:flex-initial md:w-full justify-between h-[88px] md:h-auto" 62 > 63 <!-- Title and description --> 64 <div 65 class="flex flex-col gap-1 items-start md:items-center md:mb-4" 66 > 67 <!-- Title and perUnitPrice inline on mobile --> 68 <div 69 class="flex md:flex-col gap-1.5 md:gap-1 items-center whitespace-nowrap" 70 > 71 <h3 72 class="text-[18px] md:text-[20px] font-semibold text-left md:text-center" 73 style="color: #252525; letter-spacing: -0.54px; md:letter-spacing: -0.6px; line-height: 1.12;" 74 > 75 ${a.title} 76 </h3> 77 <div class="text-body-14" style="color: #7A7A7F;"> 78 ${a.perUnitPrice} 79 </div> 80 </div> 81 82 <!-- Description below title --> 83 <p 84 class="text-[12px] md:hidden font-normal [&_*]:!text-[12px] [&_*]:!leading-normal" 85 style="color: #252525; line-height: normal;" 86 > 87 ${a.description?html`<span>${html([a.description])}</span>`:""} 88 </p> 89 </div> 90 91 <!-- Discount badge (mobile only, bottom of column 2) --> 92 ${a.discountPercentage>0?html` 93 <div 94 class="md:hidden bg-[#EBFF88] flex items-center justify-center px-[6px] pb-[1.016px] self-start" 95 >
96 <span 97 class="font-medium text-[14px]" 98 style="color: #252525; line-height: 1.6;" 99 >${a.discountPercentage}% off</span 100 > 101 </div> 102 `:""} 103 104 <!-- Desktop only: Image in middle --> 105 <div class="hidden md:flex relative justify-center"> 106 ${e.image?html` 107 <img 108 src="${e.image}" 109 alt="${a.title}" 110 class="object-contain w-[86px] h-[86px]" 111 /> 112 `:""} 113 <!-- Discount percentage circle overlay --> 114 ${a.discountPercentage>0?html` 115 <div 116 class="absolute right-[26px] -top-3 w-[52px] h-[52px] flex flex-col items-center justify-center rounded-full leading-none" 117 style="background: #EBFF88; color: #252525;" 118 > 119 <div 120 class="font-serif" 121 style="font-size: 18px; letter-spacing: -0.72px;" 122 > 123 ${a.discountPercentage}% 124 </div> 125 <div 126 class="font-mono uppercase" 127 style="font-size: 10.4px; letter-spacing: 0.624px; margin-top: 2px;" 128 > 129 OFF 130 </div> 131 </div> 132 `:""} 133 </div> 134 135 <!-- Desktop only: Description below image --> 136 <p 137 class="hidden md:block font-medium text-center md:mb-[16px] [&_*]:!text-[12px] [&_*]:!leading-[1.6]" 138 style="color: #252525; line-height: 1.6; font-size: 12px;" 139 > 140 ${a.description?html`<span>${html([a.description])}</span>`:""} 141 </p> 142 143 <!-- Desktop only: Pricing stacked --> 144 <div 145 class="hidden md:flex flex-col items-center gap-3 text-center" 146 > 147 <div 148 class="flex flex-col gap-2 items-center whitespace-nowrap my-[10px]" 149 > 150 ${a.originalPrice&&a.originalPrice!==a.price?html` 151 <div 152 class="text-[14px] font-normal line-through" 153 style="color: #4F4F52; line-height: normal;" 154 > 155 $${v(a.originalPrice)} 156 </div> 157 `:""} 158 <div 159 class="text-lg font-bold" 160 style="color: #252525; letter-spacing: -0.54px; line-height: 1.12;" 161 > 162 $${v(a.price)} 163 </div> 164 </div> 165 ${a.pricingSubtext?html` 166 <p 167 class="text-sm font-normal" 168 style="color: #7A7A7F; line-height: 1.6;" 169 > 170 ${a.pricingSubtext} 171 </p> 172 `:""} 173 </div> 174 </div> 175 176 <!-- Column 3: Pricing (mobile only) --> 177 <div 178 class="md:hidden flex flex-col items-end justify-end md:justify-between md:h-[88px]" 179 > 180 <div class="flex flex-col items-end text-right mt-[25px]"> 181 ${a.originalPrice&&a.originalPrice!==a.price?html` 182 <div 183 class="text-[14px] font-normal line-through" 184 style="color: #4F4F52; line-height: normal;" 185 > 186 $${v(a.originalPrice)} 187 </div> 188 `:""} 189 <div 190 class="text-[16px] font-bold" 191 style="color: #252525; letter-spacing: -0.32px; line-height: normal;" 192 > 193 $${v(a.price)} 194 </div> 195 </div> 196 ${a.pricingSubtext?html` 197 <p 198 class="text-[12px] font-normal whitespace-nowrap" 199 style="color: #7A7A7F; line-height: 1.6;" 200 > 201 ${a.pricingSubtext} 202 </p> 203 `:""} 204 </div> 205 </div> 206 </div> 207 `}))} 208 </div> 209 210 <!-- Description below plan selector --> 211 ${b?.value?html` 212 <div class="text-center mb-6"> 213 <p
214 class="text-[14px] md:text-[16px] text-trade-black" 215 style="line-height: 140%;" 216 > 217 ${b.value} 218 </p> 219 </div> 220 `:""} 221 `}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.