diff --git a/src/app/subscription/__tests__/subscription-screen.helpers.test.ts b/src/app/subscription/__tests__/subscription-screen.helpers.test.ts index 828f85ed..92bc0fbf 100644 --- a/src/app/subscription/__tests__/subscription-screen.helpers.test.ts +++ b/src/app/subscription/__tests__/subscription-screen.helpers.test.ts @@ -232,6 +232,24 @@ describe("subscription screen helpers", () => { ).toBeNull(); }); + it("maps the backend most popular flag to VIP and coin views", () => { + const popularVip = toVipOfferPlanViews([ + makePlan({ planId: "vip_monthly", mostPopular: true }), + ]); + const popularCoins = toCoinsOfferPlanViews([ + makePlan({ + planId: "coin_1000", + orderType: "coins_1000", + vipDays: null, + dolAmount: 1000, + mostPopular: true, + }), + ]); + + expect(popularVip[0]?.mostPopular).toBe(true); + expect(popularCoins[0]?.mostPopular).toBe(true); + }); + it("maps normal VIP original price outside first recharge activity", () => { expect( toVipOfferPlanViews([ diff --git a/src/app/subscription/components/__tests__/tailwind-components.test.tsx b/src/app/subscription/components/__tests__/tailwind-components.test.tsx index 3bcbcbb3..139fc2ac 100644 --- a/src/app/subscription/components/__tests__/tailwind-components.test.tsx +++ b/src/app/subscription/components/__tests__/tailwind-components.test.tsx @@ -6,6 +6,8 @@ import { StripePaymentDialog } from "@/app/_components/payment/stripe-payment-di import { SubscriptionCtaButton } from "../subscription-cta-button"; import { SubscriptionPaymentMethod } from "../subscription-payment-method"; import { SubscriptionPaymentSuccessDialog } from "../subscription-payment-success-dialog"; +import { SubscriptionCoinsOfferSection } from "../subscription-coins-offer-section"; +import { SubscriptionVipOfferSection } from "../subscription-vip-offer-section"; describe("subscription Tailwind components", () => { it("renders SubscriptionCtaButton with loading state", () => { @@ -93,4 +95,44 @@ describe("subscription Tailwind components", () => { expect(html).toContain("Payment unavailable"); expect(html).toContain("bg-[linear-gradient(var(--color-button-gradient-start,#ff67e0)"); }); + + it("renders most popular badges for VIP and coin plans", () => { + const vipHtml = renderToStaticMarkup( + undefined} + />, + ); + const coinsHtml = renderToStaticMarkup( + undefined} + />, + ); + + expect(vipHtml).toContain("Most Popular"); + expect(vipHtml).toContain("50% OFF"); + expect(coinsHtml).toContain("Most Popular"); + }); }); diff --git a/src/app/subscription/components/subscription-coins-offer-section.module.css b/src/app/subscription/components/subscription-coins-offer-section.module.css index 69ef1369..6cf4113a 100644 --- a/src/app/subscription/components/subscription-coins-offer-section.module.css +++ b/src/app/subscription/components/subscription-coins-offer-section.module.css @@ -118,16 +118,33 @@ line-height: 1.2; } +.badgeGroup { + display: inline-flex; + flex-direction: column; + align-items: flex-end; + justify-self: end; + gap: 4px; +} + +.popularBadge, .offerBadge { justify-self: end; padding: var(--subscription-offer-badge-padding, 5px 9px); border-radius: 999px; - background: linear-gradient(135deg, #ff5fae 0%, #ff8a34 100%); color: #ffffff; font-size: clamp(10px, 2.593vw, 14px); font-weight: 900; line-height: 1; white-space: nowrap; +} + +.popularBadge { + background: linear-gradient(135deg, #6b3a12 0%, #d99a27 55%, #f4c75a 100%); + box-shadow: 0 7px 16px rgba(164, 101, 21, 0.24); +} + +.offerBadge { + background: linear-gradient(135deg, #ff5fae 0%, #ff8a34 100%); box-shadow: 0 7px 16px rgba(255, 95, 174, 0.24); } @@ -175,6 +192,7 @@ auto; } + .popularBadge, .offerBadge { padding-right: clamp(6px, 1.296vw, 7px); padding-left: clamp(6px, 1.296vw, 7px); diff --git a/src/app/subscription/components/subscription-coins-offer-section.tsx b/src/app/subscription/components/subscription-coins-offer-section.tsx index eda0fbb9..14dc0596 100644 --- a/src/app/subscription/components/subscription-coins-offer-section.tsx +++ b/src/app/subscription/components/subscription-coins-offer-section.tsx @@ -11,6 +11,7 @@ export interface CoinsOfferPlanView { currency: string; originalPrice?: string; isFirstRechargeOffer?: boolean; + mostPopular?: boolean; firstRechargeDiscountPercent?: number | null; promotionType?: string | null; } @@ -51,11 +52,16 @@ export function SubscriptionCoinsOfferSection({ {plan.coins} Coins - {plan.isFirstRechargeOffer ? ( - - {plan.firstRechargeDiscountPercent ?? 50}% OFF - - ) : null} + + {plan.mostPopular ? ( + Most Popular + ) : null} + {plan.isFirstRechargeOffer ? ( + + {plan.firstRechargeDiscountPercent ?? 50}% OFF + + ) : null} + {plan.currency} diff --git a/src/app/subscription/components/subscription-vip-offer-section.module.css b/src/app/subscription/components/subscription-vip-offer-section.module.css index aa57e8ce..12f1fc83 100644 --- a/src/app/subscription/components/subscription-vip-offer-section.module.css +++ b/src/app/subscription/components/subscription-vip-offer-section.module.css @@ -111,21 +111,37 @@ transform: translateY(-3px); } -.offerBadge { +.badgeStack { position: absolute; top: clamp(6px, 1.481vw, 8px); left: 50%; + display: flex; max-width: calc(100% - 12px); + flex-direction: column; + align-items: center; + gap: 4px; + transform: translateX(-50%); +} + +.popularBadge, +.offerBadge { padding: var(--subscription-offer-badge-padding, 4px 8px); border-radius: 999px; - background: linear-gradient(135deg, #ff5fae 0%, #ff8a34 100%); color: #ffffff; - font-size: clamp(12px, 2.963vw, 16px); + font-size: clamp(10px, 2.407vw, 13px); font-weight: 900; line-height: 1; white-space: nowrap; +} + +.popularBadge { + background: linear-gradient(135deg, #6b3a12 0%, #d99a27 55%, #f4c75a 100%); + box-shadow: 0 7px 16px rgba(164, 101, 21, 0.28); +} + +.offerBadge { + background: linear-gradient(135deg, #ff5fae 0%, #ff8a34 100%); box-shadow: 0 7px 16px rgba(255, 95, 174, 0.28); - transform: translateX(-50%); } .planTitle { @@ -136,6 +152,14 @@ white-space: nowrap; } +.planCardWithBadges .planTitle { + margin-top: 30px; +} + +.planCardWithTwoBadges .planTitle { + margin-top: 50px; +} + .priceLine { display: inline-flex; align-items: flex-end; diff --git a/src/app/subscription/components/subscription-vip-offer-section.tsx b/src/app/subscription/components/subscription-vip-offer-section.tsx index 17535e93..9393f13b 100644 --- a/src/app/subscription/components/subscription-vip-offer-section.tsx +++ b/src/app/subscription/components/subscription-vip-offer-section.tsx @@ -9,6 +9,7 @@ export interface VipOfferPlanView { currency: string; originalPrice: string; isFirstRechargeOffer?: boolean; + mostPopular?: boolean; firstRechargeDiscountPercent?: number | null; promotionType?: string | null; } @@ -42,20 +43,30 @@ export function SubscriptionVipOfferSection({
{plans.map((plan) => { const selected = selectedPlanId === plan.id; + const badgeCount = + Number(plan.mostPopular === true) + + Number(plan.isFirstRechargeOffer === true); return (