diff --git a/app/item/[tag]/page.tsx b/app/item/[tag]/page.tsx index c72f2b802..5b5208370 100644 --- a/app/item/[tag]/page.tsx +++ b/app/item/[tag]/page.tsx @@ -38,9 +38,8 @@ export default async function Page(props) { let tag = params.tag as string let data = await getItemData(searchParams, params) - console.log('Item page data:', data) let item = parseItem(data.item) - + const isBazaar = data.itemFlags?.isBazaar ?? false function getItem(): Item { diff --git a/components/CoflCoins/CoflCoinPaymentSelection.tsx b/components/CoflCoins/CoflCoinPaymentSelection.tsx index 96fef32bc..15e0c7aac 100644 --- a/components/CoflCoins/CoflCoinPaymentSelection.tsx +++ b/components/CoflCoins/CoflCoinPaymentSelection.tsx @@ -3,10 +3,9 @@ import { useState, useEffect } from 'react' import { Card, Button, Alert, Form, InputGroup, Spinner } from 'react-bootstrap' import Number from '../Number/Number' import PurchaseElement from './PurchaseElement' -import styles from './CoflCoinsPurchase.module.css' import { postApiTopupRates, getApiDiscountCode } from '../../api/_generated/skyApi' -import type { BatchProductPricingResponse, ProviderPricingOption, ValidatedDiscount } from '../../api/_generated/skyApi.schemas' -import { getProvider, getProviderPrice, getProviderOriginalPrice } from '../../utils/pricingUtils' +import type { BatchProductPricingResponse, ValidatedDiscount } from '../../api/_generated/skyApi.schemas' +import { getProvider, getProviderPrice, getProviderOriginalPrice } from '../../utils/PricingUtils' interface CoflCoinOption { amount: number diff --git a/components/CoflCoins/CoflCoinsPurchase.tsx b/components/CoflCoins/CoflCoinsPurchase.tsx index 1f46bb4c5..dab7f63f4 100644 --- a/components/CoflCoins/CoflCoinsPurchase.tsx +++ b/components/CoflCoins/CoflCoinsPurchase.tsx @@ -42,35 +42,35 @@ function Payment(props: Props) { if (errorMatch) { const errorId = errorMatch[1] const errorMessage = decodeURIComponent(errorMatch[2].replace(/\+/g, ' ')) - + console.error('[Billing] Error from Android:', { errorId, errorMessage }) - + // Show user-friendly error message let userMessage = 'Purchase failed: ' + errorMessage if (errorMessage === 'Server validation failed' || errorMessage === 'Product not found') { userMessage += '. Please contact support if this issue persists.' } - + toast.error(userMessage, { autoClose: 10000 }) - + // Clear the error from URL window.history.replaceState(null, '', window.location.pathname + window.location.search) - + // Clear loading state if it matches the error ID if (loadingId && loadingId.includes(errorId)) { setLoadingId('') } } } - + // Load country loadDefaultCountry() - + // Check for Android Billing availability with a delay setTimeout(() => { checkGooglePlayAvailability() }, 500) - + // Also check when page becomes visible const handleVisibilityChange = () => { if (!document.hidden) { @@ -78,12 +78,12 @@ function Payment(props: Props) { } } document.addEventListener('visibilitychange', handleVisibilityChange) - + // Set up event listeners for Android Billing events (CustomEvents) const handleBillingSuccess = (event: Event) => { const customEvent = event as CustomEvent<{ productId: string; purchaseToken: string }> console.log('[Billing] androidBillingSuccess event received', customEvent.detail) - + setLoadingId('') toast.success('Purchase successful! Your CoflCoins have been added.') // Refresh coflcoins balance @@ -93,16 +93,16 @@ function Payment(props: Props) { const handleBillingError = (event: Event) => { const customEvent = event as CustomEvent<{ error: string }> console.error('[Billing] androidBillingError event received', customEvent.detail) - + setLoadingId('') - + // Show user-friendly error message const error = customEvent.detail.error let userMessage = 'Purchase failed: ' + error if (error === 'Server validation failed' || error === 'Product not found') { userMessage += '. Please contact support if this issue persists.' } - + if (error === 'Purchase canceled by user') { toast.info('Purchase cancelled') } else { @@ -112,11 +112,11 @@ function Payment(props: Props) { window.addEventListener('androidBillingSuccess', handleBillingSuccess) window.addEventListener('androidBillingError', handleBillingError) - + // Also listen for postMessage from Android app const handleMessage = (event: MessageEvent) => { console.log('[Billing] Received postMessage:', event.data) - + if (event.data?.type === 'androidBillingSuccess') { const { productId, purchaseToken } = event.data console.log('[Billing] Purchase success via postMessage', { productId, purchaseToken }) @@ -127,13 +127,13 @@ function Payment(props: Props) { const { error } = event.data console.error('[Billing] Purchase error via postMessage', error) setLoadingId('') - + // Show user-friendly error message let userMessage = 'Purchase failed: ' + error if (error === 'Server validation failed' || error === 'Product not found') { userMessage += '. Please contact support if this issue persists.' } - + if (error === 'Purchase canceled by user') { toast.info('Purchase cancelled') } else { @@ -141,17 +141,15 @@ function Payment(props: Props) { } } } - + window.addEventListener('message', handleMessage) } function checkGooglePlayAvailability() { - // Check if we're running in the Android app - // The Android app is a TWA, so we check user agent and assume billing is available const isAndroid = /android/i.test(navigator.userAgent) const isTWA = document.referrer.includes('android-app://com.coflnet.sky') const available = isAndroid && (isTWA || window.matchMedia('(display-mode: standalone)').matches) - + console.log('[Billing] checkGooglePlayAvailability result:', available, { isAndroid, isTWA, @@ -159,7 +157,7 @@ function Payment(props: Props) { userAgent: navigator.userAgent, referrer: document.referrer }) - + setIsGooglePlayAvailable(available) } @@ -232,7 +230,7 @@ function Payment(props: Props) { function onPayGooglePlay(productId: string, coflCoins?: number) { setLoadingId(coflCoins ? `${productId}_${coflCoins}` : productId) console.log('[Billing] onPayGooglePlay called', { productId, coflCoins }) - + const googleToken = typeof window !== 'undefined' ? sessionStorage.getItem('googleId') : null const requestOptions: RequestInit | undefined = googleToken ? { headers: { GoogleToken: googleToken } } : undefined postApiTopupPlaystore(requestOptions) @@ -240,18 +238,18 @@ function Payment(props: Props) { if (response.status !== 200) { throw new Error('Failed to get user ID from backend') } - + const userId = response.data.userId if (!userId) { throw new Error('User ID not found in response') } - + // Trigger Google Play billing flow via deep link with userId const deepLink = `skycofl://billing/purchase?productId=${encodeURIComponent(productId)}&userId=${encodeURIComponent(userId)}` console.log('[Billing] Triggering purchase via deep link:', deepLink) - + navigateTo(deepLink) - + // Set a timeout to show error if nothing happens setTimeout(() => { if (loadingId) { diff --git a/components/CoflCoins/GenericProviderPurchaseCard.tsx b/components/CoflCoins/GenericProviderPurchaseCard.tsx index c4fbaf04e..9c9aca622 100644 --- a/components/CoflCoins/GenericProviderPurchaseCard.tsx +++ b/components/CoflCoins/GenericProviderPurchaseCard.tsx @@ -3,7 +3,7 @@ import Tooltip from '../Tooltip/Tooltip' import styles from './CoflCoinsPurchase.module.css' import HelpIcon from '@mui/icons-material/Help' import Number from '../Number/Number' -import { getCurrencySymbol } from '../../utils/pricingUtils' +import { getCurrencySymbol } from '../../utils/PricingUtils' import type { JSX } from 'react' interface Props { diff --git a/components/CoflCoins/PurchaseElement.tsx b/components/CoflCoins/PurchaseElement.tsx index 1e5e82675..dd4c35ba0 100644 --- a/components/CoflCoins/PurchaseElement.tsx +++ b/components/CoflCoins/PurchaseElement.tsx @@ -36,19 +36,16 @@ interface Props { } // prettier-ignore -const EU_Countries = ["AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE" ] +const EU_Countries = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"] let PAYPAL_STRIPE_ALLOWED = [...EU_Countries, 'GB', 'US'] export default function PurchaseElement(props: Props) { let isDisabled = props.isDisabled || !props.countryCode - // Check if this is a custom amount (not one of the standard predefined amounts) const standardAmounts = [1800, 5400, 10800, 36000, 90000] const isCustomAmount = !standardAmounts.includes(props.coflCoinsToBuy) - - // Pass custom amount for both special multiplier and custom amounts from wizard const shouldPassCustomAmount = props.isSpecial1800CoinsMultiplier || isCustomAmount - // Build Google Play card once and reuse it to avoid duplication + const googlePlayCard = ( <> {props.isGooglePlayAvailable ? ( @@ -70,10 +67,11 @@ export default function PurchaseElement(props: Props) { /> ) : (

There are more options, eg. gift cards in our android app.

-
+ )} - ) // On Android app, only show Google Play option (reuse googlePlayCard) + ) + if (props.isAndroidApp) { return ( @@ -89,11 +87,6 @@ export default function PurchaseElement(props: Props) { ) } - console.log('PurchaseElement render', { - coflCoinsToBuy: props.coflCoinsToBuy, - isGooglePlayAvailable: props.isGooglePlayAvailable - }) - return ( diff --git a/components/ForgeFlips/ForgeFlips.tsx b/components/ForgeFlips/ForgeFlips.tsx index 78de4f116..b2b69f254 100644 --- a/components/ForgeFlips/ForgeFlips.tsx +++ b/components/ForgeFlips/ForgeFlips.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useMemo } from 'react' +import { useMemo } from 'react' import Image from 'next/image' import { useSuspenseQuery } from '@tanstack/react-query' import api from '../../api/ApiHelper' diff --git a/components/GenericFlipList/GenericFlipList.tsx b/components/GenericFlipList/GenericFlipList.tsx index 7257e4711..23cc7c8f7 100644 --- a/components/GenericFlipList/GenericFlipList.tsx +++ b/components/GenericFlipList/GenericFlipList.tsx @@ -8,7 +8,6 @@ import GoogleSignIn from '../GoogleSignIn/GoogleSignIn' import api from '../../api/ApiHelper' import styles from './GenericFlipList.module.css' import { useSortedAndFilteredItems } from '../../hooks/useSortedAndFilteredItems' -import NitroAdSlot from '../Ads/NitroAdSlot' export interface FlipListProps { items: T[] @@ -26,15 +25,8 @@ export interface FlipListProps { customItemWrapper?: (item: T, blur: boolean, key: string, content: React.ReactNode, flipCardClass: string) => React.ReactNode onAfterSignIn?: () => void customHeader?: (isLoggedIn: boolean) => React.ReactNode - // Override the placeholder text for the minimum input. Defaults to "Minimum Profit" minimumPlaceholder?: string - // Optional: provide a function that returns a href for a flip item. - // If provided, non-blurred flips will be wrapped in a plain so - // the link exists in the HTML for users/search engines with JS disabled. getFlipLink?: (item: T) => string | null | undefined - // When lists are large, render a small initial batch and load more as the - // user scrolls to reduce DOM size and JS work. Defaults keep at least 3 - // items to preserve top-3 censoring for SSR. renderBatchSize?: number initialRenderCount?: number } @@ -86,12 +78,10 @@ export function GenericFlipList({ const sentinelRef = React.useRef(null) useEffect(() => { - // reset the blur observer, when something changed setTimeout(setBlurObserver, 100) if (showColumns) { setColumns(getDefaultColumns()) } - // Reset rendered count when the processed items change (new search/sort) setRenderedCount(Math.max(3, safeInitial)) }, []) @@ -149,7 +139,6 @@ export function GenericFlipList({ setHasPremium(hasHighEnoughPremium(products, PREMIUM_RANK.STARTER)) }) - // Call the custom onAfterSignIn if provided if (onAfterSignIn) { onAfterSignIn() } @@ -195,7 +184,6 @@ export function GenericFlipList({ } function getListElement(item: T, blur: boolean) { - // Build the inner content (blur messages + actual content) const inner = ( <> {blur ? ( @@ -271,16 +259,11 @@ export function GenericFlipList({ ) - // If a link generator was provided and this isn't a blurred (censored) item, - // wrap the inner content in a plain anchor so it exists in the static HTML. let wrappedInner: React.ReactNode = inner if (!blur && typeof getFlipLink === 'function') { const href = getFlipLink(item) if (href) { const handleAnchorClick = (e: React.MouseEvent) => { - // Always prevent default when JS is enabled. - // The customItemWrapper or onFlipClick handles the actual interaction. - // The anchor is only for non-JS clients/SEO crawlers. e.preventDefault() if (onFlipClick) { onFlipClick(item, e) @@ -311,7 +294,6 @@ export function GenericFlipList({ ) } - // Memoized displayed items const list = useMemo(() => { if (isProcessing) { return [] diff --git a/components/Premium/BuySubscription/BuySubscription.tsx b/components/Premium/BuySubscription/BuySubscription.tsx index 26b113f94..4ed746e58 100644 --- a/components/Premium/BuySubscription/BuySubscription.tsx +++ b/components/Premium/BuySubscription/BuySubscription.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { PREMIUM_TYPES } from '../../../utils/PremiumTypeUtils' import api from '../../../api/ApiHelper' -import { Button, Card, Col, Row, Form, Spinner, Alert, InputGroup } from 'react-bootstrap' +import { Button, Card, Col, Row, Form, Spinner, InputGroup } from 'react-bootstrap' import styles from './BuySubscription.module.css' import stepStyles from '../PremiumPurchaseWizard/Steps/Steps.module.css' import NumberElement from '../../Number/Number' @@ -16,10 +16,8 @@ import { getProviderCurrencyCode, getDiscountPercent, getTierApiProductId, - getTierProductId, - getFallbackSubscriptionPrice -} from '../../../utils/pricingUtils' -import { VAT_RATES } from '../../../utils/PricingUtils' + VAT_RATES +} from '../../../utils/PricingUtils' interface Props { activePremiumProduct: PremiumProduct @@ -255,33 +253,33 @@ function BuySubscription(props: Props) { const getTotalDiscountInfo = (): { creatorPercent: number | null; discountCodePercent: number | null; totalSavings: number | null } => { const creatorPercent = props.selectedTier ? getDiscountPercentValue(getProductIdForTier(props.selectedTier, getCurrentDuration())) : null const discountCodePercent = validatedDiscount?.amountType === 'percent' ? validatedDiscount.amount : null - + const originalPrice = getOriginalPrice() const basePrice = getSubscriptionPrice() const finalPrice = getFinalPrice(basePrice) - + if (originalPrice && finalPrice < originalPrice) { const totalSavings = Math.round((1 - finalPrice / originalPrice) * 100) return { creatorPercent, discountCodePercent: discountCodePercent ?? null, totalSavings } } - + return { creatorPercent, discountCodePercent: discountCodePercent ?? null, totalSavings: null } } // If we have wizard selections, use them to determine the selected type and duration const wizardSelectedType = props.selectedTier ? PREMIUM_TYPES.find(type => { - switch (props.selectedTier) { - case PremiumTier.PREMIUM: - return type.productId === 'premium' - case PremiumTier.PREMIUM_PLUS: - return type.productId === 'premium_plus' - case PremiumTier.STARTER: - return type.productId === 'starter_premium' - default: - return type.productId === 'premium' - } - }) + switch (props.selectedTier) { + case PremiumTier.PREMIUM: + return type.productId === 'premium' + case PremiumTier.PREMIUM_PLUS: + return type.productId === 'premium_plus' + case PremiumTier.STARTER: + return type.productId === 'starter_premium' + default: + return type.productId === 'premium' + } + }) : undefined const wizardIsYearOption = props.selectedDuration === Duration.YEARLY @@ -411,9 +409,8 @@ function BuySubscription(props: Props) {

Tier:{' '} {getDisplayTierName()} @@ -495,7 +492,6 @@ function BuySubscription(props: Props) { - {/* Code Input Section - Side by side on desktop */} @@ -531,8 +527,8 @@ function BuySubscription(props: Props) { )} {appliedCreatorCode && !pricingError && (

- {getDiscountPercentValue(getProductIdForTier(props.selectedTier!, getCurrentDuration())) - ? `✓ Creator code applied! You get ${getDiscountPercentValue(getProductIdForTier(props.selectedTier!, getCurrentDuration()))}% off` + {getDiscountPercentValue(getProductIdForTier(props.selectedTier!, getCurrentDuration())) + ? `✓ Creator code applied! You get ${getDiscountPercentValue(getProductIdForTier(props.selectedTier!, getCurrentDuration()))}% off` : '✓ Creator code applied'}
)} @@ -639,7 +635,7 @@ function BuySubscription(props: Props) { const activeEl = document.activeElement as HTMLElement | null if (activeEl && activeEl.tagName === 'BUTTON') { activeEl.innerText = 'Redirecting to payment provider...' - ;(activeEl as HTMLButtonElement).disabled = true + ; (activeEl as HTMLButtonElement).disabled = true } }} > diff --git a/components/Premium/PremiumPurchaseWizard/PremiumPurchaseWizard.tsx b/components/Premium/PremiumPurchaseWizard/PremiumPurchaseWizard.tsx index f7d3f3a5e..cd53c506a 100644 --- a/components/Premium/PremiumPurchaseWizard/PremiumPurchaseWizard.tsx +++ b/components/Premium/PremiumPurchaseWizard/PremiumPurchaseWizard.tsx @@ -25,7 +25,6 @@ function PremiumPurchaseWizard(props: Props) { const totalSteps = 4 - // Get country code on mount useEffect(() => { if (typeof window !== 'undefined') { const stored = localStorage.getItem('countryCode') @@ -33,7 +32,6 @@ function PremiumPurchaseWizard(props: Props) { } }, []) - // Helper function to get current tier from active premium product const getCurrentTier = (): PremiumTier | null => { if (!props.activePremiumProduct) return null @@ -45,7 +43,6 @@ function PremiumPurchaseWizard(props: Props) { return null } - // Helper function to get tier rank for comparison const getTierRank = (tier: PremiumTier): number => { switch (tier) { case PremiumTier.STARTER: @@ -59,12 +56,10 @@ function PremiumPurchaseWizard(props: Props) { } } - // Helper function to check if user has active premium const hasActivePremium = (): boolean => { return props.activePremiumProduct && props.activePremiumProduct.expires.getTime() > new Date().getTime() } - // Helper function to get suggested upgrade tier const getSuggestedUpgradeTier = (): PremiumTier | null => { const currentTier = getCurrentTier() if (!currentTier) return null @@ -81,7 +76,6 @@ function PremiumPurchaseWizard(props: Props) { } } - // Initialize wizard based on URL parameters and current state useEffect(() => { const urlParams = new URLSearchParams(window.location.search) const tierParam = urlParams.get('tier') @@ -92,13 +86,11 @@ function PremiumPurchaseWizard(props: Props) { setUrlDiscountCode(codeParam) } - // If tier is specified in URL, pre-select it const preSelectedTier = parseTierFromUrl(tierParam) if (preSelectedTier) { setSelectedTier(preSelectedTier) - // Skip tier selection step if it's premium+ or if user already has a lower tier const currentTier = getCurrentTier() const currentTierRank = currentTier ? getTierRank(currentTier) : 0 const selectedTierRank = getTierRank(preSelectedTier) @@ -107,7 +99,6 @@ function PremiumPurchaseWizard(props: Props) { setCurrentStep(2) // Skip to payment method step } } else if (hasActivePremium()) { - // If user has active premium but no tier specified, suggest upgrade const suggestedTier = getSuggestedUpgradeTier() if (suggestedTier) { setSelectedTier(suggestedTier) @@ -122,9 +113,8 @@ function PremiumPurchaseWizard(props: Props) { switch (currentStep) { case 1: if (isUpgrade) { - return `Upgrade Your ${ - currentTier === PremiumTier.STARTER ? 'Starter Premium' : currentTier === PremiumTier.PREMIUM ? 'Premium' : 'Premium Plus' - }` + return `Upgrade Your ${currentTier === PremiumTier.STARTER ? 'Starter Premium' : currentTier === PremiumTier.PREMIUM ? 'Premium' : 'Premium Plus' + }` } return 'Choose Your Premium Tier' case 2: diff --git a/components/Premium/PremiumPurchaseWizard/Steps/DurationSelectionStep.tsx b/components/Premium/PremiumPurchaseWizard/Steps/DurationSelectionStep.tsx index adc8e30fd..12d66d54a 100644 --- a/components/Premium/PremiumPurchaseWizard/Steps/DurationSelectionStep.tsx +++ b/components/Premium/PremiumPurchaseWizard/Steps/DurationSelectionStep.tsx @@ -1,10 +1,8 @@ 'use client' import { Card } from 'react-bootstrap' -import { CheckCircle } from '@mui/icons-material' import styles from './Steps.module.css' import { Duration, PurchaseType, PremiumTier, getTierDisplayName, getDurationOptions } from '../types' -import { SUBSCRIPTION_PRICES } from '../../../../utils/pricingUtils' -import { VAT_RATES } from '../../../../utils/PricingUtils' +import { VAT_RATES, SUBSCRIPTION_PRICES } from '../../../../utils/PricingUtils' interface Props { selectedType: PurchaseType @@ -14,10 +12,10 @@ interface Props { countryCode?: string } -export default function DurationSelectionStep({ - selectedType, - selectedTier, - selectedDuration, +export default function DurationSelectionStep({ + selectedType, + selectedTier, + selectedDuration, onDurationSelect, countryCode }: Props) { @@ -59,10 +57,10 @@ export default function DurationSelectionStep({ return getPriceWithVAT(monthlyPrice) } - // Round up to next full cent - const roundUpToCent = (value: number): number => { - return Math.ceil(value * 100) / 100 - } + // Round up to next full cent + const roundUpToCent = (value: number): number => { + return Math.ceil(value * 100) / 100 + } if (selectedType === PurchaseType.COFLCOINS) { return ( @@ -70,9 +68,8 @@ export default function DurationSelectionStep({

How long would you like your{' '} {tierDisplayName} @@ -81,9 +78,8 @@ export default function DurationSelectionStep({

Choose your preferred duration for{' '} {tierDisplayName} diff --git a/components/Premium/PremiumPurchaseWizard/Steps/PurchaseCompletionStep.tsx b/components/Premium/PremiumPurchaseWizard/Steps/PurchaseCompletionStep.tsx index 65cf45ddf..e5d590617 100644 --- a/components/Premium/PremiumPurchaseWizard/Steps/PurchaseCompletionStep.tsx +++ b/components/Premium/PremiumPurchaseWizard/Steps/PurchaseCompletionStep.tsx @@ -2,7 +2,7 @@ import BuySubscription from '../../BuySubscription/BuySubscription' import BuyPremium from '../../BuyPremium/BuyPremium' import styles from './Steps.module.css' -import { Duration, PurchaseType, PremiumTier, getTierDisplayName, getDurationDisplayName } from '../types' +import { Duration, PurchaseType, PremiumTier } from '../types' interface Props { selectedTier: PremiumTier @@ -28,9 +28,9 @@ export default function PurchaseCompletionStep({ return (

{selectedType === PurchaseType.SUBSCRIPTION && ( - { switch (tier) { case PremiumTier.STARTER: @@ -124,18 +122,15 @@ export default function TierSelectionStep({ selectedTier, onTierSelect, currentT } } - // Helper function to check if tier can be selected (upgrade only) const canSelectTier = (tier: PremiumTier): boolean => { if (!isUpgrade || !currentTier) return true return getTierRank(tier) > getTierRank(currentTier) } - // Helper function to check if tier should be highlighted as suggested const isSuggestedTier = (tier: PremiumTier): boolean => { return suggestedTier === tier } - // Helper function to get tier display status const getTierStatus = (tier: PremiumTier): string => { if (!isUpgrade || !currentTier) return '' @@ -148,7 +143,6 @@ export default function TierSelectionStep({ selectedTier, onTierSelect, currentT return 'higher-upgrade' } - // Format expiry date const formatExpiryDate = (date: Date): string => { return date.toLocaleDateString('en-US', { year: 'numeric', diff --git a/cypress/e2e/filter.cy.ts b/cypress/e2e/filter.cy.ts index 948443f8a..5723e1309 100644 --- a/cypress/e2e/filter.cy.ts +++ b/cypress/e2e/filter.cy.ts @@ -8,8 +8,8 @@ describe('Item page', () => { cy.visit('/item/ASPECT_OF_THE_DRAGON') cy.contains('Add Filter').click() cy.get('input[placeholder="Add filter"]').type('shar') - cy.contains('a', 'Sharpness').click() - cy.get('form').contains('SharpnessNone1234567Please fill the filter or remove it').find('input').type('5') + cy.contains('a', /sharpness/i).click() + cy.get('form').contains(/SharpnessNone1234567Please fill the filter or remove it/i).find('input').type('5') cy.contains(/ended.*ago/i).click() cy.url().should('match', /.*\/auction\/.*/i) cy.contains('Sharpness 5').should('be.visible') diff --git a/utils/PricingUtils.tsx b/utils/PricingUtils.tsx index ab980abeb..2a3bd77c9 100644 --- a/utils/PricingUtils.tsx +++ b/utils/PricingUtils.tsx @@ -1,4 +1,6 @@ import { PremiumTier } from '../components/Premium/PremiumPurchaseWizard/types' +import { BatchProductPricingResponse, ProviderPricingOption } from '../api/_generated/skyApi.schemas' +import { Duration } from '../components/Premium/PremiumPurchaseWizard/types' // Base prices in EUR (before tax) export const BASE_PRICES = { @@ -113,15 +115,131 @@ export function calculatePrice(tier: PremiumTier, countryCode?: string, discount } } -export function getPricingPeriodText(tier: PremiumTier): string { - switch (tier) { - case PremiumTier.STARTER: - return 'per year' - case PremiumTier.PREMIUM: - return 'per month' - case PremiumTier.PREMIUM_PLUS: - return 'one-time' - default: - return '' +export const CURRENCY_SYMBOLS: Record = { + 'USD': '$', + 'EUR': '€', + 'GBP': '£', + 'INR': '₹', + 'JPY': '¥', + 'CNY': '¥', + 'CAD': 'C$', + 'AUD': 'A$' +} + +export const getCurrencySymbol = (currencyCode: string): string => { + return CURRENCY_SYMBOLS[currencyCode] || currencyCode +} + +export const getProvider = ( + pricingData: BatchProductPricingResponse | null, + productSlug: string, + providerSlug: string +): ProviderPricingOption | undefined => { + const product = pricingData?.products?.find(p => p.productSlug === productSlug) + return product?.providers?.find(p => p.providerSlug === providerSlug) +} + +export const getProviderPrice = ( + pricingData: BatchProductPricingResponse | null, + productSlug: string, + providerSlug: string +): number | null => { + const provider = getProvider(pricingData, productSlug, providerSlug) + return provider ? (provider.discountedPrice ?? provider.originalPrice) : null +} + +export const getProviderOriginalPrice = ( + pricingData: BatchProductPricingResponse | null, + productSlug: string, + providerSlug: string +): number | null => { + return getProvider(pricingData, productSlug, providerSlug)?.originalPrice ?? null +} + +export const getProviderCurrencyCode = ( + pricingData: BatchProductPricingResponse | null, + productSlug: string, + providerSlug: string +): string => { + return getProvider(pricingData, productSlug, providerSlug)?.currencyCode ?? 'EUR' +} + +export const getDiscountPercent = ( + pricingData: BatchProductPricingResponse | null, + productSlug: string +): number | null => { + return pricingData?.products?.find(p => p.productSlug === productSlug)?.discountPercent ?? null +} + +const TIER_PRODUCT_MAP: Record = { + [PremiumTier.PREMIUM]: 'premium', + [PremiumTier.PREMIUM_PLUS]: 'premium_plus', + [PremiumTier.STARTER]: 'starter_premium' +} + +export const getTierProductId = (tier: PremiumTier): string => { + return TIER_PRODUCT_MAP[tier] ?? 'premium' +} + +const TIER_SLUG_MAP: Record = { + [PremiumTier.PREMIUM]: { monthly: 'l_premium', yearly: 'l_premium-year' }, + [PremiumTier.PREMIUM_PLUS]: { monthly: 'l_prem_plus', yearly: 'l_prem_plus-year' }, + [PremiumTier.STARTER]: { monthly: 'l_starter_premium', yearly: 'l_starter_premium' } +} + +export const getTierSubscriptionSlug = (tier: PremiumTier, isYearly: boolean): string => { + const slugs = TIER_SLUG_MAP[tier] + return isYearly ? slugs.yearly : slugs.monthly +} + +const TIER_API_PRODUCT_MAP: Record = { + [PremiumTier.PREMIUM]: 'l_premium', + [PremiumTier.PREMIUM_PLUS]: 'l_prem_plus', + [PremiumTier.STARTER]: 'l_starter_premium' +} + +export const getTierApiProductId = (tier: PremiumTier, isYearlyOrDuration: boolean | Duration = false): string => { + const baseId = TIER_API_PRODUCT_MAP[tier] + + if (typeof isYearlyOrDuration === 'string') { + switch (isYearlyOrDuration) { + case Duration.YEARLY: + return tier !== PremiumTier.STARTER ? `${baseId}-year` : baseId + case Duration.QUARTER: + return tier !== PremiumTier.STARTER ? `${baseId}-quarter` : baseId + case Duration.MONTHLY: + default: + return baseId + } } + + // Handle legacy boolean + const isYearly = isYearlyOrDuration as boolean + return isYearly && tier !== PremiumTier.STARTER ? `${baseId}-year` : baseId +} + +export const SUBSCRIPTION_PRICES: Record = { + 'premium': { monthly: 9.69, quarterly: 27.69, yearly: 96.69 }, + 'premium_plus': { monthly: 35.69, quarterly: 99.69, yearly: 354.2 }, + 'starter_premium': { monthly: 16.99, quarterly: 16.99, yearly: 16.99 } } + +export const getFallbackSubscriptionPrice = (productId: string, isYearly: boolean | Duration = false): number => { + const prices = SUBSCRIPTION_PRICES[productId] + if (!prices) return -1 + + if (typeof isYearly === 'string') { + switch (isYearly) { + case Duration.YEARLY: + return prices.yearly + case Duration.QUARTER: + return prices.quarterly + case Duration.MONTHLY: + default: + return prices.monthly + } + } + + // Handle legacy boolean + return (isYearly as boolean) ? prices.yearly : prices.monthly +} \ No newline at end of file diff --git a/utils/pricingUtils.ts b/utils/pricingUtils.ts deleted file mode 100644 index 45afa3b2d..000000000 --- a/utils/pricingUtils.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { BatchProductPricingResponse, ProviderPricingOption } from '../api/_generated/skyApi.schemas' -import { Duration, PremiumTier } from '../components/Premium/PremiumPurchaseWizard/types' - -// Currency symbol mapping -export const CURRENCY_SYMBOLS: Record = { - 'USD': '$', - 'EUR': '€', - 'GBP': '£', - 'INR': '₹', - 'JPY': '¥', - 'CNY': '¥', - 'CAD': 'C$', - 'AUD': 'A$' -} - -// Get currency symbol or code -export const getCurrencySymbol = (currencyCode: string): string => { - return CURRENCY_SYMBOLS[currencyCode] || currencyCode -} - -// Find a provider in pricing data -export const getProvider = ( - pricingData: BatchProductPricingResponse | null, - productSlug: string, - providerSlug: string -): ProviderPricingOption | undefined => { - const product = pricingData?.products?.find(p => p.productSlug === productSlug) - return product?.providers?.find(p => p.providerSlug === providerSlug) -} - -// Get discounted or original price -export const getProviderPrice = ( - pricingData: BatchProductPricingResponse | null, - productSlug: string, - providerSlug: string -): number | null => { - const provider = getProvider(pricingData, productSlug, providerSlug) - return provider ? (provider.discountedPrice ?? provider.originalPrice) : null -} - -// Get original price -export const getProviderOriginalPrice = ( - pricingData: BatchProductPricingResponse | null, - productSlug: string, - providerSlug: string -): number | null => { - return getProvider(pricingData, productSlug, providerSlug)?.originalPrice ?? null -} - -// Get currency code -export const getProviderCurrencyCode = ( - pricingData: BatchProductPricingResponse | null, - productSlug: string, - providerSlug: string -): string => { - return getProvider(pricingData, productSlug, providerSlug)?.currencyCode ?? 'EUR' -} - -// Get discount percentage -export const getDiscountPercent = ( - pricingData: BatchProductPricingResponse | null, - productSlug: string -): number | null => { - return pricingData?.products?.find(p => p.productSlug === productSlug)?.discountPercent ?? null -} - -// Premium tier to product ID mapping -const TIER_PRODUCT_MAP: Record = { - [PremiumTier.PREMIUM]: 'premium', - [PremiumTier.PREMIUM_PLUS]: 'premium_plus', - [PremiumTier.STARTER]: 'starter_premium' -} - -// Get product ID from premium tier -export const getTierProductId = (tier: PremiumTier): string => { - return TIER_PRODUCT_MAP[tier] ?? 'premium' -} - -// Premium tier to subscription product slug mapping -const TIER_SLUG_MAP: Record = { - [PremiumTier.PREMIUM]: { monthly: 'l_premium', yearly: 'l_premium-year' }, - [PremiumTier.PREMIUM_PLUS]: { monthly: 'l_prem_plus', yearly: 'l_prem_plus-year' }, - [PremiumTier.STARTER]: { monthly: 'l_starter_premium', yearly: 'l_starter_premium' } -} - -// Get subscription product slug for a tier -export const getTierSubscriptionSlug = (tier: PremiumTier, isYearly: boolean): string => { - const slugs = TIER_SLUG_MAP[tier] - return isYearly ? slugs.yearly : slugs.monthly -} - -// Premium tier to API product ID mapping -const TIER_API_PRODUCT_MAP: Record = { - [PremiumTier.PREMIUM]: 'l_premium', - [PremiumTier.PREMIUM_PLUS]: 'l_prem_plus', - [PremiumTier.STARTER]: 'l_starter_premium' -} - -// Get API product ID for a tier - supports both legacy boolean and new Duration enum -export const getTierApiProductId = (tier: PremiumTier, isYearlyOrDuration: boolean | Duration = false): string => { - const baseId = TIER_API_PRODUCT_MAP[tier] - - // Handle Duration enum - if (typeof isYearlyOrDuration === 'string') { - switch (isYearlyOrDuration) { - case Duration.YEARLY: - return tier !== PremiumTier.STARTER ? `${baseId}-year` : baseId - case Duration.QUARTER: - return tier !== PremiumTier.STARTER ? `${baseId}-quarter` : baseId - case Duration.MONTHLY: - default: - return baseId - } - } - - // Handle legacy boolean - const isYearly = isYearlyOrDuration as boolean - return isYearly && tier !== PremiumTier.STARTER ? `${baseId}-year` : baseId -} - -// Fallback subscription prices -export const SUBSCRIPTION_PRICES: Record = { - 'premium': { monthly: 9.69, quarterly: 27.69, yearly: 96.69 }, - 'premium_plus': { monthly: 35.69, quarterly: 99.69, yearly: 354.2 }, - 'starter_premium': { monthly: 16.99, quarterly: 16.99, yearly: 16.99 } -} - -// Get fallback subscription price -export const getFallbackSubscriptionPrice = (productId: string, isYearly: boolean | Duration = false): number => { - const prices = SUBSCRIPTION_PRICES[productId] - if (!prices) return -1 - - // Handle Duration enum - if (typeof isYearly === 'string') { - switch (isYearly) { - case Duration.YEARLY: - return prices.yearly - case Duration.QUARTER: - return prices.quarterly - case Duration.MONTHLY: - default: - return prices.monthly - } - } - - // Handle legacy boolean - return (isYearly as boolean) ? prices.yearly : prices.monthly -}