Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
18 changes: 6 additions & 12 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,12 +41,10 @@ function BuySubscription(props: Props) {
const [pendingYearlyDiscount, setPendingYearlyDiscount] = useState<ValidatedDiscount | null>(null)
const [isValidatingDiscount, setIsValidatingDiscount] = useState(false)
const [discountError, setDiscountError] = useState<string | null>(null)
const { selectedCountry } = useCountryDetection()
const [showStarterDiscountHint] = useState(() => Math.random() < 0.2)

// 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 country = props.countryCode || selectedCountry?.value || 'US'

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

const response = await postApiTopupRates({
productSlugs: productSlugs,
countryCode: countryCode,
countryCode: country,
creatorCode: code || null
}, {
headers: headers
Expand Down Expand Up @@ -218,15 +217,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 @@ -286,9 +283,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