Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🪟 🤝 Free connectors program confirmation UI #21623

Merged
merged 7 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable name doesn't make much sense to me as we aren't using it anywhere for a menu, and I can't tell why a menu's width would be a relevant benchmark for this.

Is there a meaningful reason to use 2 * width-size-menu vs. the number itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a weird naming choice. Here, I used that variable purely because the modal does, to keep the two nicely-aligned vis a vis each other.

Zooming out a little, it's not used very many places:
2023-01-20-13:31:07-screenshot
I think it's a fine candidate to be pulled out or renamed or something, I just didn't want to have to verify the correctness of components/connection/CatalogTree/next/StreamDetailsPanel in order to ship the free connectors program 😬

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
Comment on lines +21 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're housing the email verification logic here rather than within the modal component itself, does it make sense to also move startStripeCheckout() here from within the modal? They feel parallel-ish to me.

Edit: actually, it looks like we're doing more there with refs and redirects and that may not be as simple a change as it first looked.


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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userDidEnroll is essentially a temporary place for us to know that a user has enrolled, but we haven't done a full re-query of the backend yet so we can show the Toast and remove the enrollment materials, right?

Copy link
Contributor Author

@ambirdsall ambirdsall Jan 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost right, but the situation is slightly worse than that. After a user successfully finishes the stripe checkout flow:

  1. Stripe redirects the user directly to the successUrl their checkout session was configured with (in this case, the airbyte URL the user came from with STRIPE_SUCCESS_QUERY appended)
  2. fires off a webhook to update our backend

That means that even if we do a full re-query of the backend right away (which in fact we do, since we're reloading the whole webapp), there's a race condition: we cannot rely on the backend to have already processed the enrollment at the moment we ought to show the user a confirmation.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this error handling hard to grok, but it works 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I certainly did when I first pulled it out of EmailVerificationHint.tsx (it's basically unchanged otherwise), but at least it's structurally simple.

The explanation, I guess for posterity:

  1. wrap the firebase call in a try/catch
  2. match against a firebase-defined code (if you want to get fancy, it effectively makes the error type a tagged union) with switch(error.code)
  3. for each case in that switch statement, match the error code against the AuthErrorCodes object imported from "firebase/auth" and give the contextually-appropriate notification

As Joey asked on Slack, a reasonable follow-up question is whether the success handling should also be centralized. Maybe! We'd just have to verify that this behavior also makes sense in the context of EmailValidationHint first, but I didn't want to hold up the free connectors work by going off on that sort of tangent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My confusion was more about throwing the error again on line 276, only to have empty catch() statements in the consumer. I don't quite get why that's necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, that is a better question, thanks for clarifying. It was necessary (emphasis on "was") because I assumed the code I was extracting was structurally appropriate to the use case 🤦 (a reasonable default assumption, given that the internal API defined in authService had an identical type signature to the firebase/auth function it was wrapping, but incorrect). It's quite a simple fix, though.

}
},
async verifyEmail(code: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import styles from "./CreditsPage.module.scss";
const CreditsPage: React.FC = () => {
const { emailVerified } = useAuthService();
useTrackPage(PageTrackingCodes.CREDITS);
const { data: freeConnectorProgramInfo } = useFreeConnectorProgram();
const { enrollmentStatusQuery } = useFreeConnectorProgram();
const { data: freeConnectorProgramInfo } = enrollmentStatusQuery;
const { showEnrollmentUi } = freeConnectorProgramInfo || {};

return (
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,7 +26,8 @@ export const ConnectionPageTitle: React.FC = () => {

const { connection } = useConnectionEditService();

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be simplified to two lines instead of three by doing

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

or something of the like? Just seems redundant as is with the intermediate steps there.


const steps = useMemo(() => {
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;
Comment on lines +1 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏