kupos-ui-components-lib 10.4.28 → 10.4.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,9 @@ interface PeruOfferBannerProps {
33
33
  coachKey?: number;
34
34
  isExpand?: any;
35
35
  isNewUiEnabled?: boolean;
36
+ // ui_type from the active discount entry: 1 = offer-text-only tier,
37
+ // 2 = full new-UI tier (timer, bomb animation, viewers).
38
+ uiType?: number;
36
39
  }
37
40
 
38
41
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -121,19 +124,13 @@ const OfferBannerContent: React.FC<OfferBannerContentProps> = ({
121
124
  </span>
122
125
  )}{" "}
123
126
  &nbsp;
124
- </span>{" "}
125
- {offerText ? "| " : ""}
126
- Termina en&nbsp;
127
- <span
128
- className="bold-text text-end"
129
- ref={(node) => CommonService.startCountdown(node, 599)}
130
- style={{ fontVariantNumeric: "tabular-nums", display: "inline-block" }}
131
- />
127
+ </span>
132
128
  </div>
133
129
  </div>
134
130
  );
135
131
 
136
- // Shown for legacy promo offers
132
+ // Shown for the offer-text-only tier (ui_type 1): the announcement icon and
133
+ // the offer text, nothing else — no timer, no viewers.
137
134
  interface LegacyOfferBannerProps {
138
135
  offerText: string;
139
136
  getAnimationIcon: PeruOfferBannerProps["getAnimationIcon"];
@@ -207,6 +204,7 @@ const PeruOfferBanner: React.FC<PeruOfferBannerProps> = ({
207
204
  offerGradient,
208
205
  isSoldOut,
209
206
  serviceItem,
207
+ renderIcon,
210
208
  isLoggedIn,
211
209
  showLoginModal,
212
210
  viewersConfig,
@@ -217,28 +215,32 @@ const PeruOfferBanner: React.FC<PeruOfferBannerProps> = ({
217
215
  coachKey,
218
216
  isExpand,
219
217
  isNewUiEnabled,
218
+ uiType,
220
219
  }) => {
221
- const isDiscountBelow20 =
222
- (serviceItem as any)?.discount_value > 0 &&
223
- (serviceItem as any)?.discount_value < 20;
224
220
  const offerText = serviceItem?.offer_text || "";
221
+ const hasDp = hasDpDiscounts(serviceItem);
225
222
 
226
- const isLegacyOffer = !!offerText && !isNewUiEnabled;
223
+ // Tier B: new UI enabled, ui_type 1 — offer text only, no timer, no viewers.
224
+ const isOfferTextOnly = isNewUiEnabled && uiType === 1 && !!offerText;
225
+ // Tier C: new UI enabled, ui_type 2 — full ribbon (timer, bomb, viewers).
226
+ const isFullNewUi = isNewUiEnabled && uiType === 2;
227
227
 
228
- // const showViewers = serviceItem?.is_dp_enabled;
229
- const showViewers = isNewUiEnabled;
228
+ const showViewers = isFullNewUi || hasDp;
230
229
 
231
- const background = isLegacyOffer ? colors?.bottomStripColor : offerGradient;
230
+ const background = isOfferTextOnly
231
+ ? colors?.bottomStripColor || offerGradient
232
+ : offerGradient;
232
233
 
233
234
  const renderLeftContent = () => {
234
- const hasDp = hasDpDiscounts(serviceItem);
235
-
236
235
  if (serviceItem?.is_dp_enabled && !hasDp) {
237
236
  return <PopularServiceBanner getAnimationIcon={getAnimationIcon} />;
238
237
  }
239
238
 
240
- // If new UI is enabled, show offer with registro and countdown
241
- if (isNewUiEnabled || offerText || (serviceItem as any)?.discount_value) {
239
+ if (isOfferTextOnly) {
240
+ return <LegacyOfferBanner offerText={offerText} getAnimationIcon={getAnimationIcon}/>;
241
+ }
242
+
243
+ if (isFullNewUi || hasDp) {
242
244
  return (
243
245
  <OfferBannerContent
244
246
  offerText={offerText}
@@ -252,16 +254,6 @@ const PeruOfferBanner: React.FC<PeruOfferBannerProps> = ({
252
254
  );
253
255
  }
254
256
 
255
- // If new UI is disabled, show offer text only
256
- if (offerText) {
257
- return (
258
- <LegacyOfferBanner
259
- offerText={offerText}
260
- getAnimationIcon={getAnimationIcon}
261
- />
262
- );
263
- }
264
-
265
257
  return null;
266
258
  };
267
259
 
@@ -269,12 +261,7 @@ const PeruOfferBanner: React.FC<PeruOfferBannerProps> = ({
269
261
  <div
270
262
  className={`text-white w-full absolute ${coachKey ? "-bottom-[32px]" : isExpand ? "-bottom-[32px]" : "-bottom-[40px]"} rounded-b-[14px] text-[14px] h-[58px] flex items-end pb-[7px]`}
271
263
  style={{
272
- background:
273
- serviceItem?.is_dp_enabled ||
274
- serviceItem?.dp_discount_percents ||
275
- (serviceItem as any)?.discount_value
276
- ? offerGradient
277
- : background,
264
+ background,
278
265
  opacity: isSoldOut ? 0.5 : 1,
279
266
  zIndex: isLinatal ? "-1" : "0",
280
267
  }}
@@ -236,14 +236,19 @@ function PeruSeatSection({
236
236
  .filter((seat) => !SEAT_EXCEPTIONS.includes(seat.label))
237
237
  .sort((a, b) => a.discountedPrice - b.discountedPrice)[0];
238
238
 
239
+ // discount_type/discount_value/max_discount live on the service item
240
+ // itself; only ui_type and the ready-made badge text come from discounts[0].
241
+ const peruDiscount = serviceItem?.discounts?.[0];
242
+ const uiType = peruDiscount?.ui_type;
243
+ const discountType = serviceItem?.discount_type;
244
+ const discountValueRaw = serviceItem?.discount_value;
245
+ const maxDiscount = serviceItem?.max_discount;
246
+
239
247
  const discountValue = (() => {
240
- if (
241
- serviceItem?.discount_type === "percentage" &&
242
- typeof serviceItem?.discount_value === "number"
243
- ) {
244
- return Math.round(serviceItem.discount_value);
248
+ if (discountType === "percentage" && typeof discountValueRaw === "number") {
249
+ return Math.round(discountValueRaw);
245
250
  }
246
- if (serviceItem?.discount_type === "fixed" && discountSeat) {
251
+ if (discountType === "fixed" && discountSeat) {
247
252
  const { originalPrice, discountedPrice } = discountSeat;
248
253
  if (originalPrice > 0 && originalPrice !== discountedPrice) {
249
254
  return Math.round(
@@ -255,21 +260,30 @@ function PeruSeatSection({
255
260
  })();
256
261
 
257
262
  const isMaxDiscountApplied = (() => {
258
- const { discount_type, discount_value, max_discount } = serviceItem ?? {};
259
263
  if (
260
- discount_type === "percentage" &&
261
- typeof discount_value === "number" &&
262
- max_discount != null &&
263
- max_discount > 0 &&
264
+ discountType === "percentage" &&
265
+ typeof discountValueRaw === "number" &&
266
+ maxDiscount != null &&
267
+ maxDiscount > 0 &&
264
268
  discountSeat
265
269
  ) {
266
270
  const rawPercentageDiscount =
267
- (discountSeat.originalPrice * discount_value) / 100;
268
- return rawPercentageDiscount > max_discount;
271
+ (discountSeat.originalPrice * discountValueRaw) / 100;
272
+ return rawPercentageDiscount > maxDiscount;
269
273
  }
270
274
  return false;
271
275
  })();
272
276
 
277
+ // Backend-provided ready-made badge text (e.g. "30% OFF" / "$2.000 OFF")
278
+ // takes precedence over the value computed client-side.
279
+ const discountBadgeText =
280
+ peruDiscount?.discount_display_badge ||
281
+ (isMaxDiscountApplied
282
+ ? `${CommonService.currency(maxDiscount)} OFF`
283
+ : discountValue != null
284
+ ? `${discountValue}% OFF`
285
+ : null);
286
+
273
287
  const renderLabels = () => {
274
288
  const isMovilBus =
275
289
  serviceItem?.operator_service_name === "MovilBus" ||
@@ -363,6 +377,41 @@ function PeruSeatSection({
363
377
  };
364
378
 
365
379
  if (hasDiscount && discountSeat) {
380
+ // Tier B (ui_type 1): offer text is announced elsewhere, but the fare
381
+ // itself must render exactly as it would with no discount — no
382
+ // strike-off, no badge, no fire icon.
383
+ if (uiType === 1) {
384
+ return (
385
+ <div
386
+ className="grid grid-cols-2 items-center text-[13.33px] relative"
387
+ style={{ opacity: isSoldOut ? 0.5 : 1 }}
388
+ >
389
+ <div className="col-start-1 row-start-3 flex h-[20px] items-end">
390
+ <span className="text-[13.33px] font-normal leading-[20px] text-[#464647]">
391
+ {discountSeat.label}
392
+ </span>
393
+ </div>
394
+
395
+ <div className="col-start-2 row-start-3 flex h-[30px] items-end justify-center relative">
396
+ <span
397
+ className="flex items-center gap-[6px] bold-text leading-[30px]"
398
+ style={{
399
+ color: isSoldOut ? "#c0c0c0" : "#464647",
400
+ fontSize: "13.33px",
401
+ }}
402
+ >
403
+ {availableSeats <= 0
404
+ ? CommonService.currency(0, currencySign)
405
+ : CommonService.discountedCurrency(
406
+ discountSeat.originalPrice,
407
+ currencySign,
408
+ )}
409
+ </span>
410
+ </div>
411
+ </div>
412
+ );
413
+ }
414
+
366
415
  return (
367
416
  <div
368
417
  className="grid grid-cols-2 items-center text-[13.33px] relative"
@@ -384,7 +433,7 @@ function PeruSeatSection({
384
433
  className="col-start-2 row-start-1 flex items-center justify-center absolute"
385
434
  style={{ top: "-22px", left: "50%", transform: "translateX(-50%)" }}
386
435
  >
387
- {discountValue != null || isMaxDiscountApplied ? (
436
+ {discountBadgeText ? (
388
437
  <span
389
438
  className="rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white"
390
439
  style={{
@@ -393,9 +442,7 @@ function PeruSeatSection({
393
442
  backgroundColor: discountSeatPriceColor,
394
443
  }}
395
444
  >
396
- {isMaxDiscountApplied
397
- ? `${CommonService.currency(serviceItem?.max_discount)} OFF`
398
- : `${discountValue}% OFF`}
445
+ {discountBadgeText}
399
446
  </span>
400
447
  ) : null}
401
448
  </div>
@@ -653,6 +653,7 @@ function SeatSection({
653
653
  style={{ top: "-22px", left: "50%", transform: "translateX(-50%)" }}
654
654
  >
655
655
  {serviceItem?.ui_type !== 1 &&
656
+ !isPeru &&
656
657
  (discountValue != null || isMaxDiscountApplied) && (
657
658
  <span
658
659
  className="rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white"
@@ -667,6 +668,21 @@ function SeatSection({
667
668
  : `${discountValue}% OFF`}
668
669
  </span>
669
670
  )}
671
+
672
+ {serviceItem?.ui_type !== 1 &&
673
+ isPeru &&
674
+ serviceItem?.discount_display_badge && (
675
+ <span
676
+ className="rounded-[100px] bg-[#ff5964] px-[6px] text-[12px] bold-text leading-[20px] text-white"
677
+ style={{
678
+ animation: "pulse-zoom 2s ease-in-out infinite",
679
+ whiteSpace: "nowrap",
680
+ backgroundColor: discountSeatCrossPriceColor,
681
+ }}
682
+ >
683
+ {serviceItem?.discount_display_badge}
684
+ </span>
685
+ )}
670
686
  </div>
671
687
 
672
688
  {serviceItem?.ui_type === 1 ? null : (
@@ -678,7 +694,7 @@ function SeatSection({
678
694
  className="text-[13.33px] font-normal leading-[20px] relative whitespace-nowrap"
679
695
  style={{
680
696
  position: "relative",
681
- color: discountSeatPriceColor,
697
+ color: discountSeatCrossPriceColor,
682
698
  }}
683
699
  >
684
700
  {formatPrice(discountSeat.originalPrice)}
@@ -478,6 +478,16 @@ function SeatSectionMobile({
478
478
  return false;
479
479
  })();
480
480
 
481
+ // Backend-provided ready-made badge text (e.g. "30% OFF" / "$2.000 OFF")
482
+ // takes precedence over the value computed client-side.
483
+ const discountBadgeText =
484
+ serviceItem?.discounts?.[0]?.discount_display_badge ||
485
+ (isMaxDiscountApplied
486
+ ? `${commonService.currency(serviceItem?.max_discount)} OFF`
487
+ : discountValue != null
488
+ ? `${discountValue}% OFF`
489
+ : null);
490
+
481
491
  const getMinValue = (data: any): number | undefined => {
482
492
  const vals = (Array.isArray(data) ? data : Object.values(data || {})).map(
483
493
  Number,
@@ -595,59 +605,56 @@ function SeatSectionMobile({
595
605
  ) : hasDiscount && discountSeat ? (
596
606
  <div>
597
607
  <div className="relative grid grid-cols-[auto_auto] justify-between gap-x-[8px] ">
598
- {serviceItem?.ui_type !== 1 &&
599
- (discountValue != null || isMaxDiscountApplied) && (
600
- <>
601
- <div
602
- className="absolute -top-[18px] right-[0px]"
608
+ {serviceItem?.ui_type !== 1 && discountBadgeText && (
609
+ <>
610
+ <div
611
+ className="absolute -top-[18px] right-[0px]"
612
+ style={{
613
+ animation: "pulse-zoom 2s ease-in-out infinite",
614
+ opacity: isSoldOut ? 0.5 : 1,
615
+ }}
616
+ >
617
+ <span
618
+ className="rounded-[100px] px-[8px] text-[12px] bold-text leading-[20px] text-[#fff]"
603
619
  style={{
604
- animation: "pulse-zoom 2s ease-in-out infinite",
605
- opacity: isSoldOut ? 0.5 : 1,
620
+ backgroundColor: tooltipBgColor,
606
621
  }}
607
622
  >
608
- <span
609
- className="rounded-[100px] px-[8px] text-[12px] bold-text leading-[20px] text-[#fff]"
610
- style={{
611
- backgroundColor: tooltipBgColor,
612
- }}
613
- >
614
- {isMaxDiscountApplied
615
- ? `${commonService.currency(serviceItem?.max_discount)} OFF`
616
- : `${discountValue}% OFF`}
617
- </span>
618
- </div>
619
-
620
- <span className="min-[420]:text-[13px] text-[12px] leading-[20px] text-[#c2c2c2]">
621
- Antes
623
+ {discountBadgeText}
622
624
  </span>
625
+ </div>
626
+
627
+ <span className="min-[420]:text-[13px] text-[12px] leading-[20px] text-[#c2c2c2]">
628
+ Antes
629
+ </span>
630
+ <span
631
+ className="min-[420]:text-[13px] text-[12px] leading-[20px] text-right "
632
+ style={{
633
+ position: "relative",
634
+ display: "inline-block",
635
+ overflow: "hidden",
636
+ color: discountSeatPriceColor,
637
+ }}
638
+ >
639
+ {commonService.currency(
640
+ discountSeat.originalPrice,
641
+ currencySign,
642
+ )}
623
643
  <span
624
- className="min-[420]:text-[13px] text-[12px] leading-[20px] text-right"
625
644
  style={{
626
- position: "relative",
627
- display: "inline-block",
628
- overflow: "hidden",
629
- color: discountSeatPriceColor,
645
+ position: "absolute",
646
+ top: "35%",
647
+ left: "50%",
648
+ width: "80%",
649
+ height: "1px",
650
+ background: discountSeatCrossPriceColor,
651
+ transform: "rotate(-12deg)",
652
+ transformOrigin: "center",
630
653
  }}
631
- >
632
- {commonService.currency(
633
- discountSeat.originalPrice,
634
- currencySign,
635
- )}
636
- <span
637
- style={{
638
- position: "absolute",
639
- top: "35%",
640
- left: "50%",
641
- width: "80%",
642
- height: "1px",
643
- background: discountSeatCrossPriceColor,
644
- transform: "rotate(-12deg)",
645
- transformOrigin: "center",
646
- }}
647
- />
648
- </span>
649
- </>
650
- )}
654
+ />
655
+ </span>
656
+ </>
657
+ )}
651
658
 
652
659
  <span
653
660
  className="min-[420]:text-[13px] text-[12px] leading-[24px]"