diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx index e00be614..6009011b 100644 --- a/src/components/avatar/avatar.tsx +++ b/src/components/avatar/avatar.tsx @@ -2,6 +2,7 @@ import Avatar from '@mui/material/Avatar'; import {FC} from "react"; import {dicebear} from "@src/utils/dicebear.ts"; +import {COLORS} from "@src/layouts/config-layout.ts"; interface AvatarProfileProps { src: string; @@ -11,12 +12,19 @@ interface AvatarProfileProps { } const AvatarProfile: FC = ({ src, alt, sx, ...other }) => { - //Check if src is a valid URL starting with http or https; if not, use the dicebear API to generate a random avatar - if (!src.startsWith('http') && !src.startsWith('https')) { src = dicebear(src) } + const imageSrc = src.startsWith('http') || src.startsWith('https') ? src : dicebear(src); + + // Default styles for the Avatar component + sx = { + backgroundColor: COLORS.GRAY_DARK, + fontWeight: 'bold', + ...sx, + }; + return ( - + ) } diff --git a/src/components/user-item/BadgeVerified.tsx b/src/components/user-item/BadgeVerified.tsx index e4e5d16b..226a0b58 100644 --- a/src/components/user-item/BadgeVerified.tsx +++ b/src/components/user-item/BadgeVerified.tsx @@ -17,7 +17,7 @@ const BadgeVerified: FC = ({ address }) => { // If the user is not verified, do not render the badge if (!isVerified) return null; - return ; + return ; }; export default BadgeVerified; diff --git a/src/config-global.ts b/src/config-global.ts index 90e46998..4595bf4e 100644 --- a/src/config-global.ts +++ b/src/config-global.ts @@ -31,7 +31,7 @@ export const GLOBAL_CONSTANTS = { process.env.VITE_ACCESS_MANAGER_ADDRESS || import.meta.env.VITE_ACCESS_MANAGER_ADDRESS || '', SENTRY_AUTH_TOKEN: process.env.VITE_SENTRY_AUTH_TOKEN || import.meta.env.VITE_SENTRY_AUTH_TOKEN || '', - SENTRY_DNS: process.env.VITE_SENTRY_DNS || import.meta.env.VITE_SENTRY_DNS || '', + SENTRY_DSN: process.env.VITE_SENTRY_DSN || import.meta.env.VITE_SENTRY_DSN || '', PINATA_API_KEY: process.env.VITE_PINATA_API_KEY || import.meta.env.VITE_PINATA_API_KEY || '', PINATA_SECRET_API_KEY: process.env.VITE_PINATA_SECRET_API_KEY || import.meta.env.VITE_PINATA_SECRET_API_KEY || '', diff --git a/src/hooks/use-referrals.ts b/src/hooks/use-referrals.ts index ff1dd70a..5ef98de5 100644 --- a/src/hooks/use-referrals.ts +++ b/src/hooks/use-referrals.ts @@ -1,7 +1,9 @@ import { useState } from 'react'; +import { useSelector } from 'react-redux'; import emailjs from '@emailjs/browser'; + import { GLOBAL_CONSTANTS } from '@src/config-global'; -import { useSelector } from 'react-redux'; +import { Invitation } from '@src/types/invitation'; // <-- Importing from separate types file import { fetchInvitations as fetchInvitationsAction, checkIfMyEmailHasPendingInvite as checkIfMyEmailHasPendingInviteAction, @@ -9,20 +11,12 @@ import { checkIfInvitationSent as checkIfInvitationSentAction, checkIfEmailAlreadyAccepted as checkIfEmailAlreadyAcceptedAction, sendInvitation as sendInvitationAction, - acceptOrCreateInvitationForUser as acceptOrCreateInvitationForUserAction + acceptOrCreateInvitationForUser as acceptOrCreateInvitationForUserAction, } from '@src/utils/supabase-actions'; -export interface Invitation { - id: string; - status: 'pending' | 'accepted' | 'rejected'; - sender_email: string; - destination: string; - sender_id: string; - receiver_id: string | null; - payload: any; - created_at: string; -} - +/** + * The type for sending emails through EmailJS. + */ export type EmailParams = { to_email: string; from_name: string; @@ -94,7 +88,10 @@ const useReferrals = () => { setLoading(true); setError(null); - const { data, error } = await acceptInvitationAction(invitationId, sessionData?.profile?.id); + const { data, error } = await acceptInvitationAction( + invitationId, + sessionData?.profile?.id + ); if (error) { setError(error); @@ -102,6 +99,7 @@ const useReferrals = () => { return null; } + // Update local state to reflect the accepted status setInvitations((prev) => prev.map((inv) => inv.id === invitationId @@ -168,6 +166,7 @@ const useReferrals = () => { * @returns {Promise} - Throws an error if something goes wrong. */ const sendInvitation = async (destination: string, payload: any): Promise => { + // Insert a new invitation into Supabase const { error } = await sendInvitationAction(destination, payload, userEmail, sessionData); if (error) { @@ -176,7 +175,7 @@ const useReferrals = () => { } else { console.log('Invitation stored successfully in Supabase.'); - // Send the email using EmailJS + // Send the email via EmailJS await sendEmail({ to_email: destination, from_name: payload?.data?.from?.displayName ?? 'Watchit Web3xAI', @@ -201,13 +200,17 @@ const useReferrals = () => { } }; + /** + * Send an email with EmailJS. + */ const sendEmail = async (data: EmailParams) => { const { from_name, to_email } = data; + // Set the template parameters for EmailJS const templateParams = { to_email, from_name, - from_email: GLOBAL_CONSTANTS.SENDER_EMAIL, + from_email: GLOBAL_CONSTANTS.SENDER_EMAIL, // <-- Enforcing the global from_email }; try { @@ -233,17 +236,15 @@ const useReferrals = () => { loading, error, - // Fetch/CRUD Methods + // CRUD/Fetch Methods fetchInvitations, - sendInvitation, - sendEmail, - - // Additional Validation/Check Methods checkIfMyEmailHasPendingInvite, acceptInvitation, checkIfInvitationSent, checkIfEmailAlreadyAccepted, - acceptOrCreateInvitationForUser + sendInvitation, + acceptOrCreateInvitationForUser, + sendEmail, }; }; diff --git a/src/index.tsx b/src/index.tsx index 1f35240d..ac5d1f00 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,10 +19,11 @@ const isDevelopment = GLOBAL_CONSTANTS.ENVIRONMENT === 'development'; Sentry.init({ environment: GLOBAL_CONSTANTS.ENVIRONMENT, enabled: !isDevelopment, - dsn: GLOBAL_CONSTANTS.SENTRY_DNS, + dsn: GLOBAL_CONSTANTS.SENTRY_DSN, integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()], // Tracing tracesSampleRate: 1.0, // Capture 100% of the transactions + tracePropagationTargets: ["localhost", /^https:\/\/app.watchit\.movie/], // Session Replay replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. diff --git a/src/layouts/_common/account-popover.tsx b/src/layouts/_common/account-popover.tsx index 1568b721..2bee9b61 100644 --- a/src/layouts/_common/account-popover.tsx +++ b/src/layouts/_common/account-popover.tsx @@ -198,6 +198,7 @@ export default function AccountPopover() { } alt="avatar" sx={{ + fontSize: '1.25rem', width: 36, height: 36, border: (theme: any) => `solid 2px ${theme.palette.background.default}`, diff --git a/src/sections/finance/components/finance-invite-friends.tsx b/src/sections/finance/components/finance-invite-friends.tsx index 6473b0f7..e835cc5d 100644 --- a/src/sections/finance/components/finance-invite-friends.tsx +++ b/src/sections/finance/components/finance-invite-friends.tsx @@ -19,6 +19,7 @@ import { ERRORS } from '@notifications/errors.ts'; import useReferrals from "@src/hooks/use-referrals"; import LoadingButton from '@mui/lab/LoadingButton'; +import {checkIfEmailAlreadyInvited} from "@src/utils/supabase-actions.ts"; interface Props extends BoxProps { img?: string; @@ -42,6 +43,7 @@ export default function FinanceInviteFriends({ } = useReferrals(); const theme = useTheme(); const sessionData = useSelector((state: any) => state.auth.session); + const userLoggedEmail = useSelector((state: any) => state.auth.email); const [email, setEmail] = useState(''); const [loading, setLoading] = useState(false); @@ -49,10 +51,17 @@ export default function FinanceInviteFriends({ setEmail(event.target.value); }; - const handleInviteClick = async () => { - // Basic email format check + /* + * Return true if the email is valid, false otherwise. + * */ + const handleValidEmail = () => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { + return emailRegex.test(email); + } + + const handleInviteClick = async () => { + + if (!handleValidEmail()) { notifyError(ERRORS.INVITATION_EMAIL_ERROR); return; } @@ -61,6 +70,23 @@ export default function FinanceInviteFriends({ // Check if there's an existing invitation from the current user to this email const alreadySent = await checkIfInvitationSent(email); + + // Check if the user has already been invited but someone else + const { invited } = await checkIfEmailAlreadyInvited(email); + + if (invited) { + notifyError(ERRORS.INVITATION_USER_ALREADY_INVITED); + setLoading(false); + return; + } + + // Check if the email entered is the same as the logged user's email + if (email === userLoggedEmail) { + notifyError(ERRORS.INVITATION_USER_CANT_INVITE_SELF); + setLoading(false); + return; + } + if (alreadySent) { // You can adapt the notification message to match your requirements notifyError(ERRORS.ALREADY_SENT_INVITATION); @@ -165,6 +191,7 @@ export default function FinanceInviteFriends({ onChange={handleInputChange} endAdornment={ = { [ERRORS.ALREADY_SENT_INVITATION]: 'You have already sent an invitation to this email address!', [ERRORS.ALREADY_ENROLLED]: 'This user is already enrolled!', [ERRORS.INVITATION_SEND_ERROR]: 'An error occurred while sending the invitation.', + [ERRORS.INVITATION_USER_ALREADY_INVITED]: 'This user has already been invited!', + [ERRORS.INVITATION_USER_CANT_INVITE_SELF]: 'You cannot invite yourself!', }; diff --git a/src/utils/supabase-actions.ts b/src/utils/supabase-actions.ts index 9ee39692..44028d54 100644 --- a/src/utils/supabase-actions.ts +++ b/src/utils/supabase-actions.ts @@ -1,7 +1,12 @@ import { supabase } from '@src/utils/supabase'; -import { Invitation } from '@src/hooks/use-referrals'; - -export const fetchInvitations = async (senderId: string): Promise<{ data: Invitation[] | null, error: string | null }> => { +import { Invitation } from '@src/types/invitation'; + +/** + * Fetches all invitations from Supabase filtered by senderId. + */ +export const fetchInvitations = async ( + senderId: string +): Promise<{ data: Invitation[] | null; error: string | null }> => { try { const { data, error } = await supabase .from('invitations') @@ -14,7 +19,12 @@ export const fetchInvitations = async (senderId: string): Promise<{ data: Invita } }; -export const checkIfMyEmailHasPendingInvite = async (userEmail: string): Promise<{ hasPending: boolean, error: string | null }> => { +/** + * Checks whether an email has a pending invitation. + */ +export const checkIfMyEmailHasPendingInvite = async ( + userEmail: string +): Promise<{ hasPending: boolean; error: string | null }> => { try { const { data, error } = await supabase .from('invitations') @@ -22,13 +32,23 @@ export const checkIfMyEmailHasPendingInvite = async (userEmail: string): Promise .eq('destination', userEmail) .eq('status', 'pending'); - return { hasPending: data && data.length > 0, error: error ? error.message : null }; + return { + hasPending: !!data && data.length > 0, + error: error ? error.message : null, + }; } catch (err: any) { return { hasPending: false, error: err.message }; } }; -export const acceptInvitation = async (invitationId: string, receiverId: string | null): Promise<{ data: Invitation | null, error: string | null }> => { +/** + * Accepts an existing invitation by setting its status to 'accepted' + * and assigning receiver_id to the current user's profile ID. + */ +export const acceptInvitation = async ( + invitationId: string, + receiverId: string | null +): Promise<{ data: Invitation | null; error: string | null }> => { try { const { data, error } = await supabase .from('invitations') @@ -45,7 +65,14 @@ export const acceptInvitation = async (invitationId: string, receiverId: string } }; -export const checkIfInvitationSent = async (userEmail: string, destinationEmail: string): Promise<{ exists: boolean, error: string | null }> => { +/** + * Checks whether the current user (userEmail) already sent an invitation + * to destinationEmail. + */ +export const checkIfInvitationSent = async ( + userEmail: string, + destinationEmail: string +): Promise<{ exists: boolean; error: string | null }> => { try { const { data, error } = await supabase .from('invitations') @@ -53,13 +80,21 @@ export const checkIfInvitationSent = async (userEmail: string, destinationEmail: .eq('sender_email', userEmail) .eq('destination', destinationEmail); - return { exists: data && data.length > 0, error: error ? error.message : null }; + return { + exists: !!data && data.length > 0, + error: error ? error.message : null, + }; } catch (err: any) { return { exists: false, error: err.message }; } }; -export const checkIfEmailAlreadyAccepted = async (destinationEmail: string): Promise<{ accepted: boolean, error: string | null }> => { +/** + * Checks if the specified email already has an accepted invitation. + */ +export const checkIfEmailAlreadyAccepted = async ( + destinationEmail: string +): Promise<{ accepted: boolean; error: string | null }> => { try { const { data, error } = await supabase .from('invitations') @@ -67,13 +102,24 @@ export const checkIfEmailAlreadyAccepted = async (destinationEmail: string): Pro .eq('destination', destinationEmail) .eq('status', 'accepted'); - return { accepted: data && data.length > 0, error: error ? error.message : null }; + return { + accepted: !!data && data.length > 0, + error: error ? error.message : null, + }; } catch (err: any) { return { accepted: false, error: err.message }; } }; -export const sendInvitation = async (destination: string, payload: any, userEmail: string, sessionData: any): Promise<{ error: string | null }> => { +/** + * Sends (inserts) a new invitation in Supabase. + */ +export const sendInvitation = async ( + destination: string, + payload: any, + userEmail: string, + sessionData: any +): Promise<{ error: string | null }> => { const { error } = await supabase .from('invitations') .insert([ @@ -83,13 +129,20 @@ export const sendInvitation = async (destination: string, payload: any, userEmai payload, sender_email: userEmail, sender_address: sessionData?.address, - } + }, ]); return { error: error ? error.message : null }; }; -export const acceptOrCreateInvitationForUser = async (userEmail: string, sessionData: any): Promise<{ error: string | null }> => { +/** + * Accepts the first invitation found for userEmail, + * or creates a new invitation with status = 'accepted' if none exist. + */ +export const acceptOrCreateInvitationForUser = async ( + userEmail: string, + sessionData: any +): Promise<{ error: string | null }> => { try { const { data: invites, error: pendingError } = await supabase .from('invitations') @@ -101,6 +154,10 @@ export const acceptOrCreateInvitationForUser = async (userEmail: string, session throw new Error(`Error fetching pending invites: ${pendingError.message}`); } + console.log('acceptOrCreateInvitationForUser invites') + console.log(invites) + + // If we already have at least one invitation, accept the first. if (invites && invites.length > 0) { const invitationId = invites[0].id; const { error } = await acceptInvitation(invitationId, sessionData?.profile?.id); @@ -108,6 +165,7 @@ export const acceptOrCreateInvitationForUser = async (userEmail: string, session throw new Error(error); } } else { + // Otherwise, create a new invitation with status='accepted' const { error: createError } = await supabase .from('invitations') .insert([ @@ -118,14 +176,16 @@ export const acceptOrCreateInvitationForUser = async (userEmail: string, session receiver_id: sessionData?.profile?.id ?? null, sender_email: userEmail, payload: { - self_register: true + self_register: true, }, status: 'accepted', }, ]); if (createError) { - throw new Error(`Error creating 'accepted' invitation: ${createError.message}`); + throw new Error( + `Error creating 'accepted' invitation: ${createError.message}` + ); } } @@ -134,3 +194,27 @@ export const acceptOrCreateInvitationForUser = async (userEmail: string, session return { error: err.message }; } }; + + +/* +* Verify if the email has already been invited. +* Find in supabase if the email has already been invited, taking the destination email as a parameter. +* */ + +export const checkIfEmailAlreadyInvited = async ( + destinationEmail: string +): Promise<{ invited: boolean; error: string | null }> => { + try { + const { data, error } = await supabase + .from('invitations') + .select('id') + .eq('destination', destinationEmail); + + return { + invited: !!data && data.length > 0, + error: error ? error.message : null, + }; + } catch (err: any) { + return { invited: false, error: err.message }; + } +};