import { CreateLinkEvent, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { BuilderFurniPlaceableStatus, CatalogPurchaseState, CatalogType, DispatchUiEvent, GetClubMemberLevel, LocalizeText, NotificationBubbleType, Offer, ProductTypeEnum, SendMessageComposer } from '../../../../../api'; import { Button, LayoutLoadingSpinnerView, Text } from '../../../../../common'; import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent } from '../../../../../events'; import { useCatalogActions, useCatalogData, useCatalogSkipPurchaseConfirmation, useCatalogUiState, useNotification, usePurse, useUiEvent } from '../../../../../hooks'; interface CatalogPurchaseWidgetViewProps { noGiftOption?: boolean; purchaseCallback?: () => void; } let isPurchasingCatalogItem = false; export const CatalogPurchaseWidgetView: FC = props => { const { noGiftOption = false, purchaseCallback = null } = props; const [ builderPlaceableRefreshTick, setBuilderPlaceableRefreshTick ] = useState(0); const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE); const [ catalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); const { currentOffer = null, currentPage = null } = useCatalogData(); const { currentType = CatalogType.NORMAL, purchaseOptions = null, setPurchaseOptions = null, setCatalogPlaceMultipleObjects = null } = useCatalogUiState(); const { requestOfferToMover = null, getBuilderFurniPlaceableStatus = null, getNodesByOfferId = null } = useCatalogActions(); const { getCurrencyAmount = null } = usePurse(); const { showSingleBubble = null } = useNotification(); const onCatalogEvent = useCallback((event: CatalogEvent) => { switch(event.type) { case CatalogPurchasedEvent.PURCHASE_SUCCESS: isPurchasingCatalogItem = false; setPurchaseState(CatalogPurchaseState.NONE); return; case CatalogPurchaseFailureEvent.PURCHASE_FAILED: isPurchasingCatalogItem = false; setPurchaseState(CatalogPurchaseState.FAILED); return; case CatalogPurchaseNotAllowedEvent.NOT_ALLOWED: isPurchasingCatalogItem = false; setPurchaseState(CatalogPurchaseState.FAILED); return; case CatalogPurchaseSoldOutEvent.SOLD_OUT: isPurchasingCatalogItem = false; setPurchaseState(CatalogPurchaseState.SOLD_OUT); return; } }, []); useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent); useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent); useUiEvent(CatalogPurchaseNotAllowedEvent.NOT_ALLOWED, onCatalogEvent); useUiEvent(CatalogPurchaseSoldOutEvent.SOLD_OUT, onCatalogEvent); const isLimitedSoldOut = useMemo(() => { if(!currentOffer) return false; if(purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)) return false; if(currentOffer.pricingModel === Offer.PRICING_MODEL_SINGLE) { const product = currentOffer.product; if(product && product.isUniqueLimitedItem) return !product.uniqueLimitedItemsLeft; } return false; }, [ currentOffer, purchaseOptions ]); const purchase = (isGift: boolean = false) => { if(!currentOffer || isPurchasingCatalogItem) return; if(GetClubMemberLevel() < currentOffer.clubLevel) { CreateLinkEvent('habboUI/open/hccenter'); return; } if(isGift) { DispatchUiEvent(new CatalogInitGiftEvent(currentOffer.page.pageId, currentOffer.offerId, purchaseOptions.extraData)); return; } isPurchasingCatalogItem = true; setPurchaseState(CatalogPurchaseState.PURCHASE); setTimeout(() => { isPurchasingCatalogItem = false; }, 10000); if(purchaseCallback) { purchaseCallback(); return; } let pageId = currentOffer.page.pageId; if(pageId === -1 && getNodesByOfferId) { const nodes = getNodesByOfferId(currentOffer.offerId); if(nodes && nodes.length) pageId = nodes[0].pageId; } SendMessageComposer(new PurchaseFromCatalogComposer(pageId, currentOffer.offerId, purchaseOptions.extraData, purchaseOptions.quantity)); }; useEffect(() => { if(!currentOffer) return; setPurchaseState(CatalogPurchaseState.NONE); }, [ currentOffer, setPurchaseOptions ]); useEffect(() => { let timeout: ReturnType = null; if((purchaseState === CatalogPurchaseState.CONFIRM) || (purchaseState === CatalogPurchaseState.FAILED)) { timeout = setTimeout(() => setPurchaseState(CatalogPurchaseState.NONE), 3000); } return () => { if(timeout) clearTimeout(timeout); }; }, [ purchaseState ]); // Builders-club state — derived + hooks MUST run unconditionally on // every render so the hook order stays stable even when currentOffer // is null (the `if(!currentOffer) return null` below would otherwise // hide the useMemo/useEffect block from the first render and React // would flag "Rendered more hooks than during the previous render"). const isBuildersClubOffer = (currentType === CatalogType.BUILDER); const isBuildersClubPlaceable = isBuildersClubOffer && !!currentOffer && !!currentOffer.product && ((currentOffer.product.productType === ProductTypeEnum.FLOOR) || (currentOffer.product.productType === ProductTypeEnum.WALL)); const builderPlaceableStatus = useMemo(() => { if(!isBuildersClubPlaceable || !getBuilderFurniPlaceableStatus || !currentOffer) return BuilderFurniPlaceableStatus.OKAY; return getBuilderFurniPlaceableStatus(currentOffer); }, [ currentOffer, getBuilderFurniPlaceableStatus, isBuildersClubPlaceable, builderPlaceableRefreshTick ]); const buildersClubPlaceOneButtonStyle = useMemo(() => ({ background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)', borderColor: '#d79d2e', color: '#ffffff' }), []); useEffect(() => { if(!isBuildersClubPlaceable) return; const interval = setInterval(() => setBuilderPlaceableRefreshTick(prevValue => (prevValue + 1)), 500); return () => clearInterval(interval); }, [ isBuildersClubPlaceable ]); if(!currentOffer) return null; const PurchaseButton = () => { if(isBuildersClubPlaceable) { const hasMissingExtraParam = (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)); const isBlockedByVisitors = (builderPlaceableStatus === BuilderFurniPlaceableStatus.VISITORS_IN_ROOM); const isDisabled = hasMissingExtraParam || isBlockedByVisitors || (builderPlaceableStatus === BuilderFurniPlaceableStatus.MISSING_OFFER) || (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_IN_ROOM) || (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_ROOM_OWNER) || (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN); const startBuilderPlacement = (placeMultiple: boolean) => { if(builderPlaceableStatus === BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED) { showSingleBubble(LocalizeText('room.error.max_furniture'), NotificationBubbleType.INFO); return; } if(isDisabled) return; setCatalogPlaceMultipleObjects(placeMultiple); requestOfferToMover(currentOffer); }; return (
{ isBlockedByVisitors && { LocalizeText('builder.placement_widget.error.visitors') } } { (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN) && { LocalizeText('builder.placement_widget.error.not_group_admin') } }
); } const priceCredits = (currentOffer.priceInCredits * purchaseOptions.quantity); const pricePoints = (currentOffer.priceInActivityPoints * purchaseOptions.quantity); if(GetClubMemberLevel() < currentOffer.clubLevel) return ; if(isLimitedSoldOut) return ; if(priceCredits > getCurrencyAmount(-1)) return ; if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return ; switch(purchaseState) { case CatalogPurchaseState.CONFIRM: return ; case CatalogPurchaseState.PURCHASE: return ; case CatalogPurchaseState.FAILED: return ; case CatalogPurchaseState.SOLD_OUT: return ; case CatalogPurchaseState.NONE: default: return ; } }; return ( <> { (!isBuildersClubOffer && !noGiftOption && !currentOffer.isRentOffer) && } ); };