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

Conversation

ambirdsall
Copy link
Contributor

@ambirdsall ambirdsall commented Jan 19, 2023

What

Suppress enrollment banners and show confirmation Toast when user returns to the successUrl from an enrollment stripe checkout session; dismiss modal and show confirmation Toast when a user requests a verification email resend from the enrollment modal. Closes #21616.

screen recordings

Enrollment success. In retrospect, I should have started recording the gif before opening the modal, to make the difference more apparent when the enrollment banner is hidden. Ah well, c'est la vie. Gifs are hidden behind <details> tags because they are large and distracting:

![fcp-enrollment-success](https://user-images.githubusercontent.com/7516653/213574254-30a04245-9128-464e-beb3-06f3724f031e.gif)

Email validation resend requested

![fcp-email-validation-confirmation](https://user-images.githubusercontent.com/7516653/213574240-cf6c21ff-a791-4631-93aa-829fda766cfe.gif)

How

Pretty straightforward implementation. One key but nonobvious detail: this changes the query param appended to the stripe successUrl, in order to distinguish between a Free Connector Program enrollment and a one-off transaction on the credits page.

Open question about a future UX improvement

Should the email verification popup mention that the user has to refresh the page after confirming their email? Simply retriggering the modal won't reflect any changes that may have happened if the banner that triggers it hasn't rerendered, because the current modal implementation cannot directly use the required hooks (it renders outside of the necessary context provider components).

Options:

  • tell users to refresh the page in the popup
  • start polling email validation status from AuthService in the banner components as soon as the user requests email validation
  • other??

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.
@octavia-squidington-iv octavia-squidington-iv added the area/frontend Related to the Airbyte webapp label Jan 19, 2023
@ambirdsall ambirdsall marked this pull request as ready for review January 19, 2023 22:31
@ambirdsall ambirdsall changed the title Alex/free connectors program confirmation UI 🪟 🤝 Free connectors program confirmation UI Jan 19, 2023

return (
return userDidEnroll ? (
// eslint-disable-next-line react/jsx-no-useless-fragment
Copy link
Contributor Author

@ambirdsall ambirdsall Jan 19, 2023

Choose a reason for hiding this comment

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

If the whole UI subtree should be rendered or not based on some condition, I think keeping the conditional logic outside the UI it toggles, like this:

return someCond ? (
  <></>
) : (
  <TheActualComponent>
    <And>
      <ItsChildren />
    </And>
  </TheActualComponent>
);

can be much nicer to read than sticking it inside the nominal UI it's trying to toggle:

return (
  <>
    {someCond ? null : (
      <TheActualComponent>
        <And>
          <ItsChildren />
        </And>
      </TheActualComponent>
    )}
  </>
)

Copy link
Contributor

Choose a reason for hiding this comment

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

No strong opinion here, they are functionally the same and I don't see a big difference in readability. Usually I would argue to keep conditions like this outside the component (e.g. don't render the component at all), but in this case it makes sense inside.

I would suggest ditching the empty fragment (<></>) that is not necessary here (just return null if you don't want react to render anything to the DOM.

@ambirdsall ambirdsall force-pushed the alex/free-connectors-program-confirmation-UI branch from 5cbde96 to 83f1a27 Compare January 19, 2023 23:40
Copy link
Contributor

@josephkmh josephkmh left a comment

Choose a reason for hiding this comment

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

Just a couple small comments. I think we should find a way to re-check whether a user's email has been verified when the window regains focus. That would make the UX a lot smoother. But I think that will require a refactor of the AuthService, so we can make that a separate PR.

text: formatMessage({ id: "freeConnectorProgram.enrollmentModal.validationEmailConfirmation" }),
type: ToastType.INFO,
});
closeModal();
Copy link
Contributor

@josephkmh josephkmh Jan 20, 2023

Choose a reason for hiding this comment

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

I think closing the modal is kind of weird UX. But since we don't have an easy way to revalidate whether the user's email has been verified, it might be the easiest short-term solution.

Btw I think we can safely move ModalServiceProvider inside the AuthenticationProvider in App.tsx, that way we can call most of our normal hooks (including useAuthService()) from within modals. @timroes any reason why ModalServiceProvider is so far outside the provider tree in cloud's App.tsx?

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 think closing the modal is kind of weird UX

Agreed. Unfortunately, because the modal is at a higher z-index than the toast, leaving it open means the only way to confirm the action is rendering something inline in the modal, which also seemed weird.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can switch those z-indexes around. In general I'd assume an incoming notification should be higher even than an open modal?

Copy link
Collaborator

Choose a reason for hiding this comment

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

And for posterity (talked offline with Joey about that): We should be fine reordering the providers, but we might need to check which orders work since there are "some" dependencies between them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 to both. To keep this particular PR's diff focused, though, I'll leave the providers reastructuring and clean up associated cleanup for a follow-up PR.


return (
return userDidEnroll ? (
// eslint-disable-next-line react/jsx-no-useless-fragment
Copy link
Contributor

Choose a reason for hiding this comment

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

No strong opinion here, they are functionally the same and I don't see a big difference in readability. Usually I would argue to keep conditions like this outside the component (e.g. don't render the component at all), but in this case it makes sense inside.

I would suggest ditching the empty fragment (<></>) that is not necessary here (just return null if you don't want react to render anything to the DOM.

@ambirdsall
Copy link
Contributor Author

Updates since the last round of reviews:

  • while keeping the shared error handling in sendEmailVerification, also reraise caught errors. Client code (all two files of it lol) now need to use .catch() if they don't want to risk crashing the page, but it means a chained .then callback will only be run on success instead of being a de facto .finally
  • updated the toast z-index to be higher than the modal's. Also, for readability, sorted the z-index variables in order of, well, z-index.
  • centered the toast popup vis a vis the modal.

Before tweaking the toast alignment:
2023-01-20-12:22:36-screenshot
2023-01-20-12:22:48-screenshot

and after:
2023-01-20-12:51:16-screenshot
2023-01-20-12:51:29-screenshot

Copy link
Contributor

@teallarson teallarson left a comment

Choose a reason for hiding this comment

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

I'll defer to Joey on a review here! I mostly just had questions.

@@ -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 😬

Comment on lines +21 to +30
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
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.

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.

Comment on lines 29 to 31
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.

Comment on lines +1 to +11
$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;
Copy link
Contributor

Choose a reason for hiding this comment

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

🙏

Copy link
Contributor

@josephkmh josephkmh left a comment

Choose a reason for hiding this comment

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

I think the error handling here is kind of hard to reason about (throwing & catching without handling seems... unintuitive?) but it works for now, and we've got a deadline. So happy to merge it the way it is!

@@ -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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/frontend Related to the Airbyte webapp
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Show confirmation upon successfull enrollment in Free Connector Program and verification email re-send
5 participants