import _get from 'lodash/get' import _isEmpty from 'lodash/isEmpty' import _some from 'lodash/some' import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react' import { Alert } from 'react-native' import { IEventResponse, IFetchAccessTokenResponse, IPromoCodeResponse, } from '../../api/types' import { SessionHandle, TicketsCore, TicketsCoreHandle } from '../../core' import { SessionHandleType } from '../../core/Session/SessionCoreTypes' import { IBookTicketsOptions, IGetTicketsPayload, IGroupedTickets, } from '../../core/TicketsCore/TicketsCoreTypes' import { IAddToCartResponse, IEvent, IOnFetchTicketsSuccess, ISelectedTicket, ITicket, } from '../../types' import TicketsView from './TicketsView' import { IPasswordProtectedEventData, ITicketsProps } from './types' import { Logger } from '../../utils/Logger' const Tickets = forwardRef( ( { onAddToCartSuccess, onAddToCartError, onFetchTicketsError, styles, texts, isAccessCodeEnabled = false, isPromoEnabled = true, onPressMyOrders, onPressLogout, onFetchTicketsSuccess, onFetchEventError, onLoadingChange, onFetchEventSuccess, onAddToWaitingListError, onAddToWaitingListSuccess, promoCodeCloseIcon, isCheckingCurrentSession, config = { areTicketsGrouped: true, areActivityIndicatorsEnabled: true, areAlertsEnabled: true, areTicketsSortedBySoldOut: true, }, referrerId, debugMode, }, ref ) => { const logger = new Logger({ level: debugMode ? 'debug' : 'error' }) const [isUserLogged, setIsUserLogged] = useState(false) const [isGettingTickets, setIsGettingTickets] = useState(false) const [isGettingEvent, setIsGettingEvent] = useState(false) const [event, setEvent] = useState() const [isBooking, setIsBooking] = useState(false) const [areTicketGroupsShown, setAreTicketGroupsShown] = useState(false) const [tickets, setTickets] = useState([]) const [groupedTickets, setGroupedTickets] = useState([]) const [isWaitingListVisible, setIsWaitingListVisible] = useState(false) const [isAccessCode, setIsAccessCode] = useState(false) const [selectedTicket, setSelectedTicket] = useState() const [promoCodeResponse, setPromoCodeResponse] = useState< IPromoCodeResponse | undefined >(undefined) const [isFirstCall, setIsFirstCall] = useState(true) const [passwordProtectedEventData, setPasswordProtectedEventData] = useState< IPasswordProtectedEventData | undefined >() //#region Refs const eventErrorCodeRef = useRef(0) const ticketsCoreRef = useRef(null) const sessionHandleRef = useRef(null) const isApiErrorRef = useRef(false) //#endregion Refs const showAlert = (message: string) => { if (config.areAlertsEnabled) { Alert.alert('', message) } } const isTicketOnSale = (ticketsForCheck: ITicket[]) => _some( ticketsForCheck, (item: ITicket) => item.salesStarted && !item.salesEnded && !item.soldOut ) const areTicketsOnSale = (): boolean => { if (event?.salesEnded) { return false } if ( config.areTicketsGrouped && areTicketGroupsShown && groupedTickets.length === 0 ) { return false } if (!config.areTicketsGrouped && tickets.length === 0) { return false } if (config.areTicketsGrouped) { return _some(groupedTickets, (gTicket) => isTicketOnSale(gTicket.data)) } else { return isTicketOnSale(tickets) } } //#region Core Api calls const retrieveStoredAccessToken = async () => { if (!ticketsCoreRef.current) { return { eventError: { message: 'Ticket core is not initialized' } } } setIsUserLogged(await ticketsCoreRef.current.getIsUserLoggedIn()) } const getTicketsCore = async (promoCode?: string): Promise => { if (!ticketsCoreRef.current) { return { error: { message: 'Ticket core is not initialized' } } } return await ticketsCoreRef.current.getTickets({ referredId: referrerId, promoCode, areTicketsSortedBySoldOut: config.areTicketsSortedBySoldOut, areTicketsGrouped: config.areTicketsGrouped, }) } const unlockPasswordProtectedEventCore = async ( password: string ): Promise => { if (!ticketsCoreRef.current) { return { eventError: { message: 'Ticket core is not initialized' } } } return await ticketsCoreRef.current.unlockPasswordProtectedEvent(password) } const getEventCore = async (): Promise => { if (!ticketsCoreRef.current) { return { eventError: { message: 'Ticket core is not initialized' } } } return await ticketsCoreRef.current.getEvent() } const addToCartCore = async ( data: IBookTicketsOptions ): Promise => { if (!ticketsCoreRef.current) { return { error: { message: 'Ticket core is not initialized' } } } return await ticketsCoreRef.current.addToCart(data) } //#endregion Core Api calls //#region Fetch Initial Data const fetchInitialData = async () => { try { logger.info('Fetching initial data') await retrieveStoredAccessToken() logger.info('retrieveStoredAccessToken') await getEventData() logger.info('getEventData success') } catch (error) { logger.error('Error while fetching initial data', error) } } //#endregion Fetch Initial Data //#region Api Calls const getTickets = async (promoCode: string = '') => { setIsGettingTickets(true) isApiErrorRef.current = false const { error, tickets: responseTickets, promoCodeResult, isInWaitingList, isAccessCodeRequired, areGroupsShown, } = await getTicketsCore(promoCode) logger.info('getTicketsCore response', { error, tickets: responseTickets, promoCodeResult, isInWaitingList, isAccessCodeRequired, areGroupsShown, }) setIsGettingTickets(false) if (error) { onFetchTicketsError?.(error) isApiErrorRef.current = true return showAlert('Error while getting tickets, please try again') } setIsWaitingListVisible(!!isInWaitingList) setIsAccessCode(!!isAccessCodeRequired) setAreTicketGroupsShown(areGroupsShown || false) if (responseTickets && !_isEmpty(responseTickets)) { if (config.areTicketsGrouped) { if (areGroupsShown) { setGroupedTickets(responseTickets as IGroupedTickets[]) } else { setTickets(responseTickets as ITicket[]) } } else { setTickets(responseTickets as ITicket[]) } if (isFirstCall && promoCodeResult?.isValid) { setPromoCodeResponse(promoCodeResult) } else if (!isFirstCall) { setPromoCodeResponse(promoCodeResult) } const onFetchTicketsData: IOnFetchTicketsSuccess = { promoCodeResponse: { success: true, message: promoCodeResponse?.message, }, tickets: responseTickets, isInWaitingList, isAccessCodeRequired, areTicketsGroupsShown: areGroupsShown, } onFetchTicketsSuccess?.(onFetchTicketsData) if ( !isFirstCall && (!promoCodeResult?.isValid || promoCodeResult.isValid === 0) ) { onFetchTicketsSuccess?.({ ...onFetchTicketsData, promoCodeResponse: { success: false, message: promoCodeResult?.message, }, }) } } setIsFirstCall(false) } const getEventData = async () => { logger.info('Fetching event data: API call') if (isApiErrorRef.current === true) { logger.info('Fetching event data: API call: Skip, error already occurred') return } setIsGettingEvent(true) const { eventError, eventData } = await getEventCore() logger.info('Fetching event data: API call: response', { eventError, eventData, }) setIsGettingEvent(false) if (eventError) { logger.info('Fetching event data: API call: Error', eventError) eventErrorCodeRef.current = eventError.code || 400 if (eventError.code === 401) { logger.info('Fetching event data: API call: Error: 401') setPasswordProtectedEventData({ isPasswordProtected: true, message: eventError.message, }) } else { logger.info('Fetching event data: API call: Error: ' + eventError.message) showAlert(eventError.message) } return onFetchEventError?.( eventError || { message: 'There was an error while fetching event' } ) } eventErrorCodeRef.current = 0 if (!eventData) { logger.info('Fetching event data: API call: Error: No event data') return onFetchEventError?.({ message: 'There was an error while fetching event', }) } try { logger.info('Fetching tickets: getTickets()') await getTickets() } catch (error) { logger.info('Fetching tickets: getTickets() Error: ' + error) showAlert(error) } onFetchEventSuccess?.(eventData) setEvent(eventData) } const performBookTickets = async () => { if (!selectedTicket || !selectedTicket.selectedOption) { onAddToCartError?.({ message: 'Please select a ticket' }) return showAlert('Please select a ticket') } const ticketQuantity = typeof selectedTicket.selectedOption.value === 'string' ? parseInt(selectedTicket.selectedOption.value, 10) : selectedTicket.selectedOption.value const ticketsOptions: IBookTicketsOptions = { quantity: ticketQuantity, optionName: _get(selectedTicket, 'optionName'), ticketId: _get(selectedTicket, 'id'), price: selectedTicket.price, } setIsBooking(true) const { data: result, error: addToCartError } = await addToCartCore(ticketsOptions) setIsBooking(false) if (result) { return onAddToCartSuccess?.(result) } if (addToCartError) { showAlert(addToCartError.message || 'Error while adding tickets to cart') return onAddToCartError?.(addToCartError) } } //#endregion Api Calls const onLoadingChangeCallback = useCallback( (loading: boolean) => { if (onLoadingChange) { onLoadingChange(loading) } }, [onLoadingChange] ) //#region UseEffects useEffect(() => { if (isGettingEvent || isGettingTickets || isBooking) { onLoadingChangeCallback(true) } else { onLoadingChangeCallback(false) } }, [isGettingTickets, isGettingEvent, isBooking, onLoadingChangeCallback]) useEffect(() => { if (!isCheckingCurrentSession && isFirstCall) { fetchInitialData() } }, [isCheckingCurrentSession]) //#endregion UseEffects //#region Imperative Handler useImperativeHandle(ref, () => ({ async refreshAccessToken(refreshToken: string): Promise { if (!sessionHandleRef.current) { return { accessTokenError: { message: 'Session Handle ref is not initialized', }, } } const { accessTokenError, accessTokenData } = await sessionHandleRef.current!.refreshAccessToken(refreshToken) if (!accessTokenError && accessTokenData?.accessToken) { await fetchInitialData() } return { accessTokenData, accessTokenError, } }, async reloadData() { await fetchInitialData() }, })) //#endregion Imperative Handler //#region Handlers const handleOnPressGetTickets = () => { if (eventErrorCodeRef.current !== 0) { showAlert('Event not found') return onAddToCartError?.({ code: eventErrorCodeRef.current, message: 'Event not found', }) } performBookTickets() } const handleOnPressApplyPromoCode = async (code: string) => { await getTickets(code) } const handleOnSelectTicketOption = (ticket: ISelectedTicket) => { setSelectedTicket(ticket) } const handleOnLogout = async () => { if (!ticketsCoreRef.current) { return { error: { message: 'Ticket core is not initialized' } } } await ticketsCoreRef.current.logout() setIsUserLogged(false) await getEventData() onPressLogout?.() } const handleOnLoadingChange = (loading: boolean) => { onLoadingChangeCallback(loading) } const handleOnSubmitEventPassword = async (password: string) => { setPasswordProtectedEventData({ ...passwordProtectedEventData, isLoading: true, }) const { eventData, eventError } = await unlockPasswordProtectedEventCore(password) if (eventError) { return setPasswordProtectedEventData({ ...passwordProtectedEventData, isLoading: false, apiError: eventError.message, }) } setPasswordProtectedEventData({ ...passwordProtectedEventData, isLoading: false, apiError: '', isPasswordProtected: false, message: '', }) eventErrorCodeRef.current = 0 onFetchEventSuccess?.(eventData!) setEvent(eventData) getTickets() } //#endregion //#region RENDER return ( ) //#endregion RENDER } ) export default Tickets