Skip to content

Commit

Permalink
🪟 🤝 Free connectors program confirmation UI (#21623)
Browse files Browse the repository at this point in the history
* Extract STRIPE_SUCCESS_QUERY to hook for reuse

Also updates the query string itself so it's no longer identical to the
pre-existing checkout success query: this means that a one-off credit
transaction triggered from the credits page won't be mistaken for
enrollment.

* Hide banners, show pop-up after successful enrollment

* Show confirmation when user requests verification email

* Reraise sendEmailVerification errors, so calling code can register success-only actions in a `.then` callback

* Increase the z-index of the Toast UI, so that pop-up notifications aren't obscured by an active modal
  • Loading branch information
ambirdsall authored Jan 20, 2023
1 parent ff3726e commit 680f5da
Show file tree
Hide file tree
Showing 12 changed files with 75 additions and 17 deletions.
1 change: 1 addition & 0 deletions airbyte-webapp/src/components/ui/Toast/Toast.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ $toast-bottom-margin: 27px;
position: fixed;
box-sizing: border-box;
bottom: $toast-bottom-margin;
margin-left: calc(vars.$width-size-menu / 2);
left: 50%;
transform: translate(-50%, 0);
z-index: z-indices.$notification;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import { Text } from "components/ui/Text";

import { StripeCheckoutSessionCreate, StripeCheckoutSessionRead } from "packages/cloud/lib/domain/stripe";

import { STRIPE_SUCCESS_QUERY } from "../hooks/useFreeConnectorProgram";
import { ReactComponent as CardSVG } from "./cards.svg";
import { ReactComponent as ConnectorGridSvg } from "./connectorGrid.svg";
import styles from "./EnrollmentModal.module.scss";
import { ReactComponent as FreeAlphaBetaPillsSVG } from "./free-alpha-beta-pills.svg";
import { ReactComponent as FreeSVG } from "./free.svg";
import { ReactComponent as MailSVG } from "./mail.svg";

const STRIPE_SUCCESS_QUERY = "stripeCheckoutSuccess";

interface EnrollmentModalContentProps {
closeModal: () => void;
createCheckout: (p: StripeCheckoutSessionCreate) => Promise<StripeCheckoutSessionRead>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useIntl } from "react-intl";

import { ToastType } from "components/ui/Toast";

import { useModalService } from "hooks/services/Modal";
import { useNotificationService } from "hooks/services/Notification";
import { useAuthService } from "packages/cloud/services/auth/AuthService";
import { useStripeCheckout } from "packages/cloud/services/stripe/StripeService";
import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService";
Expand All @@ -10,6 +15,19 @@ export const useShowEnrollmentModal = () => {
const { mutateAsync: createCheckout } = useStripeCheckout();
const workspaceId = useCurrentWorkspaceId();
const { emailVerified, sendEmailVerification } = useAuthService();
const { formatMessage } = useIntl();
const { registerNotification } = useNotificationService();

const verifyEmail = () =>
sendEmailVerification()
.then(() => {
registerNotification({
id: "fcp/verify-email",
text: formatMessage({ id: "freeConnectorProgram.enrollmentModal.validationEmailConfirmation" }),
type: ToastType.INFO,
});
})
.catch(); // don't crash the page on error

return {
showEnrollmentModal: () => {
Expand All @@ -21,7 +39,7 @@ export const useShowEnrollmentModal = () => {
createCheckout={createCheckout}
closeModal={closeModal}
emailVerified={emailVerified}
sendEmailVerification={sendEmailVerification}
sendEmailVerification={verifyEmail}
/>
),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Callout } from "components/ui/Callout";
import { Text } from "components/ui/Text";

import { useShowEnrollmentModal } from "./EnrollmentModal";
import { useFreeConnectorProgram } from "./hooks/useFreeConnectorProgram";
import styles from "./InlineEnrollmentCallout.module.scss";

export const EnrollLink: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
Expand All @@ -17,7 +18,9 @@ export const EnrollLink: React.FC<PropsWithChildren<unknown>> = ({ children }) =
);
};
export const InlineEnrollmentCallout: React.FC = () => {
return (
const { userDidEnroll } = useFreeConnectorProgram();

return userDidEnroll ? null : (
<Callout variant="info" className={styles.container}>
<Text size="sm">
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { Text } from "components/ui/Text";

import { ReactComponent as ConnectorsBadges } from "./connectors-badges.svg";
import { useShowEnrollmentModal } from "./EnrollmentModal";
import { useFreeConnectorProgram } from "./hooks/useFreeConnectorProgram";
import styles from "./LargeEnrollmentCallout.module.scss";

export const LargeEnrollmentCallout: React.FC = () => {
const { showEnrollmentModal } = useShowEnrollmentModal();
const { userDidEnroll } = useFreeConnectorProgram();

return (
return userDidEnroll ? null : (
<Callout variant="boldInfo" className={styles.container}>
<FlexContainer direction="row" alignItems="center" className={styles.flexRow}>
<FlexItem grow={false} alignSelf="center">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
import { useState } from "react";
import { useIntl } from "react-intl";
import { useQuery } from "react-query";
import { useSearchParams } from "react-router-dom";
import { useEffectOnce } from "react-use";

import { ToastType } from "components/ui/Toast";

import { useExperiment } from "hooks/services/Experiment";
import { useNotificationService } from "hooks/services/Notification";
import { useConfig } from "packages/cloud/services/config";
import { useDefaultRequestMiddlewares } from "services/useDefaultRequestMiddlewares";
import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService";

import { webBackendGetFreeConnectorProgramInfoForWorkspace } from "../lib/api";

export const STRIPE_SUCCESS_QUERY = "fcpEnrollmentSuccess";

export const useFreeConnectorProgram = () => {
const workspaceId = useCurrentWorkspaceId();
const { cloudApiUrl } = useConfig();
const config = { apiUrl: cloudApiUrl };
const middlewares = useDefaultRequestMiddlewares();
const requestOptions = { config, middlewares };
const freeConnectorProgramEnabled = useExperiment("workspace.freeConnectorsProgram.visible", false);
const [searchParams, setSearchParams] = useSearchParams();
const [userDidEnroll, setUserDidEnroll] = useState(false);
const { formatMessage } = useIntl();
const { registerNotification } = useNotificationService();

return useQuery(["freeConnectorProgramInfo", workspaceId], () =>
useEffectOnce(() => {
if (searchParams.has(STRIPE_SUCCESS_QUERY)) {
// Remove the stripe parameter from the URL
setSearchParams({}, { replace: true });
setUserDidEnroll(true);
registerNotification({
id: "fcp/enrolled",
text: formatMessage({ id: "freeConnectorProgram.enroll.success" }),
type: ToastType.SUCCESS,
});
}
});

const enrollmentStatusQuery = useQuery(["freeConnectorProgramInfo", workspaceId], () =>
webBackendGetFreeConnectorProgramInfoForWorkspace({ workspaceId }, requestOptions).then(
({ hasEligibleConnector, hasPaymentAccountSaved }) => {
const userIsEligibleToEnroll = !hasPaymentAccountSaved && hasEligibleConnector;
Expand All @@ -27,4 +53,9 @@ export const useFreeConnectorProgram = () => {
}
)
);

return {
enrollmentStatusQuery,
userDidEnroll,
};
};
2 changes: 2 additions & 0 deletions airbyte-webapp/src/packages/cloud/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,13 @@
"freeConnectorProgram.title": "Free Connector Program",
"freeConnectorProgram.enrollNow": "Enroll now!",
"freeConnectorProgram.enroll.description": "Enroll in the <b>Free Connector Program</b> to use Alpha and Beta connectors for <b>free</b>.",
"freeConnectorProgram.enroll.success": "Successfully enrolled in the Free Connector Program",
"freeConnectorProgram.enrollmentModal.title": "Free connector program",
"freeConnectorProgram.enrollmentModal.free": "<p1>Alpha and Beta Connectors are free while you're in the program.</p1><p2>The whole Connection is free until both Connectors have move into General Availability (GA)</p2>",
"freeConnectorProgram.enrollmentModal.emailNotification": "We will let you know through email before a Connector you use moves to GA",
"freeConnectorProgram.enrollmentModal.cardOnFile": "When both Connectors are in GA, the Connection will no longer be free. You'll need to have a credit card on file to enroll so Airbyte can handle a Connection's transition to paid service.",
"freeConnectorProgram.enrollmentModal.unvalidatedEmailWarning": "You need to <b>verify your email</b> address before you can enroll in the Free Connector Program. <resendEmail>Re-send verification email</resendEmail>.",
"freeConnectorProgram.enrollmentModal.validationEmailConfirmation": "Verification email sent",
"freeConnectorProgram.enrollmentModal.cancelButtonText": "Cancel",
"freeConnectorProgram.enrollmentModal.enrollButtonText": "Enroll now!",
"freeConnectorProgram.enrollmentModal.unvalidatedEmailButtonText": "Resend email validation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export const AuthenticationProvider: React.FC<React.PropsWithChildren<unknown>>
type: ToastType.ERROR,
});
}
throw error;
}
},
async verifyEmail(code: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import styles from "./CreditsPage.module.scss";
const CreditsPage: React.FC = () => {
const { emailVerified } = useAuthService();
useTrackPage(PageTrackingCodes.CREDITS);
const { data: freeConnectorProgramInfo } = useFreeConnectorProgram();
const { showEnrollmentUi } = freeConnectorProgramInfo || {};
const { enrollmentStatusQuery } = useFreeConnectorProgram();
const { showEnrollmentUi } = enrollmentStatusQuery.data || {};

return (
<MainPageWithScroll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export const EmailVerificationHint: React.FC<Props> = ({ className }) => {
const [isEmailResend, setIsEmailResend] = useState(false);

const onResendVerificationMail = async () => {
await sendEmailVerification();
// the shared error handling inside `sendEmailVerification` suffices
await sendEmailVerification().catch();
setIsEmailResend(true);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export const ConnectionPageTitle: React.FC = () => {

const { connection } = useConnectionEditService();

const { data: freeConnectorProgramInfo } = useFreeConnectorProgram();
const displayEnrollmentCallout = freeConnectorProgramInfo?.showEnrollmentUi;
const { enrollmentStatusQuery } = useFreeConnectorProgram();
const { showEnrollmentUi } = enrollmentStatusQuery.data || {};

const steps = useMemo(() => {
const steps = [
Expand Down Expand Up @@ -80,7 +80,7 @@ export const ConnectionPageTitle: React.FC = () => {
<div className={styles.statusContainer}>
<FlexContainer direction="column" gap="none">
<ConnectionInfoCard />
{displayEnrollmentCallout && <InlineEnrollmentCallout />}
{showEnrollmentUi && <InlineEnrollmentCallout />}
</FlexContainer>
</div>
<StepsMenu lightMode data={steps} onSelect={onSelectStep} activeStep={currentStep} />
Expand Down
10 changes: 5 additions & 5 deletions airbyte-webapp/src/scss/_z-indices.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
$tooltip: 9999 + 3;
$tooltip: 9999 + 4;
$notification: 9999 + 3;
$datepicker: 9999 + 2;
$modal: 9999 + 1;
$sidebar: 9999;
$panelSplitter: 0;
$dropdownMenu: 2;
$notification: 20;
$schemaChangesBackdrop: 3;
$schemaChangesBackdropContent: 4;
$schemaChangesBackdrop: 3;
$dropdownMenu: 2;
$switchSliderBefore: 1;
$tableScroll: 1;
$panelSplitter: 0;

0 comments on commit 680f5da

Please sign in to comment.