feat(web): onboarding — skip affordance, stable layout, robust timing, dismissible close
- Show a "Atla" link once the trial actually provisioned so impatient or returning users can jump straight to the confirmation card; captures onboarding_skipped. - Reserve a min-height on the provisioning container so the dialog doesn't jerk when the loader/error blocks appear or disappear. - Re-check animation-ended on visibilitychange — background tabs throttle setTimeout, which could leave a user stuck if they tabbed away during the 7.5s window. - Show the default close X again when the modal is dismissible (completed or escape-hatch active) instead of hiding it for the whole lifecycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,10 +116,23 @@ export function WelcomeOnboardingModal({
|
||||
trialMutation.mutate();
|
||||
}, [subData, eligibleForTrial, onFinished, trialMutation.mutate]);
|
||||
|
||||
// The animation runs ~7s; mark it ended a touch after.
|
||||
// The animation runs ~7s; mark it ended a touch after. Background tabs
|
||||
// throttle setTimeout, so if the tab was hidden during the window we
|
||||
// re-check on visibilitychange and force the flag if the animation should
|
||||
// already be over.
|
||||
useEffect(() => {
|
||||
const start = Date.now();
|
||||
const timer = setTimeout(() => setAnimationEnded(true), 7500);
|
||||
return () => clearTimeout(timer);
|
||||
const onVisibility = () => {
|
||||
if (document.visibilityState === "visible" && Date.now() - start >= 7500) {
|
||||
setAnimationEnded(true);
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
document.removeEventListener("visibilitychange", onVisibility);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Safety net: if we've been stuck on provisioning past the threshold, OR the
|
||||
@@ -157,7 +170,9 @@ export function WelcomeOnboardingModal({
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-w-[calc(100vw-2rem)] overflow-hidden border-brand/25 [&>button]:hidden sm:max-w-lg"
|
||||
className={`max-w-[calc(100vw-2rem)] overflow-hidden border-brand/25 sm:max-w-lg ${
|
||||
canDismiss ? "" : "[&>button]:hidden"
|
||||
}`}
|
||||
onInteractOutside={(e) => {
|
||||
if (!canDismiss) e.preventDefault();
|
||||
}}
|
||||
@@ -166,7 +181,7 @@ export function WelcomeOnboardingModal({
|
||||
}}
|
||||
>
|
||||
{phase === "provisioning" ? (
|
||||
<div className="flex flex-col items-center gap-6 py-4">
|
||||
<div className="flex min-h-[300px] flex-col items-center gap-6 py-4">
|
||||
<DialogHeader className="items-center">
|
||||
<DialogTitle className="flex items-center gap-2 text-xl">
|
||||
<Sparkles className="h-6 w-6 animate-pulse text-brand" />
|
||||
@@ -209,6 +224,20 @@ export function WelcomeOnboardingModal({
|
||||
{t("subscription.onboarding.step4")}...
|
||||
</div>
|
||||
)}
|
||||
{/* Once the trial is real, let impatient or returning users skip
|
||||
the rest of the animation and jump to the confirmation card. */}
|
||||
{trialMutation.isSuccess && !animationEnded && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
capture("onboarding_skipped", { source: "welcome_onboarding" });
|
||||
setAnimationEnded(true);
|
||||
}}
|
||||
className="text-xs text-muted-foreground underline underline-offset-4 transition hover:text-foreground"
|
||||
>
|
||||
{t("subscription.onboarding.skip")}
|
||||
</button>
|
||||
)}
|
||||
{trialMutation.isError && !escapeHatchEnabled && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-sm leading-5 text-red-600 dark:text-red-400">
|
||||
|
||||
Reference in New Issue
Block a user