Skip to content
Draft
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
17 changes: 8 additions & 9 deletions components/CoflCoins/CoflCoinPaymentSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from '../../api/_generated/skyApi'
import type { BatchProductPricingResponse, ProviderPricingOption } from '../../api/_generated/skyApi.schemas'
import { getProvider, getProviderPrice, getProviderOriginalPrice } from '../../utils/pricingUtils'
import type { BatchProductPricingResponse } from '../../api/_generated/skyApi.schemas'
import { getProvider, getProviderPrice, getProviderOriginalPrice } from '../../utils/PricingUtils'

interface CoflCoinOption {
amount: number
Expand Down Expand Up @@ -118,7 +117,7 @@ function CoflCoinPaymentSelection({ selectedOption, onBack, countryCode, coflCoi
const getDiscountMultiplier = (providerSlug: string, productSlug: string): number | undefined => {
const originalPrice = getProviderOriginalPriceForComponent(providerSlug, productSlug)
const discountedPrice = getProviderPriceForComponent(providerSlug, productSlug)

if (originalPrice && discountedPrice && originalPrice > discountedPrice) {
return discountedPrice / originalPrice
}
Expand Down Expand Up @@ -227,9 +226,9 @@ function CoflCoinPaymentSelection({ selectedOption, onBack, countryCode, coflCoi
<Alert variant="info" style={{ marginBottom: '20px' }}>
<Alert.Heading style={{ fontSize: '0.95rem', marginBottom: '8px' }}>💡 Limited to Google Play</Alert.Heading>
<p style={{ marginBottom: 0, fontSize: '0.9rem' }}>
The Android app only supports Google Play Billing. For other payment methods like PayPal or Stripe, visit the <a
href="https://coflnet.com/premium"
target="_blank"
The Android app only supports Google Play Billing. For other payment methods like PayPal or Stripe, visit the <a
href="https://coflnet.com/premium"
target="_blank"
rel="noopener noreferrer"
style={{ fontWeight: '600' }}
>
Expand All @@ -247,8 +246,8 @@ function CoflCoinPaymentSelection({ selectedOption, onBack, countryCode, coflCoi
</div>

{/* Responsive Grid Layout: Payment Options + Creator Code */}
<div style={{
display: 'grid',
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '20px',
marginBottom: '20px'
Expand Down
52 changes: 25 additions & 27 deletions components/CoflCoins/CoflCoinsPurchase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,48 +42,48 @@ 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) {
checkGooglePlayAvailability()
}
}
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
Expand All @@ -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 {
Expand All @@ -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 })
Expand All @@ -127,39 +127,37 @@ 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 {
toast.error(userMessage, { autoClose: 10000 })
}
}
}

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,
isStandalone: window.matchMedia('(display-mode: standalone)').matches,
userAgent: navigator.userAgent,
referrer: document.referrer
})

setIsGooglePlayAvailable(available)
}

Expand Down Expand Up @@ -232,26 +230,26 @@ 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)
.then(response => {
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) {
Expand Down
2 changes: 1 addition & 1 deletion components/CoflCoins/GenericProviderPurchaseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 5 additions & 12 deletions components/CoflCoins/PurchaseElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
Expand All @@ -70,10 +67,11 @@ export default function PurchaseElement(props: Props) {
/>
) : (
<a href='https://play.google.com/store/apps/details?id=com.coflnet.sky'><p style={{ color: '#adb5bd', marginBottom: 0 }}>There are more options, eg. gift cards in our android app.</p>
</a>
</a>
)}
</>
) // On Android app, only show Google Play option (reuse googlePlayCard)
)

if (props.isAndroidApp) {
return (
<Card className={styles.premiumPlanCard} style={{ width: '100%' }}>
Expand All @@ -89,11 +87,6 @@ export default function PurchaseElement(props: Props) {
)
}

console.log('PurchaseElement render', {
coflCoinsToBuy: props.coflCoinsToBuy,
isGooglePlayAvailable: props.isGooglePlayAvailable
})

return (
<Card className={styles.premiumPlanCard} style={{ width: '100%' }}>
<Card.Header>
Expand Down
2 changes: 1 addition & 1 deletion components/ForgeFlips/ForgeFlips.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
18 changes: 0 additions & 18 deletions components/GenericFlipList/GenericFlipList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
items: T[]
Expand All @@ -26,15 +25,8 @@ export interface FlipListProps<T> {
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 <a> 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
}
Expand Down Expand Up @@ -86,12 +78,10 @@ export function GenericFlipList<T>({
const sentinelRef = React.useRef<HTMLDivElement | null>(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))
}, [])

Expand Down Expand Up @@ -149,7 +139,6 @@ export function GenericFlipList<T>({
setHasPremium(hasHighEnoughPremium(products, PREMIUM_RANK.STARTER))
})

// Call the custom onAfterSignIn if provided
if (onAfterSignIn) {
onAfterSignIn()
}
Expand Down Expand Up @@ -195,7 +184,6 @@ export function GenericFlipList<T>({
}

function getListElement(item: T, blur: boolean) {
// Build the inner content (blur messages + actual content)
const inner = (
<>
{blur ? (
Expand Down Expand Up @@ -271,16 +259,11 @@ export function GenericFlipList<T>({
</>
)

// 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) => {
// If there's an onFlipClick handler, call it and prevent default
// so client-side navigation/behavior can occur. If not, allow
// the anchor to work normally (useful when JS is disabled).
if (onFlipClick) {
e.preventDefault()
onFlipClick(item, e)
Expand Down Expand Up @@ -311,7 +294,6 @@ export function GenericFlipList<T>({
)
}

// Memoized displayed items
const list = useMemo(() => {
if (isProcessing) {
return []
Expand Down
Loading