From ef754dbf9385cac76a6bb2e9ca5d8ace2d978d1a Mon Sep 17 00:00:00 2001 From: vinitProjectWork Date: Tue, 11 Nov 2025 23:32:38 +0530 Subject: [PATCH 1/3] feat: add sign-in functionality and enhance navigation components - Implemented a sign-in handler in App component to navigate to the email capture screen. - Updated NavigationButtons to conditionally display a sign-in link based on authentication status. - Enhanced various screen components to support the new sign-in functionality by passing the onSignInClick handler. - Improved user experience by ensuring seamless navigation and visibility of sign-in options where applicable. --- App.tsx | 6 + components/common/NavigationButtons.tsx | 98 ++++-- components/screens/AccountCreationScreen.tsx | 301 ++++++++++++------- components/screens/CompositeScreen.tsx | 195 +++++++++++- components/screens/DateScreen.tsx | 3 +- components/screens/MultiSelectScreen.tsx | 3 +- components/screens/NumberScreen.tsx | 3 +- components/screens/SingleSelectScreen.tsx | 101 +++++-- components/screens/TextScreen.tsx | 3 +- components/screens/common.ts | 1 + forms/weight-loss/data.ts | 14 + hooks/useFormLogic.ts | 2 +- 12 files changed, 548 insertions(+), 182 deletions(-) diff --git a/App.tsx b/App.tsx index a8d114f..3296a1b 100644 --- a/App.tsx +++ b/App.tsx @@ -921,6 +921,11 @@ const App: React.FC = ({ formConfig: providedFormConfig, defaultCondit screen.type !== 'interstitial' && screen.type !== 'content'; + // Handler to navigate to email capture screen for sign-in + const handleSignInClick = () => { + goToScreen('capture.email'); + }; + const commonProps = { screen, answers, @@ -931,6 +936,7 @@ const App: React.FC = ({ formConfig: providedFormConfig, defaultCondit onBack: screen.id === 'complete.assessment_review' ? undefined : goToPrev, defaultCondition: resolvedCondition, showLoginLink, + onSignInClick: handleSignInClick, // Add sign-in handler }; if (screen.id === 'treatment.glp1_history') { diff --git a/components/common/NavigationButtons.tsx b/components/common/NavigationButtons.tsx index 25a549a..b956a5e 100644 --- a/components/common/NavigationButtons.tsx +++ b/components/common/NavigationButtons.tsx @@ -1,5 +1,6 @@ import { ArrowRight, Check } from 'lucide-react'; import { motion } from 'framer-motion'; +import { useState, useEffect } from 'react'; interface NavigationButtonsProps { showBack?: boolean; @@ -10,6 +11,7 @@ interface NavigationButtonsProps { nextLabel?: string; nextButtonType?: 'button' | 'submit'; layout?: 'spread' | 'grouped'; // New prop for layout variant + onSignInClick?: () => void; // Optional handler for sign-in link } export default function NavigationButtons({ @@ -21,38 +23,78 @@ export default function NavigationButtons({ nextLabel = 'Continue', nextButtonType = 'button', layout = 'spread', + onSignInClick, }: NavigationButtonsProps) { + const [showSignInLink, setShowSignInLink] = useState(false); + + // Check if zappy_auth_token exists in localStorage + useEffect(() => { + const checkAuthToken = () => { + if (typeof window !== 'undefined') { + const authToken = sessionStorage.getItem('client_record_id'); + setShowSignInLink(!authToken); + } + }; + + checkAuthToken(); + // Also check on storage events (in case token is added/removed in another tab) + if (typeof window !== 'undefined') { + window.addEventListener('storage', checkAuthToken); + return () => window.removeEventListener('storage', checkAuthToken); + } + }, []); + // Always center the continue button, no back button at bottom // Unified button styling matching DemographicsScreen, EmailCaptureScreen, etc. return ( - - + - {isNextLoading && ( - - - - - )} - {nextLabel} - {!isNextLoading && } - - + + {isNextLoading && ( + + + + + )} + {nextLabel} + {!isNextLoading && } + + + + {/* Sign-in link - Show if no auth token and handler is provided */} + {showSignInLink && onSignInClick && ( + + + + )} + ); } diff --git a/components/screens/AccountCreationScreen.tsx b/components/screens/AccountCreationScreen.tsx index a4d62e2..721ac65 100644 --- a/components/screens/AccountCreationScreen.tsx +++ b/components/screens/AccountCreationScreen.tsx @@ -554,8 +554,39 @@ function MockPaymentForm({ const planName = selectedPlan?.name || answers["selected_plan_name"] || "Selected Plan"; const planPrice = - selectedPlan?.per_month_price || answers["selected_plan_price"] || 299; + selectedPlan?.invoice_amount || answers["selected_plan_price"] || 299; const invoiceAmount = selectedPlan?.invoice_amount || answers["selected_plan_invoice_amount"] || 299; + + // Extract plan details for multi-month display + const planType = selectedPlan?.plan || answers["selected_plan_type"] || "month"; // 'month', '3-month', '12-month' + const billingFrequency = selectedPlan?.billingFrequency || "Billed every 4 weeks"; + const savings = selectedPlan?.savings || appliedDiscount?.amount || answers["discount_amount"] || null; + const savingsPercent = selectedPlan?.savingsPercent || (savings && planPrice > 0 ? Math.round((savings / planPrice) * 100) : 0); + + // Calculate total amounts based on plan type + const getOrderTotals = () => { + if (planType === "3-month") { + return { + totalDue: planPrice * 3, + billingCycle: "3 months", + deliveries: 3, + }; + } else if (planType === "12-month") { + return { + totalDue: planPrice * 12, + billingCycle: "annually", + deliveries: 12, + }; + } + // Default to monthly + return { + totalDue: planPrice, + billingCycle: "monthly", + deliveries: 1, + }; + }; + + const orderTotals = getOrderTotals(); // Determine CTA text and title based on user state const ctaText = isExistingCustomer @@ -580,141 +611,191 @@ function MockPaymentForm({ transition={{ delay: 0.1 }} className="bg-white rounded-2xl p-6 shadow-sm border border-[#E8E8E8]" > - {/* Plan Summary */} -
-
-
+ {/* Plan Header */} +
+
+
+

{planName}

-
-

{planName}

-

{selectedMedication}

-
+

+ {selectedMedication} +

+ {savings && savings > 0 && ( +
+ + Save ${savings} ({savingsPercent}% off) +
+ )}
-
- ${planPrice} +
+ ${planPrice} + /mo
-
/month
+ {planType !== "month" && ( +

+ ${orderTotals.totalDue} {orderTotals.billingCycle} +

+ )}
- {/* Payment Info */} -
-
+ {/* Benefits */} +
+
+
+ +
+

+ {orderTotals.deliveries === 1 + ? "Monthly delivery of medication" + : `${orderTotals.deliveries} monthly deliveries included`} +

+
+
+
+ +
+

No insurance required

+
+
+
+ +
+

Ongoing provider support & dosage adjustments

+
+
+
+ +
+

Cancel anytime, no long-term commitment

+
+
+ + {/* Pricing Breakdown */} +
+ {planType !== "month" ? ( + <> +
+ {planName} ({planType}) + ${planPrice}/mo × {orderTotals.deliveries} +
+
+ Billed {orderTotals.billingCycle} + ${orderTotals.totalDue} +
+ {savings && savings > 0 && ( +
+ Your savings + -${savings} +
+ )} + + ) : ( +
+ Monthly subscription + ${planPrice} +
+ )} +
Due today - $0 + $0
+
+ + {/* Important Notice */} +

- You'll be charged ${invoiceAmount} once prescribed. You won't be charged if a provider determines that our program isn't right for you. + You'll be charged {planType !== "month" ? `$${orderTotals.totalDue} ${orderTotals.billingCycle}` : `$${planPrice} monthly`} once prescribed. You won't be charged if a provider determines that our program isn't right for you.

{/* Discount Code Section */}
- - Have a discount code? - - Have a discount code? + - +
-
-
- { - setDiscountCode(event.target.value.toUpperCase()); - setDiscountError(""); - }} - /> - -
- {discountError && ( -
- -

{discountError}

-
- )} +
+ { + setDiscountCode(event.target.value.toUpperCase()); + setDiscountError(""); + }} + /> +
-
- - {appliedDiscount && ( -
-
-
-
- - - - - Discount applied! - -
-

- Code:{" "} - {appliedDiscount.code} -

-

- Discount:{" "} - {appliedDiscount.percentage > 0 - ? `${appliedDiscount.percentage}% off` - : `$${appliedDiscount.amount} off`} -

- {appliedDiscount.description && ( -

- {appliedDiscount.description} + {discountError && ( +

+ +

{discountError}

+
+ )} + {appliedDiscount && ( +
+
+
+
+ + + Discount applied! + +
+

+ Code:{" "} + {appliedDiscount.code}

- )} +

+ Discount:{" "} + {appliedDiscount.percentage > 0 + ? `${appliedDiscount.percentage}% off` + : `$${appliedDiscount.amount} off`} +

+ {appliedDiscount.description && ( +

+ {appliedDiscount.description} +

+ )} +
+
-
-
- )} + )} +
diff --git a/components/screens/CompositeScreen.tsx b/components/screens/CompositeScreen.tsx index 6c6683e..6c8bba6 100644 --- a/components/screens/CompositeScreen.tsx +++ b/components/screens/CompositeScreen.tsx @@ -128,10 +128,11 @@ const getYearsAgoDate = (years: number): Date => { return date; }; -const CompositeScreen: React.FC }> = ({ screen, answers, updateAnswer, onSubmit, showBack, onBack, headerSize, calculations = {}, showLoginLink, apiPopulatedFields = new Set() }) => { +const CompositeScreen: React.FC }> = ({ screen, answers, updateAnswer, onSubmit, showBack, onBack, headerSize, calculations = {}, showLoginLink, onSignInClick, apiPopulatedFields = new Set() }) => { const { title, help_text, fields, footer_note, validation, post_screen_note } = screen; const [errors, setErrors] = useState>({}); const autoAdvanceTimeoutRef = React.useRef(null); + const pendingScrollToFieldRef = React.useRef(null); useEffect(() => { const initialState = answers['home_state']; @@ -148,6 +149,38 @@ const CompositeScreen: React.FC { + if (pendingScrollToFieldRef.current) { + const fieldId = pendingScrollToFieldRef.current; + + // Use requestAnimationFrame to ensure DOM has updated + requestAnimationFrame(() => { + const fieldElement = document.querySelector(`[data-field-id="${fieldId}"]`); + + if (fieldElement) { + const rect = fieldElement.getBoundingClientRect(); + const isVisible = rect.width > 0 && rect.height > 0; + + if (isVisible) { + // Add a small offset to account for fixed headers + const offset = -80; + const elementPosition = fieldElement.getBoundingClientRect().top + window.pageYOffset; + const offsetPosition = elementPosition + offset; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); + + // Clear the pending scroll + pendingScrollToFieldRef.current = null; + } + } + }); + } + }, [answers]); // Re-run when answers change (fields become visible) + const allFields = useMemo(() => { const flattened: Field[] = []; const recurse = (items: FieldOrFieldGroup[]) => { @@ -647,15 +680,85 @@ const CompositeScreen: React.FC { - onSubmit(); - }, delay); + + // Find the next field that will be shown + // Check both progressive_display and conditional_display + const currentFieldIndex = allFields.findIndex(f => f.id === field.id); + const tempAnswers = { ...answers, [field.id]: val }; + + // First, try to find a field that directly depends on the current field + let nextField = allFields.find((f, index) => { + // Skip fields before the current one + if (index <= currentFieldIndex) return false; + + // Check if field has progressive_display pointing to current field + if (f.progressive_display?.show_after_field === field.id) { + return true; + } + + // Check if field has conditional_display that depends on current field + if (f.conditional_display?.show_if) { + const showIf = f.conditional_display.show_if; + // Check if the condition references the current field + if (showIf.includes(`${field.id} ==`) || showIf.includes(`${field.id}!=`)) { + // Check if field will be visible with the new answer + return shouldShowField(f, tempAnswers); + } + } + + return false; + }); + + // If no direct dependency found, find the next visible field + if (!nextField) { + nextField = allFields.find((f, index) => { + if (index <= currentFieldIndex) return false; + return shouldShowField(f, tempAnswers); + }); + } + + if (nextField) { + // Wait for visual confirmation (300-600ms), using 500ms + const delay = 500; + autoAdvanceTimeoutRef.current = setTimeout(() => { + // Set the field ID to scroll to - the useEffect will handle the actual scrolling + // when the field becomes visible after React re-renders + pendingScrollToFieldRef.current = nextField.id; + + // Try to scroll immediately in case the field is already visible + requestAnimationFrame(() => { + const nextFieldElement = document.querySelector(`[data-field-id="${nextField.id}"]`); + if (nextFieldElement) { + const rect = nextFieldElement.getBoundingClientRect(); + const isVisible = rect.width > 0 && rect.height > 0; + + if (isVisible) { + const offset = -80; + const elementPosition = nextFieldElement.getBoundingClientRect().top + window.pageYOffset; + const offsetPosition = elementPosition + offset; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); + pendingScrollToFieldRef.current = null; + } + } + }); + }, delay); + } else { + // No next field, submit the screen to go to next step + // Wait for visual confirmation first + const delay = 500; + autoAdvanceTimeoutRef.current = setTimeout(() => { + onSubmit(); + }, delay); + } } }; return ( -
+
{field.label && (