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
2 changes: 0 additions & 2 deletions frontend/src/components/common/CheckInStatusModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ export const CheckInStatusModal = ({
}: CheckInStatusModalProps) => {
const {data: checkInListsResponse, isLoading, ...rest} = useGetEventCheckInLists(eventId);

console.log('CheckInStatusModal - checkInListsResponse:', checkInListsResponse, 'isLoading:', isLoading, 'rest:', rest);

if (isLoading) {
return (
<Modal.Root opened={isOpen} onClose={onClose} size="md">
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/common/StatusToggle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {useUpdateEventStatus} from '../../../mutations/useUpdateEventStatus';
import {useUpdateOrganizerStatus} from '../../../mutations/useUpdateOrganizerStatus';
import {IdParam} from '../../../types';
import classes from './StatusToggle.module.scss';
import {trackEvent, AnalyticsEvents} from '../../../utilites/analytics';

interface StatusToggleProps {
entityType: 'event' | 'organizer';
Expand Down Expand Up @@ -47,6 +48,9 @@ export const StatusToggle: React.FC<StatusToggleProps> = ({

mutation.mutate(mutationParams as any, {
onSuccess: () => {
if (entityType === 'event' && newStatus === 'LIVE') {
trackEvent(AnalyticsEvents.EVENT_PUBLISHED);
}
const successMessage = entityType === 'event'
? t`Event status updated`
: t`Organizer status updated`;
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/forms/OrganizerForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {useFormErrorResponseHandler} from "../../../hooks/useFormErrorResponseHa
import {useGetMe} from "../../../queries/useGetMe.ts";
import {IconBuilding} from "@tabler/icons-react";
import classes from "../../routes/welcome/Welcome.module.scss";
import {trackEvent, AnalyticsEvents} from "../../../utilites/analytics.ts";

interface OrganizerFormProps {
onSuccess?: (organizer: Organizer) => void;
Expand Down Expand Up @@ -82,6 +83,7 @@ export const OrganizerCreateForm = ({onSuccess, onCancel}: OrganizerFormProps) =
organizerData: values,
}, {
onSuccess: ({data: organizer}) => {
trackEvent(AnalyticsEvents.ORGANIZER_CREATED);
if (onSuccess) {
onSuccess(organizer);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export const CreateWebhookModal = ({onClose}: GenericModalProps) => {
const createMutation = useCreateWebhook();

const handleSubmit = (requestData: WebhookRequest) => {
console.log(eventId, requestData);
createMutation.mutate({
eventId: eventId as IdParam,
webhook: requestData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Anchor, Button, Grid, Group, Text, ThemeIcon, Title} from "@mantine/core
import {Account, StripeConnectAccountsResponse} from "../../../../../../types.ts";
import paymentClasses from "./PaymentSettings.module.scss";
import classes from "../../ManageAccount.module.scss";
import {useEffect, useState} from "react";
import {useEffect, useRef, useState} from "react";
import {IconAlertCircle, IconBrandStripe, IconCheck, IconExternalLink, IconInfoCircle} from '@tabler/icons-react';
import {Card} from "../../../../../common/Card";
import {formatCurrency} from "../../../../../../utilites/currency.ts";
Expand All @@ -19,6 +19,7 @@ import {VatSettings} from './VatSettings';
import {VatSettingsModal} from './VatSettings/VatSettingsModal.tsx';
import {VatNotice, getVatInfo} from './VatNotice';
import {useGetAccountVatSetting} from '../../../../../../queries/useGetAccountVatSetting.ts';
import {trackEvent, AnalyticsEvents} from "../../../../../../utilites/analytics.ts";

interface FeePlanDisplayProps {
configuration?: {
Expand Down Expand Up @@ -725,6 +726,27 @@ const PaymentSettings = () => {

const [showVatModal, setShowVatModal] = useState(false);
const [hasCheckedVatModal, setHasCheckedVatModal] = useState(false);
const hasTrackedStripeConnection = useRef(false);

// Track Stripe connection when user returns with completed setup
useEffect(() => {
if (typeof window === 'undefined') return;
if (hasTrackedStripeConnection.current) return;
if (!stripeAccountsQuery.data) return;

const urlParams = new URLSearchParams(window.location.search);
const isReturn = urlParams.get('is_return') === '1';
if (!isReturn) return;

const completedAccount = stripeAccountsQuery.data.stripe_connect_accounts.find(
acc => acc.is_setup_complete
);

if (completedAccount) {
hasTrackedStripeConnection.current = true;
trackEvent(AnalyticsEvents.STRIPE_CONNECTED);
}
}, [stripeAccountsQuery.data]);

// Check if user is returning from Stripe and needs to fill VAT info
// Only for Hi.Events Cloud - open-source doesn't have VAT handling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {useEffect, useState} from 'react';
import {StripePlatform} from "../../../../types.ts";
import {isHiEvents} from "../../../../utilites/helpers.ts";
import {StripeConnectButton} from "../../../common/StripeConnectButton";
import {trackEvent, AnalyticsEvents} from "../../../../utilites/analytics.ts";

export const DashBoardSkeleton = () => {
return (
Expand Down Expand Up @@ -67,16 +68,20 @@ export const EventDashboard = () => {
};

const handleStatusToggle = () => {
const newStatus = event?.status === 'LIVE' ? 'DRAFT' : 'LIVE';
const message = event?.status === 'LIVE'
? t`Are you sure you want to make this event draft? This will make the event invisible to the public`
: t`Are you sure you want to make this event public? This will make the event visible to the public`;

confirmationDialog(message, () => {
statusToggleMutation.mutate({
eventId,
status: event?.status === 'LIVE' ? 'DRAFT' : 'LIVE'
status: newStatus
}, {
onSuccess: () => {
if (newStatus === 'LIVE') {
trackEvent(AnalyticsEvents.EVENT_PUBLISHED);
}
showSuccess(t`Event status updated`);
},
onError: (error: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {getProductsFromEvent} from "../../../../utilites/helpers.ts";
import {useEffect, useState} from 'react';
import ConfettiAnimation from "./ConfettiAnimaiton";
import {Browser, useBrowser} from "../../../../hooks/useGetBrowser.ts";
import {trackEvent, AnalyticsEvents} from "../../../../utilites/analytics.ts";

const GettingStarted = () => {
const {eventId} = useParams();
Expand Down Expand Up @@ -52,11 +53,15 @@ const GettingStarted = () => {
const statusToggleMutation = useUpdateEventStatus();

const handleStatusToggle = () => {
const newStatus = event?.status === 'LIVE' ? 'DRAFT' : 'LIVE';
statusToggleMutation.mutate({
eventId,
status: event?.status === 'LIVE' ? 'DRAFT' : 'LIVE'
status: newStatus
}, {
onSuccess: () => {
if (newStatus === 'LIVE') {
trackEvent(AnalyticsEvents.EVENT_PUBLISHED);
}
showSuccess(t`Event status updated`);
},
onError: (error: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {eventCheckoutPath, eventHomepagePath} from "../../../../utilites/urlHelp
import {showInfo} from "../../../../utilites/notifications.tsx";
import countries from "../../../../../data/countries.json";
import classes from "./CollectInformation.module.scss";
import {trackEvent, AnalyticsEvents} from "../../../../utilites/analytics.ts";

const LoadingSkeleton = () =>
(
Expand Down Expand Up @@ -210,6 +211,9 @@ export const CollectInformation = () => {

onSuccess: (data) => {
const nextPage = order?.is_payment_required ? 'payment' : 'summary';
if (nextPage === 'summary') {
trackEvent(AnalyticsEvents.PURCHASE_COMPLETED_FREE);
}
navigate(eventCheckoutPath(eventId, data.data.short_id, nextPage));
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {InlineOrderSummary} from "../../../common/InlineOrderSummary";
import {showError} from "../../../../utilites/notifications.tsx";
import {getConfig} from "../../../../utilites/config.ts";
import classes from "./Payment.module.scss";
import {trackEvent, AnalyticsEvents} from "../../../../utilites/analytics.ts";

const Payment = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -67,6 +68,8 @@ const Payment = () => {
orderShortId
}, {
onSuccess: () => {
const totalCents = Math.round((order?.total_gross || 0) * 100);
trackEvent(AnalyticsEvents.PURCHASE_COMPLETED_OFFLINE, { value: totalCents });
navigate(`/checkout/${eventId}/${orderShortId}/summary`);
},
onError: (error: any) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {usePollGetOrderPublic} from "../../../../queries/usePollGetOrderPublic.ts";
import {useNavigate, useParams} from "react-router";
import {useEffect, useState} from "react";
import {useEffect, useRef, useState} from "react";
import classes from './PaymentReturn.module.scss';
import {t} from "@lingui/macro";
import {useGetOrderStripePaymentIntentPublic} from "../../../../queries/useGetOrderStripePaymentIntentPublic.ts";
import {CheckoutContent} from "../../../layouts/Checkout/CheckoutContent";
import {eventCheckoutPath} from "../../../../utilites/urlHelper.ts";
import {HomepageInfoMessage} from "../../../common/HomepageInfoMessage";
import {isSsr} from "../../../../utilites/helpers.ts";
import {trackEvent, AnalyticsEvents} from "../../../../utilites/analytics.ts";

/**
* This component is responsible for handling the return from the payment provider.
Expand All @@ -24,6 +25,7 @@ export const PaymentReturn = () => {
const [attemptManualConfirmation, setAttemptManualConfirmation] = useState(false);
const paymentIntentQuery = useGetOrderStripePaymentIntentPublic(eventId, orderShortId, attemptManualConfirmation);
const [cannotConfirmPayment, setCannotConfirmPayment] = useState(false);
const hasTrackedPurchase = useRef(false);

useEffect(
() => {
Expand All @@ -44,6 +46,11 @@ export const PaymentReturn = () => {
return;
}
if (paymentIntentQuery.data?.status === 'succeeded') {
if (!hasTrackedPurchase.current && order) {
hasTrackedPurchase.current = true;
const totalCents = Math.round((order.total_gross || 0) * 100);
trackEvent(AnalyticsEvents.PURCHASE_COMPLETED_PAID, { value: totalCents });
}
navigate(eventCheckoutPath(eventId, orderShortId, 'summary'));
} else {
// At this point we've tried multiple times to confirm the payment and failed.
Expand All @@ -59,6 +66,11 @@ export const PaymentReturn = () => {
}

if (order?.status === 'COMPLETED') {
if (!hasTrackedPurchase.current) {
hasTrackedPurchase.current = true;
const totalCents = Math.round((order.total_gross || 0) * 100);
trackEvent(AnalyticsEvents.PURCHASE_COMPLETED_PAID, { value: totalCents });
}
navigate(eventCheckoutPath(eventId, orderShortId, 'summary'));
}
if (order?.payment_status === 'PAYMENT_FAILED' || (typeof window !== 'undefined' && window?.location.search.includes('failed'))) {
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/components/routes/welcome/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {useDebouncedValue, useMediaQuery} from "@mantine/hooks";
import {Event} from "../../../types.ts";
import {useCreateEvent} from "../../../mutations/useCreateEvent.ts";
import {NavLink, useNavigate} from "react-router";
import {useEffect, useState} from "react";
import {useEffect, useRef, useState} from "react";
import {useGetEvents} from "../../../queries/useGetEvents.ts";
import {LoadingContainer} from "../../common/LoadingContainer";
import {OrganizerCreateForm} from "../../forms/OrganizerForm";
Expand All @@ -21,6 +21,7 @@ import {DateTimePicker} from "@mantine/dates";
import dayjs from "dayjs";
import {EventCategories} from "../../../constants/eventCategories.ts";
import {getConfig} from "../../../utilites/config.ts";
import {trackEvent, AnalyticsEvents} from "../../../utilites/analytics.ts";

export const CreateOrganizer = ({progressInfo}: {
progressInfo?: { currentStep: number, totalSteps: number, progressPercentage: number }
Expand Down Expand Up @@ -92,6 +93,7 @@ const ConfirmVerificationPin = ({progressInfo}: {
code: values.pin,
}, {
onSuccess: () => {
trackEvent(AnalyticsEvents.SIGNUP_COMPLETED);
showSuccess(t`Email verified successfully!`);
form.reset();
setCompletedPin('');
Expand Down Expand Up @@ -254,6 +256,7 @@ export const CreateEvent = ({progressInfo}: {
eventData: submitData,
}, {
onSuccess: (values) => {
trackEvent(AnalyticsEvents.FIRST_EVENT_CREATED);
navigate(`/manage/event/${values.data.id}/getting-started?new_event=true`)
}
});
Expand Down Expand Up @@ -441,11 +444,24 @@ const Welcome = () => {
const organizersQuery = useGetOrganizers();
const organizers = organizersQuery?.data?.data;
const organizerExists = organizersQuery.isFetched && Number(organizers?.length) > 0;
const hasTrackedSignup = useRef(false);

const requiresVerification = userData
&& userData.enforce_email_confirmation_during_registration
&& !userData.is_email_verified;

useEffect(() => {
if (!userData || hasTrackedSignup.current) {
return;
}
// Only track if email verification was NEVER required for this account
// Users who needed verification are tracked in ConfirmVerificationPin's onSuccess
if (!userData.enforce_email_confirmation_during_registration) {
hasTrackedSignup.current = true;
trackEvent(AnalyticsEvents.SIGNUP_COMPLETED);
}
}, [userData]);

return (
<div className={classes.welcomeContainer}>
<Container size="sm" className={classes.welcomeContent}>
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/mutations/useCreateProductCategory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ export const useCreateProductCategory = () => {
}) => productCategoryClient.create(eventId, productCategoryData),

onSuccess: (_, variables) => {
console.log('Product category created successfully', {
eventId: variables.eventId,
productCategoryData: variables.productCategoryData,
});

return queryClient
.invalidateQueries({queryKey: [GET_EVENT_PRODUCT_CATEGORIES_QUERY_KEY, variables.eventId]});
}
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/utilites/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
declare global {
interface Window {
fathom?: {
trackEvent: (eventName: string, options?: { _value?: number }) => void;
trackPageview: () => void;
};
}
}

interface TrackEventOptions {
value?: number;
}

export const AnalyticsEvents = {
SIGNUP_COMPLETED: 'signup_completed',
ORGANIZER_CREATED: 'organizer_created',
FIRST_EVENT_CREATED: 'first_event_created',
EVENT_PUBLISHED: 'event_published',
STRIPE_CONNECTED: 'stripe_connected',
PURCHASE_COMPLETED_PAID: 'purchase_completed_paid',
PURCHASE_COMPLETED_OFFLINE: 'purchase_completed_offline',
PURCHASE_COMPLETED_FREE: 'purchase_completed_free',
} as const;

export type AnalyticsEventName = typeof AnalyticsEvents[keyof typeof AnalyticsEvents];

export function trackEvent(eventName: AnalyticsEventName | string, options?: TrackEventOptions): void {
if (typeof window === 'undefined') {
return;
}
// Fathom Analytics
if (window.fathom?.trackEvent) {
const fathomOptions = options?.value ? { _value: options.value } : undefined;
window.fathom.trackEvent(eventName, fathomOptions);
}

// Future: Google Analytics 4
// if (window.gtag) {
// window.gtag('event', eventName, {
// value: options?.value ? options.value / 100 : undefined,
// });
// }
}