diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 7cba3e4b788c..d284e9e393df 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -2576,9 +2576,34 @@ export const REQUEST_NEW_INTEGRATIONS = { REQUEST_MODAL_EMAIL: { LABEL: () => "Email", DESCRIPTION: () => - "Appsmith might use this email exclusively to follow up on your integration request.", + "Appsmith will use this email exclusively to follow up on your integration request.", NAME: "email", ERROR: () => "Please enter email", }, SUCCESS_TOAST_MESSAGE: () => "Thank you! We are looking into your request.", }; + +export const PREMIUM_DATASOURCES = { + RELEVANT_EMAIL_DESCRIPTION: () => + "Unblock advanced integrations. Let our team guide you in selecting the plan that fits your needs. Schedule a call now to see how Appsmith can transform your workflows!", + NON_RELEVANT_EMAIL_DESCRIPTION: () => + "Unblock advanced integrations. Let our team guide you in selecting the plan that fits your needs. Give us your email and the Appsmith team will reach out to you soon.", + LEARN_MORE: () => "Learn more about Premium", + SCHEDULE_CALL: () => "Schedule a call", + SUBMIT: () => "Submit", + SUCCESS_TOAST_MESSAGE: () => + "Thank you! The Appsmith Team will contact you shortly.", + FORM_EMAIL: { + LABEL: () => "Email", + DESCRIPTION: () => + "Appsmith will use this email to follow up on your integration interest.", + NAME: "email", + ERROR: () => "Please enter email", + }, + PREMIUM_TAG: () => "Premium", + SOON_TAG: () => "Soon", + COMING_SOON_SUFFIX: () => "Coming soon", + COMING_SOON_DESCRIPTION: () => + "The Appsmith Team is actively working on it. We’ll let you know when this integration is live. ", + NOTIFY_ME: () => "Notify me", +}; diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts index de41ab80134d..eb124bcebf55 100644 --- a/app/client/src/ce/entities/FeatureFlag.ts +++ b/app/client/src/ce/entities/FeatureFlag.ts @@ -48,6 +48,7 @@ export const FEATURE_FLAG = { "release_table_html_column_type_enabled", release_gs_all_sheets_options_enabled: "release_gs_all_sheets_options_enabled", + ab_premium_datasources_view_enabled: "ab_premium_datasources_view_enabled", } as const; export type FeatureFlag = keyof typeof FEATURE_FLAG; @@ -89,6 +90,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = { release_evaluation_scope_cache: false, release_table_html_column_type_enabled: false, release_gs_all_sheets_options_enabled: false, + ab_premium_datasources_view_enabled: false, }; export const AB_TESTING_EVENT_KEYS = { diff --git a/app/client/src/ce/utils/analyticsUtilTypes.ts b/app/client/src/ce/utils/analyticsUtilTypes.ts index e054c7cc1eed..4d5638a7e701 100644 --- a/app/client/src/ce/utils/analyticsUtilTypes.ts +++ b/app/client/src/ce/utils/analyticsUtilTypes.ts @@ -355,7 +355,8 @@ export type EventName = | "MALFORMED_USAGE_PULSE" | "REQUEST_INTEGRATION_CTA" | "REQUEST_INTEGRATION_SUBMITTED" - | "TABLE_WIDGET_V2_HTML_CELL_USAGE"; + | "TABLE_WIDGET_V2_HTML_CELL_USAGE" + | PREMIUM_DATASOURCES_EVENTS; type HOMEPAGE_CREATE_APP_FROM_TEMPLATE_EVENTS = | "TEMPLATE_DROPDOWN_CLICK" @@ -469,3 +470,12 @@ export type CUSTOM_WIDGET_EVENTS = | "CUSTOM_WIDGET_BUILDER_DEBUGGER_VISIBILITY_CHANGED" | "CUSTOM_WIDGET_API_TRIGGER_EVENT" | "CUSTOM_WIDGET_API_UPDATE_MODEL"; + +export type PREMIUM_DATASOURCES_EVENTS = + | "PREMIUM_INTEGRATION_CTA" + | "PREMIUM_MODAL_RELEVANT_LEARN_MORE" + | "PREMIUM_MODAL_NOT_RELEVANT_LEARN_MORE" + | "PREMIUM_MODAL_RELEVANT_SCHEDULE_CALL" + | "PREMIUM_MODAL_NOT_RELEVANT_SUBMIT" + | "SOON_INTEGRATION_CTA" + | "SOON_NOTIFY_REQUEST"; diff --git a/app/client/src/constants/PremiumDatasourcesConstants.ts b/app/client/src/constants/PremiumDatasourcesConstants.ts new file mode 100644 index 000000000000..e659b55bbbef --- /dev/null +++ b/app/client/src/constants/PremiumDatasourcesConstants.ts @@ -0,0 +1,21 @@ +import { getAssetUrl } from "ee/utils/airgapHelpers"; +import { ASSETS_CDN_URL } from "./ThirdPartyConstants"; + +interface PremiumIntegration { + name: string; + icon: string; +} + +export const PREMIUM_INTEGRATIONS: PremiumIntegration[] = [ + { + name: "Zendesk", + icon: getAssetUrl(`${ASSETS_CDN_URL}/zendesk-icon.png`), + }, + { + name: "Salesforce", + icon: getAssetUrl(`${ASSETS_CDN_URL}/salesforce-icon.png`), + }, +]; + +export const PREMIUM_INTEGRATION_CONTACT_FORM = + "PREMIUM_INTEGRATION_CONTACT_FORM"; diff --git a/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx b/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx index 8ee774b06e51..e23016fdfae5 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceTab.tsx @@ -41,6 +41,7 @@ import AIDataSources from "./AIDataSources"; import Debugger from "../DataSourceEditor/Debugger"; import { isPluginActionCreating } from "PluginActionEditor/store"; import RequestNewIntegration from "./RequestNewIntegration"; +import PremiumDatasources from "pages/Editor/IntegrationEditor/PremiumDatasources"; const NewIntegrationsContainer = styled.div` ${thinScrollbar}; @@ -130,6 +131,7 @@ function CreateNewDatasource({ active, isCreating, isOnboardingScreen, + isPremiumDatasourcesViewEnabled, pageId, showMostPopularPlugins, showUnsupportedPluginDialog, // TODO: Fix this the next time the file is edited @@ -170,7 +172,11 @@ function CreateNewDatasource({ parentEntityType={parentEntityType} showMostPopularPlugins={showMostPopularPlugins} showUnsupportedPluginDialog={showUnsupportedPluginDialog} - /> + > + {showMostPopularPlugins && isPremiumDatasourcesViewEnabled && ( + + )} + ); } @@ -252,6 +258,7 @@ interface CreateNewDatasourceScreenProps { pageId: string; isOnboardingScreen?: boolean; isRequestNewIntegrationEnabled: boolean; + isPremiumDatasourcesViewEnabled: boolean; } interface CreateNewDatasourceScreenState { @@ -283,6 +290,7 @@ class CreateNewDatasourceTab extends React.Component< dataSources, isCreating, isOnboardingScreen, + isPremiumDatasourcesViewEnabled, isRequestNewIntegrationEnabled, pageId, showDebugger, @@ -313,6 +321,7 @@ class CreateNewDatasourceTab extends React.Component< active={false} isCreating={isCreating} isOnboardingScreen={!!isOnboardingScreen} + isPremiumDatasourcesViewEnabled={isPremiumDatasourcesViewEnabled} location={location} pageId={pageId} showMostPopularPlugins @@ -386,6 +395,9 @@ const mapStateToProps = (state: AppState) => { const isRequestNewIntegrationEnabled = !!featureFlags?.ab_request_new_integration_enabled; + const isPremiumDatasourcesViewEnabled = + !!featureFlags?.ab_premium_datasources_view_enabled; + return { dataSources: getDatasources(state), mockDatasources: getMockDatasources(state), @@ -395,6 +407,7 @@ const mapStateToProps = (state: AppState) => { showDebugger, pageId, isRequestNewIntegrationEnabled, + isPremiumDatasourcesViewEnabled, }; }; diff --git a/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx b/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx index 85eee49230eb..adbb9d50963f 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/DatasourceHome.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { type ReactNode } from "react"; import styled from "styled-components"; import { connect } from "react-redux"; import { initialize } from "redux-form"; @@ -132,6 +132,7 @@ interface DatasourceHomeScreenProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any showUnsupportedPluginDialog: (callback: any) => void; isAirgappedInstance?: boolean; + children?: ReactNode; } interface ReduxDispatchProps { @@ -294,6 +295,7 @@ class DatasourceHomeScreen extends React.Component { ); })} + {this.props.children} ); diff --git a/app/client/src/pages/Editor/IntegrationEditor/NewQuery.tsx b/app/client/src/pages/Editor/IntegrationEditor/NewQuery.tsx index 15ea740854db..78fec8f8a883 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/NewQuery.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/NewQuery.tsx @@ -56,7 +56,9 @@ class QueryHomeScreen extends React.Component { parentEntityType={parentEntityType} showMostPopularPlugins={showMostPopularPlugins} showUnsupportedPluginDialog={showUnsupportedPluginDialog} - /> + > + {this.props.children} + ); } diff --git a/app/client/src/pages/Editor/IntegrationEditor/PremiumDatasources/ContactForm.tsx b/app/client/src/pages/Editor/IntegrationEditor/PremiumDatasources/ContactForm.tsx new file mode 100644 index 000000000000..369fb7c7b020 --- /dev/null +++ b/app/client/src/pages/Editor/IntegrationEditor/PremiumDatasources/ContactForm.tsx @@ -0,0 +1,175 @@ +import { Button, Flex, ModalHeader, Text, toast } from "@appsmith/ads"; +import { createMessage, PREMIUM_DATASOURCES } from "ee/constants/messages"; +import type { AppState } from "ee/reducers"; +import React, { useCallback } from "react"; +import { connect, useSelector } from "react-redux"; +import { + Field, + formValueSelector, + getFormSyncErrors, + reduxForm, + type FormErrors, + type InjectedFormProps, +} from "redux-form"; +import { getCurrentUser } from "selectors/usersSelectors"; +import styled from "styled-components"; +import { isEmail } from "utils/formhelpers"; +import ReduxFormTextField from "components/utils/ReduxFormTextField"; +import { PRICING_PAGE_URL } from "constants/ThirdPartyConstants"; +import { getAppsmithConfigs } from "ee/configs"; +import { getInstanceId, isFreePlan } from "ee/selectors/tenantSelectors"; +import { pricingPageUrlSource } from "ee/utils/licenseHelpers"; +import { RampFeature, RampSection } from "utils/ProductRamps/RampsControlList"; +import { + getContactFormModalDescription, + getContactFormModalTitle, + getContactFormSubmitButtonText, + handleLearnMoreClick, + handleSubmitEvent, + shouldLearnMoreButtonBeVisible, +} from "utils/PremiumDatasourcesHelpers"; +import { PREMIUM_INTEGRATION_CONTACT_FORM } from "constants/PremiumDatasourcesConstants"; + +const FormWrapper = styled.form` + display: flex; + flex-direction: column; + gap: var(--ads-spaces-7); +`; + +const PremiumDatasourceContactForm = ( + props: PremiumDatasourceContactFormProps, +) => { + const instanceId = useSelector(getInstanceId); + const appsmithConfigs = getAppsmithConfigs(); + const isFreePlanInstance = useSelector(isFreePlan); + + const redirectPricingURL = PRICING_PAGE_URL( + appsmithConfigs.pricingUrl, + pricingPageUrlSource, + instanceId, + RampFeature.PremiumDatasources, + RampSection.PremiumDatasourcesContactModal, + ); + + const onSubmit = () => { + submitEvent(); + toast.show(createMessage(PREMIUM_DATASOURCES.SUCCESS_TOAST_MESSAGE), { + kind: "success", + }); + props.closeModal(); + }; + + const onClickLearnMore = useCallback(() => { + handleLearnMoreClick( + props.integrationName, + props.email || "", + redirectPricingURL, + ); + }, [redirectPricingURL, props.email, props.integrationName]); + + const submitEvent = useCallback(() => { + handleSubmitEvent( + props.integrationName, + props.email || "", + !isFreePlanInstance, + ); + }, [props.email, props.integrationName, isFreePlanInstance]); + + return ( + <> + + {getContactFormModalTitle(props.integrationName, !isFreePlanInstance)} + + + + {getContactFormModalDescription( + props.email || "", + !isFreePlanInstance, + )} + + + + {shouldLearnMoreButtonBeVisible(!isFreePlanInstance) && ( + + )} + + + + + ); +}; + +const selector = formValueSelector(PREMIUM_INTEGRATION_CONTACT_FORM); + +interface PremiumDatasourceContactFormValues { + email?: string; +} + +type PremiumDatasourceContactFormProps = PremiumDatasourceContactFormValues & { + formSyncErrors?: FormErrors; + closeModal: () => void; + integrationName: string; +} & InjectedFormProps< + PremiumDatasourceContactFormValues, + { + formSyncErrors?: FormErrors; + closeModal: () => void; + integrationName: string; + } + >; + +const validate = (values: PremiumDatasourceContactFormValues) => { + const errors: Partial = {}; + + if (!values.email || !isEmail(values.email)) { + errors.email = createMessage(PREMIUM_DATASOURCES.FORM_EMAIL.ERROR); + } + + return errors; +}; + +export default connect((state: AppState) => { + const currentUser = getCurrentUser(state); + + return { + email: selector(state, "email"), + initialValues: { + email: currentUser?.email, + }, + formSyncErrors: getFormSyncErrors(PREMIUM_INTEGRATION_CONTACT_FORM)(state), + }; +}, null)( + reduxForm< + PremiumDatasourceContactFormValues, + { + formSyncErrors?: FormErrors; + closeModal: () => void; + integrationName: string; + } + >({ + validate, + form: PREMIUM_INTEGRATION_CONTACT_FORM, + enableReinitialize: true, + })(PremiumDatasourceContactForm), +); diff --git a/app/client/src/pages/Editor/IntegrationEditor/PremiumDatasources/index.tsx b/app/client/src/pages/Editor/IntegrationEditor/PremiumDatasources/index.tsx new file mode 100644 index 000000000000..e3b52ff4e61b --- /dev/null +++ b/app/client/src/pages/Editor/IntegrationEditor/PremiumDatasources/index.tsx @@ -0,0 +1,91 @@ +import React, { useState } from "react"; +import { ApiCard, CardContentWrapper } from "../NewApi"; +import { getAssetUrl } from "ee/utils/airgapHelpers"; +import { Modal, ModalContent, Tag, Text } from "@appsmith/ads"; +import styled from "styled-components"; +import ContactForm from "./ContactForm"; +import { PREMIUM_INTEGRATIONS } from "constants/PremiumDatasourcesConstants"; +import { + getTagText, + handlePremiumDatasourceClick, +} from "utils/PremiumDatasourcesHelpers"; +import { isFreePlan } from "ee/selectors/tenantSelectors"; +import { useSelector } from "react-redux"; + +const ModalContentWrapper = styled(ModalContent)` + max-width: 518px; +`; + +const PremiumTag = styled(Tag)<{ isBusinessOrEnterprise: boolean }>` + color: ${(props) => + props.isBusinessOrEnterprise + ? "var(--ads-v2-color-gray-700)" + : "var(--ads-v2-color-purple-700)"}; + background-color: ${(props) => + props.isBusinessOrEnterprise + ? "var(--ads-v2-color-gray-100)" + : "var(--ads-v2-color-purple-100)"}; + border-color: ${(props) => + props.isBusinessOrEnterprise ? "#36425233" : "#401d7333"}; + padding: var(--ads-v2-spaces-3) var(--ads-v2-spaces-2); + text-transform: uppercase; + > span { + font-weight: 700; + font-size: 10px; + } +`; + +export default function PremiumDatasources() { + const [selectedIntegration, setSelectedIntegration] = useState(""); + const isFreePlanInstance = useSelector(isFreePlan); + const handleOnClick = (name: string) => { + handlePremiumDatasourceClick(name, !isFreePlanInstance); + setSelectedIntegration(name); + }; + + const onOpenChange = (isOpen: boolean) => { + if (!isOpen) { + setSelectedIntegration(""); + } + }; + + return ( + <> + {PREMIUM_INTEGRATIONS.map((integration) => ( + { + handleOnClick(integration.name); + }} + > + + {integration.name} + + {integration.name} + + + {getTagText(!isFreePlanInstance)} + + + + ))} + + + setSelectedIntegration("")} + integrationName={selectedIntegration} + /> + + + + ); +} diff --git a/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/form.tsx b/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/form.tsx index d2585f0d599a..13e144775cd3 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/form.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/form.tsx @@ -41,6 +41,7 @@ const RequestIntegrationForm = (props: RequestIntegrationFormProps) => { { ); } - if (!values.email || !isEmail(values.email)) { + if (values.email && !isEmail(values.email)) { errors.email = createMessage( REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.ERROR, ); diff --git a/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/index.tsx b/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/index.tsx index 0154902442b1..8d432635c267 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/index.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/index.tsx @@ -5,6 +5,7 @@ import { ModalContent, ModalHeader, ModalTrigger, + Text, } from "@appsmith/ads"; import { createMessage, REQUEST_NEW_INTEGRATIONS } from "ee/constants/messages"; import React, { useState, type ReactNode } from "react"; @@ -42,8 +43,10 @@ function RequestModal({ children }: { children: ReactNode }) { export default function RequestNewIntegration() { return ( - -

{createMessage(REQUEST_NEW_INTEGRATIONS.UNABLE_TO_FIND)}

+ + + {createMessage(REQUEST_NEW_INTEGRATIONS.UNABLE_TO_FIND)} +