-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
🪟 🤝 Free connectors program confirmation UI #21623
Conversation
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.
|
||
return ( | ||
return userDidEnroll ? ( | ||
// eslint-disable-next-line react/jsx-no-useless-fragment |
There was a problem hiding this comment.
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>
)}
</>
)
There was a problem hiding this comment.
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.
5cbde96
to
83f1a27
Compare
There was a problem hiding this 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.
...cloud/components/experiments/FreeConnectorProgram/EnrollmentModal/useShowEnrollmentModal.tsx
Outdated
Show resolved
Hide resolved
text: formatMessage({ id: "freeConnectorProgram.enrollmentModal.validationEmailConfirmation" }), | ||
type: ToastType.INFO, | ||
}); | ||
closeModal(); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
...p/src/packages/cloud/components/experiments/FreeConnectorProgram/InlineEnrollmentCallout.tsx
Outdated
Show resolved
Hide resolved
Updates since the last round of reviews:
|
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
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 😬
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- Stripe redirects the user directly to the
successUrl
their checkout session was configured with (in this case, the airbyte URL the user came from withSTRIPE_SUCCESS_QUERY
appended) - 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.
const { enrollmentStatusQuery } = useFreeConnectorProgram(); | ||
const { data: freeConnectorProgramInfo } = enrollmentStatusQuery; | ||
const displayEnrollmentCallout = freeConnectorProgramInfo?.showEnrollmentUi; |
There was a problem hiding this comment.
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
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.
$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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙏
There was a problem hiding this 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; |
There was a problem hiding this comment.
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 🙂
There was a problem hiding this comment.
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:
- wrap the firebase call in a
try
/catch
- 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)
- for each
case
in that switch statement, match the error code against theAuthErrorCodes
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
What
Suppress enrollment banners and show confirmation
Toast
when user returns to thesuccessUrl
from an enrollment stripe checkout session; dismiss modal and show confirmationToast
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:Email validation resend requested
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:
AuthService
in the banner components as soon as the user requests email validation