diff --git a/apps/api/src/app/team/usecase/accept-invitation/accept-invitation.usecase.ts b/apps/api/src/app/team/usecase/accept-invitation/accept-invitation.usecase.ts index 2c44f543e..99bd881e4 100644 --- a/apps/api/src/app/team/usecase/accept-invitation/accept-invitation.usecase.ts +++ b/apps/api/src/app/team/usecase/accept-invitation/accept-invitation.usecase.ts @@ -1,17 +1,28 @@ import { Injectable } from '@nestjs/common'; -import { EmailService } from '@impler/services'; import { AuthService } from 'app/auth/services/auth.service'; +import { EmailService, PaymentAPIService } from '@impler/services'; import { EMAIL_SUBJECT, IJwtPayload, SCREENS, UserRolesEnum } from '@impler/shared'; -import { EnvironmentRepository, ProjectInvitationRepository, ProjectRepository } from '@impler/dal'; +import { + ProjectEntity, + ProjectRepository, + EnvironmentRepository, + ProjectInvitationEntity, + ProjectInvitationRepository, +} from '@impler/dal'; +import { LEAD_SIGNUP_USING } from '@shared/constants'; +import { LeadService } from '@shared/services/lead.service'; +import { captureException } from '@shared/helpers/common.helper'; @Injectable() export class AcceptInvitation { constructor( + private leadService: LeadService, private authService: AuthService, - private environmentRepository: EnvironmentRepository, - private projectInvitationRepository: ProjectInvitationRepository, + private emailService: EmailService, private projectRepository: ProjectRepository, - private emailService: EmailService + private paymentAPIService: PaymentAPIService, + private environmentRepository: EnvironmentRepository, + private projectInvitationRepository: ProjectInvitationRepository ) {} async exec({ invitationId, user }: { invitationId: string; user: IJwtPayload }) { @@ -19,13 +30,39 @@ export class AcceptInvitation { const environment = await this.environmentRepository.findOne({ _projectId: invitation._projectId, }); + const userProjects = await this.environmentRepository.count({ + 'apiKeys._userId': user._id, + }); + if (userProjects < 1) await this.registerUser(user); const project = await this.projectRepository.findOne({ _id: environment._projectId }); + await this.sendEmails(invitation, project); + await this.environmentRepository.addApiKey(environment._id, user._id, invitation.role); await this.projectInvitationRepository.delete({ _id: invitationId }); + const accessToken = this.authService.getSignedToken( + { + _id: user._id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + role: invitation.role as UserRolesEnum, + isEmailVerified: true, + profilePicture: user.profilePicture, + accessToken: environment.key, + }, + invitation._projectId + ); + + return { + accessToken, + screen: SCREENS.HOME, + }; + } + async sendEmails(invitation: ProjectInvitationEntity, project: ProjectEntity) { const emailContentsSender = this.emailService.getEmailContent({ type: 'ACCEPT_PROJECT_INVITATION_RECIEVER_EMAIL', data: { @@ -58,24 +95,26 @@ export class AcceptInvitation { from: process.env.EMAIL_FROM, senderName: process.env.EMAIL_FROM_NAME, }); - - const accessToken = this.authService.getSignedToken( - { - _id: user._id, - firstName: user.firstName, - lastName: user.lastName, + } + async registerUser(user: IJwtPayload) { + try { + const userData = { + name: user.firstName + ' ' + user.lastName, email: user.email, - role: invitation.role as UserRolesEnum, - isEmailVerified: true, - profilePicture: user.profilePicture, - accessToken: environment.key, - }, - invitation._projectId - ); - - return { - accessToken, - screen: SCREENS.HOME, - }; + externalId: user.email, + }; + await this.paymentAPIService.createUser(userData); + await this.leadService.createLead({ + 'First Name': user.firstName, + 'Last Name': user.lastName, + 'Lead Email': user.email, + 'Lead Source': 'Invitation', + 'Mentioned Role': user.role, + 'Signup Method': LEAD_SIGNUP_USING.EMAIL, + 'Company Size': user.companySize, + }); + } catch (error) { + captureException(error); + } } } diff --git a/apps/api/src/app/team/usecase/index.ts b/apps/api/src/app/team/usecase/index.ts index be3cf9900..b7ebb3e0d 100644 --- a/apps/api/src/app/team/usecase/index.ts +++ b/apps/api/src/app/team/usecase/index.ts @@ -1,15 +1,16 @@ import { Invite } from './invite/invite.usecase'; -import { SentInvitations } from './sent-invitation/sent-invitation.usecase'; -import { GetInvitation } from './get-invitation/get-invitation.usecase'; -import { AcceptInvitation } from './accept-invitation/accept-invitation.usecase'; import { GenerateUniqueApiKey } from 'app/environment/usecases'; +import { GetInvitation } from './get-invitation/get-invitation.usecase'; +import { SentInvitations } from './sent-invitation/sent-invitation.usecase'; +import { TeamMemberMeta } from './team-member-meta/team-member-meta.usecase'; import { ListTeamMembers } from './list-team-members/list-team-members.usecase'; -import { UpdateTeamMember } from './update-team-member-role/update-team-member.usecase'; -import { RemoveTeamMember } from './delete-team-member/delete-team-member.usecase'; import { RevokeInvitation } from './revoke-invitation/revoke-invitation.usecase'; +import { AcceptInvitation } from './accept-invitation/accept-invitation.usecase'; +import { RemoveTeamMember } from './delete-team-member/delete-team-member.usecase'; import { DeclineInvitation } from './decline-invitation/decline-invitation.usecase'; -import { TeamMemberMeta } from './team-member-meta/team-member-meta.usecase'; +import { UpdateTeamMember } from './update-team-member-role/update-team-member.usecase'; import { PaymentAPIService } from '@impler/services'; +import { LeadService } from '@shared/services/lead.service'; export const USE_CASES = [ Invite, @@ -23,6 +24,7 @@ export const USE_CASES = [ RevokeInvitation, DeclineInvitation, TeamMemberMeta, + LeadService, PaymentAPIService, // ]; diff --git a/apps/web/components/home/PlanDetails/PlanDetails.tsx b/apps/web/components/home/PlanDetails/PlanDetails.tsx index 65c427343..2b5c462bb 100644 --- a/apps/web/components/home/PlanDetails/PlanDetails.tsx +++ b/apps/web/components/home/PlanDetails/PlanDetails.tsx @@ -4,12 +4,6 @@ import { modals } from '@mantine/modals'; import { useCallback, useContext, useEffect } from 'react'; import { Title, Text, Flex, Button, Skeleton, Stack } from '@mantine/core'; -import { track } from '@libs/amplitude'; -import { numberFormatter } from '@impler/shared'; -import { SelectCardModal } from '@components/settings'; -import { usePlanDetails } from '@hooks/usePlanDetails'; -import { TooltipLink } from '@components/guide-point'; -import { PlansModal } from '@components/UpgradePlan/PlansModal'; import { CONSTANTS, MODAL_KEYS, @@ -20,8 +14,16 @@ import { SubjectsEnum, AppAbility, } from '@config'; -import { AbilityContext, Can } from 'store/ability.context'; +import { Alert } from '@ui/Alert'; +import { track } from '@libs/amplitude'; +import { numberFormatter } from '@impler/shared'; +import { TooltipLink } from '@components/guide-point'; +import { SelectCardModal } from '@components/settings'; +import { usePlanDetails } from '@hooks/usePlanDetails'; +import { PlansModal } from '@components/UpgradePlan/PlansModal'; import { useAppState } from 'store/app.context'; +import { AbilityContext, Can } from 'store/ability.context'; +import { InformationIcon } from '@assets/icons/Information.icon'; export function PlanDetails() { const router = useRouter(); @@ -118,74 +120,79 @@ export function PlanDetails() { isLessThanZero || activePlanDetails!.usage.IMPORTED_ROWS > numberOfRecords ? colors.danger : colors.yellow; return ( - numberOfRecords ? colors.danger : colors.yellow - }`, - backgroundColor: backgroundColor + '20', - }} - > - - - - {numberFormatter(activePlanDetails!.usage.IMPORTED_ROWS)} - {'/'} - {numberFormatter(numberOfRecords)} - - - Records Imported - - - - - {activePlanDetails.plan.name} - - - Active Plan - - - {Number(activePlanDetails.plan.charge) ? ( - <> + <> + } p="xs"> + You're viewing details of {profileInfo?.projectName} project + + numberOfRecords ? colors.danger : colors.yellow + }`, + backgroundColor: backgroundColor + '20', + }} + > + + + + {numberFormatter(activePlanDetails!.usage.IMPORTED_ROWS)} + {'/'} + {numberFormatter(numberOfRecords)} + + + Records Imported + + + + + {activePlanDetails.plan.name} + + + Active Plan + + + {Number(activePlanDetails.plan.charge) ? ( + <> + + + {'$' + activePlanDetails.plan.charge} + + + Outstanding Amount + + + + ) : null} + + + <>{activePlanDetails!.expiryDate}</> + + + Expiry Date + + + + - - {'$' + activePlanDetails.plan.charge} - - - Outstanding Amount + + + View all transactions - - ) : null} - - - <>{activePlanDetails!.expiryDate}</> - - - Expiry Date - + - - - - - View all transactions - - - + - - - + ); } diff --git a/apps/web/components/settings/AddCard/PaymentMethods/PaymentMethods.tsx b/apps/web/components/settings/AddCard/PaymentMethods/PaymentMethods.tsx index 9082c464e..904a8b437 100644 --- a/apps/web/components/settings/AddCard/PaymentMethods/PaymentMethods.tsx +++ b/apps/web/components/settings/AddCard/PaymentMethods/PaymentMethods.tsx @@ -8,6 +8,7 @@ import { MODAL_KEYS, ROUTES } from '@config'; import { PaymentMethodOption } from './PaymentMethodOption'; interface PaymentMethodsProps { + isAddCardDisabled?: boolean; paymentMethods: ICardData[] | undefined; selectedPaymentMethod: string | undefined; handlePaymentMethodChange: (methodId: string) => void; @@ -15,6 +16,7 @@ interface PaymentMethodsProps { export function PaymentMethods({ paymentMethods, + isAddCardDisabled, selectedPaymentMethod, handlePaymentMethodChange, }: PaymentMethodsProps) { @@ -44,7 +46,7 @@ export function PaymentMethods({ ))} - diff --git a/apps/web/components/settings/AddCard/SelectCardModal.tsx b/apps/web/components/settings/AddCard/SelectCardModal.tsx index 47f0acb15..1775f7ad8 100644 --- a/apps/web/components/settings/AddCard/SelectCardModal.tsx +++ b/apps/web/components/settings/AddCard/SelectCardModal.tsx @@ -16,15 +16,16 @@ interface SelectCardModalProps { export function SelectCardModal({ email, planCode, paymentMethodId }: SelectCardModalProps) { const { + handleProceed, + paymentMethods, appliedCouponCode, + isPurchaseLoading, setAppliedCouponCode, - handlePaymentMethodChange, - handleProceed, + selectedPaymentMethod, isCouponFeatureEnabled, isPaymentMethodsFetching, isPaymentMethodsLoading, - selectedPaymentMethod, - paymentMethods, + handlePaymentMethodChange, } = useSubscribe({ email, planCode, @@ -52,6 +53,7 @@ export function SelectCardModal({ email, planCode, paymentMethodId }: SelectCard @@ -63,8 +65,8 @@ export function SelectCardModal({ email, planCode, paymentMethodId }: SelectCard - diff --git a/apps/web/config/constants.config.ts b/apps/web/config/constants.config.ts index 2eee1a043..c3229ef26 100644 --- a/apps/web/config/constants.config.ts +++ b/apps/web/config/constants.config.ts @@ -192,6 +192,7 @@ export const NOTIFICATION_KEYS = { PROJECT_CREATED: 'PROJECT_CREATED', PROJECT_DELETED: 'PROJECT_DELETED', + PROJECT_SWITCHED: 'PROJECT_SWITCHED', OUTPUT_UPDATED: 'OUTPUT_UPDATED', DESTINATION_UPDATED: 'DESTINATION_UPDATED', @@ -415,6 +416,7 @@ export const HOW_HEARD_ABOUT_US = [ { value: 'Bubble.io', label: 'Bubble.io' }, { value: 'Colleague', label: 'Colleague' }, { value: 'Linkdin', label: 'Linkdin' }, + { value: 'Invitation', label: 'Invitation' }, ]; export const PLACEHOLDERS = { diff --git a/apps/web/design-system/Alert/Alert.styles.tsx b/apps/web/design-system/Alert/Alert.styles.tsx index eeb477a01..fc8dcb580 100644 --- a/apps/web/design-system/Alert/Alert.styles.tsx +++ b/apps/web/design-system/Alert/Alert.styles.tsx @@ -1,11 +1,12 @@ -import { createStyles } from '@mantine/core'; +import { createStyles, MantineTheme } from '@mantine/core'; -const getWrapperStyles = (): React.CSSProperties => ({ - alignItems: 'center', -}); - -export default createStyles((): Record => { +export default createStyles((theme: MantineTheme): Record => { return { - wrapper: getWrapperStyles(), + wrapper: { + alignItems: 'center', + }, + icon: { + marginRight: theme.spacing.xs, + }, }; }); diff --git a/apps/web/hooks/usePlanDetails.tsx b/apps/web/hooks/usePlanDetails.tsx index 871758416..5232b95e4 100644 --- a/apps/web/hooks/usePlanDetails.tsx +++ b/apps/web/hooks/usePlanDetails.tsx @@ -10,12 +10,11 @@ interface UsePlanDetailProps { export function usePlanDetails({ projectId }: UsePlanDetailProps) { const { meta, setPlanMeta } = usePlanMetaData(); - const { data: activePlanDetails, isLoading: isActivePlanLoading } = useQuery< - unknown, - IErrorObject, - ISubscriptionData, - [string, string] - >( + const { + data: activePlanDetails, + isLoading: isActivePlanLoading, + refetch: refetchActivePlanDetails, + } = useQuery( [API_KEYS.FETCH_ACTIVE_SUBSCRIPTION, projectId], () => commonApi(API_KEYS.FETCH_ACTIVE_SUBSCRIPTION as any, { @@ -42,5 +41,6 @@ export function usePlanDetails({ projectId }: UsePlanDetailProps) { meta, activePlanDetails, isActivePlanLoading, + refetchActivePlanDetails, }; } diff --git a/apps/web/hooks/useProject.tsx b/apps/web/hooks/useProject.tsx index 7b72f9d65..bfa83fb10 100644 --- a/apps/web/hooks/useProject.tsx +++ b/apps/web/hooks/useProject.tsx @@ -122,8 +122,20 @@ export function useProject() { ); const onProjectIdChange = (id: string) => { - switchProject(id); - track({ name: 'PROJECT SWITCH', properties: {} }); + const project = projects?.find((item) => item._id === id); + if (project) { + switchProject(id); + track({ name: 'PROJECT SWITCH', properties: {} }); + notify(NOTIFICATION_KEYS.PROJECT_SWITCHED, { + title: 'Project switched', + message: ( + <> + You're switched to {project.name} project. + + ), + color: 'green', + }); + } }; const sortedProjects = projects ? projects.sort((a) => (a._id === profileData?._projectId ? -1 : 1)) : []; diff --git a/apps/web/hooks/useSubscribe.tsx b/apps/web/hooks/useSubscribe.tsx index e70e2d73d..ec4de4661 100644 --- a/apps/web/hooks/useSubscribe.tsx +++ b/apps/web/hooks/useSubscribe.tsx @@ -1,13 +1,15 @@ import { useState } from 'react'; import getConfig from 'next/config'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; +import { modals } from '@mantine/modals'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + import { notify } from '@libs/notify'; import { commonApi } from '@libs/api'; -import { API_KEYS, CONSTANTS, NOTIFICATION_KEYS, ROUTES } from '@config'; -import { modals } from '@mantine/modals'; import { ICardData, IErrorObject } from '@impler/shared'; import { ConfirmationModal } from '@components/ConfirmationModal'; +import { API_KEYS, CONSTANTS, NOTIFICATION_KEYS, ROUTES } from '@config'; +import { useAppState } from 'store/app.context'; const { publicRuntimeConfig } = getConfig(); @@ -24,11 +26,12 @@ interface ISubscribeResponse { } export const useSubscribe = ({ email, planCode, paymentMethodId }: UseSubscribeProps) => { - const queryClient = useQueryClient(); const router = useRouter(); + const queryClient = useQueryClient(); + const { profileInfo } = useAppState(); const isCouponFeatureEnabled = publicRuntimeConfig.NEXT_PUBLIC_COUPON_ENABLED; - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(paymentMethodId); const [appliedCouponCode, setAppliedCouponCode] = useState(undefined); + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(paymentMethodId); const { data: paymentMethods, @@ -65,7 +68,8 @@ export const useSubscribe = ({ email, planCode, paymentMethodId }: UseSubscribeP }), { onSuccess: (response) => { - queryClient.invalidateQueries([API_KEYS.FETCH_ACTIVE_SUBSCRIPTION, email]); + queryClient.invalidateQueries([API_KEYS.FETCH_ACTIVE_SUBSCRIPTION, profileInfo?._projectId]); + modals.closeAll(); if (response && response.status) { modals.open({ title: @@ -77,12 +81,12 @@ export const useSubscribe = ({ email, planCode, paymentMethodId }: UseSubscribeP } }, onError: (error: IErrorObject) => { + modals.closeAll(); notify(NOTIFICATION_KEYS.PURCHASE_FAILED, { title: 'Purchase Failed', message: error.message, color: 'red', }); - queryClient.invalidateQueries([API_KEYS.FETCH_ACTIVE_SUBSCRIPTION, email]); if (error && error.statusCode) { modals.open({ title: CONSTANTS.SUBSCRIPTION_FAILED_TITLE, @@ -94,7 +98,6 @@ export const useSubscribe = ({ email, planCode, paymentMethodId }: UseSubscribeP ); const handleProceed = () => { - modals.closeAll(); if (selectedPaymentMethod) { subscribe({ email,