Skip to content
65 changes: 5 additions & 60 deletions components/CoflCoins/CoflCoinsPurchase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import { postApiTopupPlaystore } from '../../api/_generated/skyApi'
import { useCoflCoins } from '../../utils/Hooks'
import CoflCoinPurchaseWizard from './CoflCoinPurchaseWizard'
import PurchaseElement from './PurchaseElement'
import { Country, getCountry, getCountryFromUserLanguage } from '../../utils/CountryUtils'
import CountrySelect from '../CountrySelect/CountrySelect'
import { USER_COUNTRY_CODE } from '../../utils/SettingsUtils'
import { useCountryDetection } from '../../hooks/useCountryDetection'
import styles from './CoflCoinsPurchase.module.css'

interface Props {
Expand All @@ -21,8 +20,7 @@ function Payment(props: Props) {
let [loadingId, setLoadingId] = useState('')
let [currentRedirectLink, setCurrentRedirectLink] = useState('')
let [showAll, setShowAll] = useState(false)
let [defaultCountry, setDefaultCountry] = useState<Country>()
let [selectedCountry, setSelectedCountry] = useState<Country>()
const { selectedCountry, handleCountryChange } = useCountryDetection()
let [useWizard, setUseWizard] = useState(true)
let [isGooglePlayAvailable, setIsGooglePlayAvailable] = useState(false)
let coflCoins = useCoflCoins()
Expand Down Expand Up @@ -63,9 +61,6 @@ function Payment(props: Props) {
}
}

// Load country
loadDefaultCountry()

// Check for Android Billing availability with a delay
setTimeout(() => {
checkGooglePlayAvailability()
Expand Down Expand Up @@ -161,39 +156,6 @@ function Payment(props: Props) {
setIsGooglePlayAvailable(available)
}

async function loadDefaultCountry() {
let cachedCountryCode = localStorage.getItem(USER_COUNTRY_CODE)
if (cachedCountryCode) {
setDefaultCountry(getCountry(cachedCountryCode))
setSelectedCountry(getCountry(cachedCountryCode))
return
}

let response: Response | null = null
try {
response = await fetch('https://api.country.is')
} catch (error) {
console.warn('Failed to fetch country from api.country.is:', error)
// Fallback to country from browser language
let country = getCountryFromUserLanguage()
setDefaultCountry(country)
setSelectedCountry(country)
return
}

if (response && response.ok) {
let result = await response.json()
let country = getCountry(result.country) || getCountryFromUserLanguage()
setDefaultCountry(country)
setSelectedCountry(country)
localStorage.setItem(USER_COUNTRY_CODE, result.country)
} else {
let country = getCountryFromUserLanguage()
setDefaultCountry(country)
setSelectedCountry(country)
}
}

function onPayPaypal(productId: string, coflCoins?: number) {
setLoadingId(coflCoins ? `${productId}_${coflCoins}` : productId)
setCurrentRedirectLink('')
Expand Down Expand Up @@ -294,11 +256,7 @@ function Payment(props: Props) {
if (!props.cancellationRightLossConfirmed || !selectedCountry) {
return (
<div>
{defaultCountry ? (
<CountrySelect key="country-select" isLoading={!defaultCountry} defaultCountry={defaultCountry} onCountryChange={setSelectedCountry} />
) : (
<CountrySelect key="loading-country-select" isLoading />
)}
<CountrySelect onCountryChange={handleCountryChange} />

{!props.cancellationRightLossConfirmed && (
<Alert variant="warning" style={{ marginTop: '20px' }}>
Expand All @@ -320,16 +278,7 @@ function Payment(props: Props) {
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div>
{defaultCountry ? (
<CountrySelect
key="country-select"
isLoading={!defaultCountry}
defaultCountry={defaultCountry}
onCountryChange={setSelectedCountry}
/>
) : (
<CountrySelect key="loading-country-select" isLoading />
)}
<CountrySelect onCountryChange={handleCountryChange} />
</div>
<Button variant="outline-secondary" size="sm" onClick={() => setUseWizard(false)}>
Switch to Classic View
Expand All @@ -354,11 +303,7 @@ function Payment(props: Props) {
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div>
{defaultCountry ? (
<CountrySelect key="country-select" isLoading={!defaultCountry} defaultCountry={defaultCountry} onCountryChange={setSelectedCountry} />
) : (
<CountrySelect key="loading-country-select" isLoading />
)}
<CountrySelect onCountryChange={handleCountryChange} />
</div>
<Button variant="outline-primary" size="sm" onClick={() => setUseWizard(true)}>
Switch to New Wizard
Expand Down
18 changes: 10 additions & 8 deletions components/CountrySelect/CountrySelect.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
'use client'
import { useRef, useState, type JSX } from 'react'
import { useId, useRef, type JSX } from 'react'
import { Menu, MenuItem, Typeahead } from 'react-bootstrap-typeahead'
import { Form, InputGroup } from 'react-bootstrap'
import { Country, getCountries } from '../../utils/CountryUtils'
import { default as TypeaheadType } from 'react-bootstrap-typeahead/types/core/Typeahead'
import { useCountryDetection } from '../../hooks/useCountryDetection'

interface Props {
onCountryChange?(country: Country)
defaultCountry?: Country
isLoading?: boolean
}

export default function CountrySelect(props: Props) {
const key = useId()
const countryOptions = getCountries()
let [selectedCountry, setSelectedCountryCode] = useState<any>(props.defaultCountry)
let { selectedCountry, handleCountryChange } = useCountryDetection();
let ref = useRef<TypeaheadType>(null)

function getCountryImage(countryCode: string): JSX.Element {
Expand All @@ -28,20 +28,22 @@ export default function CountrySelect(props: Props) {
/>
)
}

return (
<div style={{ display: 'flex', alignItems: 'center', gap: 15, paddingBottom: 15 }}>
<label htmlFor="countryTypeahead">Your Country: </label>
<Typeahead
key={`${key}-${!selectedCountry ? 'disabled' : 'enabled'}`}
id="countryTypeahead"
style={{ width: 'auto' }}
disabled={props.isLoading}
placeholder={props.isLoading ? 'Loading...' : 'Select your country'}
disabled={!selectedCountry}
placeholder={!selectedCountry ? 'Loading...' : 'Select your country'}
ref={ref}
defaultSelected={selectedCountry ? [selectedCountry] : []}
isLoading={props.isLoading}
isLoading={!selectedCountry}
onChange={e => {
if (e[0]) {
setSelectedCountryCode(e[0])
handleCountryChange(e[0] as Country)
if (props.onCountryChange) {
props.onCountryChange(e[0] as Country)
}
Expand Down
20 changes: 0 additions & 20 deletions components/Premium/BuyPremium/BuyPremium.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,26 +169,6 @@ function BuyPremium(props: Props) {
return props.selectedTier ? getTierDisplayName(props.selectedTier) : purchasePremiumType.label
}

const getDurationDisplayName = () => {
if (!props.selectedDuration) return getDurationString()
switch (props.selectedDuration) {
case Duration.HOUR:
return '1 Hour'
case Duration.WEEK:
return '1 Week'
case Duration.MONTHLY:
return '1 Month'
case Duration.QUARTER:
return '3 Months'
case Duration.QUARTER:
return 'Quarterly'
case Duration.YEARLY:
return 'Yearly'
default:
return getDurationString()
}
}

// If coming from wizard, show only the selected option with summary
if (props.selectedTier && props.selectedDuration !== undefined) {
return (
Expand Down
19 changes: 6 additions & 13 deletions components/Premium/BuySubscription/BuySubscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getTierApiProductId,
VAT_RATES
} from '../../../utils/PricingUtils'
import { useCountryDetection } from '../../../hooks/useCountryDetection'

interface Props {
activePremiumProduct: PremiumProduct
Expand All @@ -40,11 +41,8 @@ function BuySubscription(props: Props) {
const [pendingYearlyDiscount, setPendingYearlyDiscount] = useState<ValidatedDiscount | null>(null)
const [isValidatingDiscount, setIsValidatingDiscount] = useState(false)
const [discountError, setDiscountError] = useState<string | null>(null)

// Get country code from prop or localStorage
const countryCode = (props.countryCode && props.countryCode.length > 0)
? props.countryCode
: (typeof window !== 'undefined' ? localStorage.getItem('countryCode') || 'US' : 'US')
const { selectedCountry } = useCountryDetection()
const country = props.countryCode || selectedCountry?.value || 'US'

useEffect(() => {
fetchPricing(creatorCode)
Expand Down Expand Up @@ -85,7 +83,7 @@ function BuySubscription(props: Props) {

const response = await postApiTopupRates({
productSlugs: productSlugs,
countryCode: countryCode,
countryCode: country,
creatorCode: code || null
}, {
headers: headers
Expand Down Expand Up @@ -217,15 +215,13 @@ function BuySubscription(props: Props) {

// Get VAT rate for current country
const getVATRate = (): number => {
if (!countryCode) return 0
const upperCode = countryCode.toUpperCase()
const upperCode = country.toUpperCase()
return VAT_RATES[upperCode] ?? 0
}

// Check if VAT should be included in the price (known country with VAT rate)
const shouldIncludeVATInPrice = (): boolean => {
if (!countryCode) return false
const upperCode = countryCode.toUpperCase()
const upperCode = country.toUpperCase()
return upperCode !== 'US' && VAT_RATES[upperCode] !== undefined
}

Expand Down Expand Up @@ -285,9 +281,6 @@ function BuySubscription(props: Props) {
})
: undefined

const wizardIsYearOption = props.selectedDuration === Duration.YEARLY
const wizardIsQuarterOption = props.selectedDuration === Duration.QUARTER

// Helper to get the current duration for product ID
const getCurrentDuration = (): Duration => {
return props.selectedDuration || Duration.MONTHLY
Expand Down
17 changes: 6 additions & 11 deletions components/Premium/PremiumPurchaseWizard/PremiumPurchaseWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PremiumTier, PurchaseType, Duration } from './types'
import { TierSelectionStep, PaymentMethodStep, DurationSelectionStep, PurchaseCompletionStep } from './Steps'
import { PREMIUM_RANK } from '../../../utils/PremiumTypeUtils'
import { parseTierFromUrl } from '../../../utils/PremiumUpgradeUtils'
import { useCountryDetection } from '../../../hooks/useCountryDetection'

interface Props {
activePremiumProduct: PremiumProduct
Expand All @@ -21,17 +22,10 @@ function PremiumPurchaseWizard(props: Props) {
const [selectedType, setSelectedType] = useState<PurchaseType | null>(null)
const [selectedDuration, setSelectedDuration] = useState<Duration | null>(null)
const [urlDiscountCode, setUrlDiscountCode] = useState<string | null>(null)
const [countryCode, setCountryCode] = useState<string>('US')
const { selectedCountry, handleCountryChange } = useCountryDetection()

const totalSteps = 4

useEffect(() => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('countryCode')
if (stored) setCountryCode(stored)
}
}, [])

const getCurrentTier = (): PremiumTier | null => {
if (!props.activePremiumProduct) return null

Expand Down Expand Up @@ -182,7 +176,8 @@ function PremiumPurchaseWizard(props: Props) {
isUpgrade={isUpgrade}
suggestedTier={suggestedTier}
activePremiumProduct={props.activePremiumProduct}
onCountryCodeChange={setCountryCode}
selectedCountry={selectedCountry}
onCountryChange={handleCountryChange}
/>
)
case 2:
Expand All @@ -194,7 +189,7 @@ function PremiumPurchaseWizard(props: Props) {
selectedTier={selectedTier!}
selectedDuration={selectedDuration}
onDurationSelect={handleDurationSelect}
countryCode={countryCode}
countryCode={selectedCountry?.value}
/>
)
case 4:
Expand All @@ -207,7 +202,7 @@ function PremiumPurchaseWizard(props: Props) {
premiumSubscriptions={props.premiumSubscriptions}
onNewActivePremiumProduct={props.onNewActivePremiumProduct}
initialDiscountCode={urlDiscountCode}
countryCode={countryCode}
countryCode={selectedCountry?.value}
/>
)
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ export default function PurchaseCompletionStep({
selectedTier={selectedTier}
selectedDuration={selectedDuration}
initialDiscountCode={initialDiscountCode}
// pass countryCode if BuyPremium ever needs VAT-aware logic
// countryCode={countryCode}
/>
)}
</div>
Expand Down
67 changes: 67 additions & 0 deletions components/Premium/PremiumPurchaseWizard/Steps/TierCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ReactNode } from 'react'
import { Card } from 'react-bootstrap'
import styles from './Steps.module.css'
import { PremiumTier } from '../types'

export interface TierConfig {
tier: PremiumTier
icon: string
title: string
titleClass?: string
features: string[]
description: string
upgradeDescription?: string
}

export type TierStatus = 'current' | 'downgrade' | 'upgrade' | 'higher-upgrade' | ''

interface TierCardProps {
config: TierConfig
pricing: ReactNode
status: TierStatus
isSuggested: boolean
canSelect: boolean
onSelect: (tier: PremiumTier) => void
extraBadge?: string
suggestedFeature?: string
}

export default function TierCard({ config, pricing, status, isSuggested, canSelect, onSelect, extraBadge, suggestedFeature }: TierCardProps) {
const { tier, icon, title, titleClass, features, description, upgradeDescription } = config

const cardClassName = [
styles.optionCard,
!canSelect && styles.disabled,
status === 'current' && styles.currentTier,
isSuggested && styles.suggested
]
.filter(Boolean)
.join(' ')

return (
<Card className={cardClassName} onClick={() => canSelect && onSelect(tier)}>
<Card.Body className={styles.optionBody}>
<div className={styles.optionIcon}>{icon}</div>
<h5 className={[styles.tierTitle, titleClass && styles[titleClass]].filter(Boolean).join(' ')}>
{title}
{status === 'current' && <span className={styles.currentBadge}>Current</span>}
{isSuggested && <span className={styles.suggestedBadge}>Recommended Upgrade</span>}
</h5>
<div className={styles.tierPrice}>{pricing}</div>
<p className={styles.tierDescription}>{isSuggested && upgradeDescription ? upgradeDescription : description}</p>
{extraBadge && !isSuggested && <small className={styles.recommendation}>{extraBadge}</small>}
{!canSelect && (
<div className={styles.disabledOverlay}>
<small>Cannot downgrade</small>
</div>
)}
<ul className={styles.featureList}>
{features.map(feature => (
<li key={feature}>{feature}</li>
))}
{suggestedFeature && isSuggested && <li className={styles.newFeature}>{suggestedFeature}</li>}
</ul>
</Card.Body>
</Card>
)
}
Loading
Loading