({ stepIndex }: TrialStepProps)
| 72 | } |
| 73 | |
| 74 | export function TrialStep({ stepIndex }: TrialStepProps) { |
| 75 | const { data: offers, isPending, isError } = useOffers({ |
| 76 | // we have a escape hatch on error, so don't retry. |
| 77 | retry: 0 |
| 78 | }); |
| 79 | const { toast } = useToast(); |
| 80 | const router = useRouter(); |
| 81 | const searchParams = useSearchParams(); |
| 82 | const captureEvent = useCaptureEvent(); |
| 83 | const [billingInterval, setBillingInterval] = useState<BillingInterval>("year"); |
| 84 | const [isPrimaryLoading, setIsPrimaryLoading] = useState(false); |
| 85 | const [isSkipLoading, setIsSkipLoading] = useState(false); |
| 86 | const { data: session } = useSession(); |
| 87 | const sessionEmail = session?.user?.email ?? ""; |
| 88 | const [currentEmail, setCurrentEmail] = useState<string>(""); |
| 89 | |
| 90 | // Only treat the email as an override when the user has actually changed it |
| 91 | // away from the canonical session email. |
| 92 | const overrideEmail = |
| 93 | sessionEmail && currentEmail && currentEmail !== sessionEmail |
| 94 | ? currentEmail |
| 95 | : undefined; |
| 96 | |
| 97 | const [isReturningFromCheckoutSuccess, setIsReturningFromCheckoutSuccess] = useState(searchParams.get('checkout') === 'success'); |
| 98 | |
| 99 | // Fire-once when offers resolve, so isTrialEligible is reliable and react-query |
| 100 | // refetches don't cause duplicate views. |
| 101 | const hasCapturedViewRef = useRef(false); |
| 102 | useEffect(() => { |
| 103 | if (isReturningFromCheckoutSuccess) { |
| 104 | return; |
| 105 | } |
| 106 | if (offers && !hasCapturedViewRef.current) { |
| 107 | hasCapturedViewRef.current = true; |
| 108 | captureEvent('wa_onboard_trial_step_viewed', { |
| 109 | isTrialEligible: offers.trial.eligible, |
| 110 | }); |
| 111 | } |
| 112 | }, [offers, captureEvent, isReturningFromCheckoutSuccess]); |
| 113 | |
| 114 | // Post-checkout: complete onboarding, then forward `session_id` to `/` so |
| 115 | // CheckoutReturnHandler (mounted in the (app) layout) can fire the activation |
| 116 | // dialog. We defer completeOnboarding until after Stripe actually returns |
| 117 | // success — abandoning checkout leaves the user on /onboard to retry. |
| 118 | const hasHandledReturnRef = useRef(false); |
| 119 | useEffect(() => { |
| 120 | if (!isReturningFromCheckoutSuccess || hasHandledReturnRef.current) { |
| 121 | return; |
| 122 | } |
| 123 | hasHandledReturnRef.current = true; |
| 124 | |
| 125 | const sessionId = searchParams.get('session_id'); |
| 126 | void (async () => { |
| 127 | const result = await completeOnboarding(); |
| 128 | if (isServiceError(result)) { |
| 129 | toast({ |
| 130 | description: `Failed to complete onboarding: ${result.message}`, |
| 131 | variant: "destructive", |
nothing calls this directly
no test coverage detected