Skip to content

Commit 0952b96

Browse files
committed
fix (onboarding): complete profile for old users
1 parent 4d08e1f commit 0952b96

7 files changed

Lines changed: 316 additions & 10 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NextResponse } from "next/server"
2+
import { withAuth } from "@lib/api-utils"
3+
4+
const appServerUrl =
5+
process.env.NEXT_PUBLIC_ENVIRONMENT === "selfhost"
6+
? process.env.INTERNAL_APP_SERVER_URL
7+
: process.env.NEXT_PUBLIC_APP_SERVER_URL
8+
9+
export const POST = withAuth(async function POST(request, { authHeader }) {
10+
try {
11+
const body = await request.json()
12+
const response = await fetch(
13+
`${appServerUrl}/api/settings/complete-profile`,
14+
{
15+
method: "POST",
16+
headers: { "Content-Type": "application/json", ...authHeader },
17+
body: JSON.stringify(body)
18+
}
19+
)
20+
21+
const data = await response.json()
22+
if (!response.ok) {
23+
throw new Error(data.detail || "Failed to complete profile.")
24+
}
25+
return NextResponse.json(data)
26+
} catch (error) {
27+
console.error("API Error in /settings/complete-profile (POST):", error)
28+
return NextResponse.json(
29+
{ error: "Internal Server Error", details: error.message },
30+
{ status: 500 }
31+
)
32+
}
33+
})
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"use client"
2+
3+
import React, { useState } from "react"
4+
import { motion } from "framer-motion"
5+
import { cn } from "@utils/cn"
6+
import toast from "react-hot-toast"
7+
import { useRouter } from "next/navigation"
8+
import {
9+
IconBrandWhatsapp,
10+
IconLoader,
11+
IconSparkles
12+
} from "@tabler/icons-react"
13+
import InteractiveNetworkBackground from "@components/ui/InteractiveNetworkBackground"
14+
15+
const questions = [
16+
{
17+
id: "needs-pa",
18+
question:
19+
"Do you often juggle multiple priorities, manage a small team, lead projects, and handle countless day-to-day tasks on your own? Many professionals spend too much time scheduling meetings, organizing their calendar, responding to emails, and doing other administrative work that eats into their day. Do you ever wish you had someone to take these repetitive tasks off your plate?\n\nDo you ever feel the need for a personal assistant?",
20+
type: "yes-no",
21+
required: true
22+
},
23+
{
24+
id: "whatsapp_notifications_number",
25+
question:
26+
"To send you important notifications, task updates, and reminders on WhatsApp, please enter your number with the country code.",
27+
type: "text-input",
28+
required: true,
29+
placeholder: "+14155552671",
30+
icon: <IconBrandWhatsapp />
31+
}
32+
]
33+
34+
const CompleteProfilePage = () => {
35+
const [answers, setAnswers] = useState({})
36+
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
37+
const [isSubmitting, setIsSubmitting] = useState(false)
38+
const router = useRouter()
39+
40+
const handleAnswer = (questionId, answer) => {
41+
setAnswers((prev) => ({ ...prev, [questionId]: answer }))
42+
}
43+
44+
const handleNext = () => {
45+
if (currentQuestionIndex < questions.length - 1) {
46+
setCurrentQuestionIndex((prev) => prev + 1)
47+
}
48+
}
49+
50+
const handleSubmit = async () => {
51+
if (!answers["needs-pa"] || !answers["whatsapp_notifications_number"]) {
52+
toast.error("Please answer all questions to continue.")
53+
return
54+
}
55+
setIsSubmitting(true)
56+
try {
57+
const response = await fetch("/api/settings/complete-profile", {
58+
method: "POST",
59+
headers: { "Content-Type": "application/json" },
60+
body: JSON.stringify(answers)
61+
})
62+
if (!response.ok) {
63+
const result = await response.json()
64+
throw new Error(result.detail || "Failed to update profile.")
65+
}
66+
toast.success("Thank you! Your profile is now complete.")
67+
router.push("/chat")
68+
} catch (error) {
69+
toast.error(`Error: ${error.message}`)
70+
} finally {
71+
setIsSubmitting(false)
72+
}
73+
}
74+
75+
const currentQuestion = questions[currentQuestionIndex]
76+
77+
return (
78+
<div className="relative flex flex-col items-center justify-center min-h-screen w-full p-4 sm:p-8 text-white overflow-hidden">
79+
<div className="absolute inset-0 z-[-1]">
80+
<InteractiveNetworkBackground />
81+
</div>
82+
<motion.div
83+
key="complete-profile"
84+
initial={{ opacity: 0, y: 20 }}
85+
animate={{ opacity: 1, y: 0 }}
86+
className="relative z-10 w-full max-w-2xl text-center"
87+
>
88+
<IconSparkles
89+
size={60}
90+
className="mx-auto text-brand-orange mb-4"
91+
/>
92+
<h1 className="text-3xl sm:text-4xl font-bold mb-2">
93+
Just a quick update...
94+
</h1>
95+
<p className="text-neutral-300 mb-8">
96+
We've added some new features and need a couple more details
97+
to personalize your experience.
98+
</p>
99+
100+
<div className="bg-neutral-900/50 border border-neutral-700/50 rounded-2xl p-6 sm:p-8 text-left space-y-6">
101+
<p className="whitespace-pre-wrap text-neutral-200">
102+
{currentQuestion.question}
103+
</p>
104+
105+
{currentQuestion.type === "yes-no" && (
106+
<div className="flex gap-4 pt-2">
107+
<button
108+
onClick={() => {
109+
handleAnswer(currentQuestion.id, "yes")
110+
setTimeout(handleNext, 100)
111+
}}
112+
className="px-6 py-2 rounded-lg font-semibold bg-neutral-700 hover:bg-neutral-600"
113+
>
114+
Yes
115+
</button>
116+
<button
117+
onClick={() => {
118+
handleAnswer(currentQuestion.id, "no")
119+
setTimeout(handleNext, 100)
120+
}}
121+
className="px-6 py-2 rounded-lg font-semibold bg-neutral-700 hover:bg-neutral-600"
122+
>
123+
No
124+
</button>
125+
</div>
126+
)}
127+
128+
{currentQuestion.type === "text-input" && (
129+
<div className="relative pt-2">
130+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400">
131+
{currentQuestion.icon}
132+
</div>
133+
<input
134+
type="text"
135+
value={answers[currentQuestion.id] || ""}
136+
onChange={(e) =>
137+
handleAnswer(
138+
currentQuestion.id,
139+
e.target.value
140+
)
141+
}
142+
placeholder={currentQuestion.placeholder}
143+
className="w-full pl-10 pr-4 py-3 bg-neutral-800 border border-neutral-700 rounded-lg focus:ring-2 focus:ring-brand-orange"
144+
autoFocus
145+
/>
146+
</div>
147+
)}
148+
</div>
149+
150+
<div className="mt-8">
151+
<button
152+
onClick={handleSubmit}
153+
disabled={
154+
isSubmitting ||
155+
currentQuestionIndex !== questions.length - 1
156+
}
157+
className="w-full max-w-xs py-3 px-6 rounded-lg bg-brand-orange text-brand-black font-semibold text-lg transition-all hover:bg-brand-orange/90 disabled:opacity-50 disabled:cursor-not-allowed"
158+
>
159+
{isSubmitting ? (
160+
<IconLoader className="animate-spin mx-auto" />
161+
) : (
162+
"Continue to App"
163+
)}
164+
</button>
165+
</div>
166+
</motion.div>
167+
</div>
168+
)
169+
}
170+
171+
export default CompleteProfilePage

src/client/components/LayoutWrapper.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ export default function LayoutWrapper({ children }) {
156156
const res = await fetch("/api/user/data")
157157
if (!res.ok) throw new Error("Could not verify user status.")
158158
const data = await res.json()
159-
if (data?.data?.onboardingComplete) {
159+
if (data?.data?.needsDataCompletion) {
160+
router.push("/complete-profile")
161+
} else if (data?.data?.onboardingComplete) {
160162
setIsAllowed(true)
161163
} else {
162164
toast.error("Please complete onboarding first.")

src/server/main/misc/routes.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from main.dependencies import mongo_manager, auth_helper, websocket_manager as main_websocket_manager
1616
from pydantic import BaseModel
1717
from workers.tasks import cud_memory_task
18-
from main.settings.google_sheets_utils import update_onboarding_data_in_sheet, update_plan_in_sheet
18+
from main.settings.google_sheets_utils import update_onboarding_data_in_sheet, update_plan_in_sheet, check_if_contact_is_missing
1919
from main.notifications.whatsapp_client import check_phone_number_exists, send_whatsapp_message
2020

2121
# Google API libraries for validation
@@ -201,21 +201,27 @@ async def check_user_profile_endpoint(user_id: str = Depends(PermissionChecker(r
201201
@router.post("/get-user-data", summary="Get User Profile's userData field")
202202
async def get_user_data_endpoint(payload: dict = Depends(auth_helper.get_decoded_payload_with_claims)):
203203
user_id = payload.get("sub")
204+
user_email = payload.get("email")
204205
profile_doc = await mongo_manager.get_user_profile(user_id)
205206

206207
token_plan = payload.get("plan", "free")
207-
208+
208209
# Check if profile exists and if plan is up-to-date
209210
profile_exists = profile_doc is not None
211+
onboarding_complete = profile_doc.get("userData", {}).get("onboardingComplete", False) if profile_exists else False
212+
needs_data_completion = False
213+
214+
# If onboarding is complete, check if we need to ask for the new questions
215+
if onboarding_complete and user_email:
216+
needs_data_completion = await check_if_contact_is_missing(user_email)
217+
210218
stored_plan = profile_doc.get("userData", {}).get("plan") if profile_exists else None
211219

212220
# This condition covers both creating a new profile and updating an existing one's plan.
213221
if not profile_exists or stored_plan != token_plan:
214222
logger.info(f"Updating plan for user {user_id} to '{token_plan}'. Profile exists: {profile_exists}")
215223
await mongo_manager.update_user_profile(user_id, {"userData.plan": token_plan})
216224

217-
# NEW: Update GSheet when plan changes
218-
user_email = payload.get("email")
219225
if user_email and stored_plan != token_plan: # Only update if the plan actually changed
220226
try:
221227
await update_plan_in_sheet(user_email, token_plan)
@@ -226,11 +232,13 @@ async def get_user_data_endpoint(payload: dict = Depends(auth_helper.get_decoded
226232
profile_doc = await mongo_manager.get_user_profile(user_id)
227233

228234
if profile_doc and "userData" in profile_doc:
229-
return JSONResponse(content={"data": profile_doc["userData"], "status": 200})
230-
235+
response_data = profile_doc["userData"]
236+
response_data["needsDataCompletion"] = needs_data_completion
237+
return JSONResponse(content={"data": response_data, "status": 200})
238+
231239
# Fallback in case re-fetch fails or returns an empty doc
232240
logger.warning(f"Could not retrieve or create userData for user {user_id}. Returning empty data.")
233-
return JSONResponse(content={"data": {}, "status": 200})
241+
return JSONResponse(content={"data": {"needsDataCompletion": needs_data_completion}, "status": 200})
234242

235243
@router.websocket("/ws/notifications")
236244
async def notifications_websocket_endpoint(websocket: WebSocket):

src/server/main/settings/google_sheets_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,26 @@ async def update_plan_in_sheet(user_email: str, new_plan: str):
127127
logger.warning(f"User with email {user_email} not found in Google Sheet. Could not update plan.")
128128
except Exception as e:
129129
logger.error(f"An error occurred while updating plan in Google Sheet for {user_email}: {e}", exc_info=True)
130+
131+
async def check_if_contact_is_missing(user_email: str) -> bool:
132+
"""Checks if the contact number (Column B) is missing for a user in the sheet. Returns True if missing."""
133+
service = _get_sheets_service()
134+
if not service:
135+
# If we can't check the sheet, assume data is not missing to avoid blocking the user.
136+
logger.error("Could not get Google Sheets service to check for missing contact.")
137+
return False
138+
139+
try:
140+
range_to_read = f"{SHEET_NAME}!C:C" # Read email column
141+
result = service.spreadsheets().values().get(spreadsheetId=GOOGLE_SHEET_ID, range=range_to_read).execute()
142+
rows = result.get('values', [])
143+
144+
for i, row in enumerate(rows):
145+
if row and row[0] == user_email:
146+
# Found the user, now check their contact cell in column B
147+
contact_range = f"{SHEET_NAME}!B{i + 1}"
148+
contact_result = service.spreadsheets().values().get(spreadsheetId=GOOGLE_SHEET_ID, range=contact_range).execute()
149+
contact_values = contact_result.get('values', [[]])
150+
return not (contact_values and contact_values[0] and contact_values[0][0])
151+
return True # User not found in sheet at all, so data is missing.
152+
except Exception as e:

src/server/main/settings/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ class ProfileUpdateRequest(BaseModel):
1717
onboardingAnswers: Dict[str, Any]
1818
personalInfo: Dict[str, Any]
1919
preferences: Dict[str, Any]
20+
21+
class CompleteProfileRequest(BaseModel):
22+
needs_pa: str # "yes" or "no"
23+
whatsapp_notifications_number: str

src/server/main/settings/routes.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from main.dependencies import auth_helper
66
from main.dependencies import mongo_manager
77
from main.auth.utils import PermissionChecker, AuthHelper
8-
from main.notifications.whatsapp_client import check_phone_number_exists
9-
from main.settings.models import WhatsAppMcpRequest, WhatsAppNotificationNumberRequest, ProfileUpdateRequest, WhatsAppNotificationRequest
8+
from main.notifications.whatsapp_client import check_phone_number_exists, send_whatsapp_message
9+
from main.settings.models import WhatsAppMcpRequest, WhatsAppNotificationNumberRequest, ProfileUpdateRequest, WhatsAppNotificationRequest, CompleteProfileRequest
1010

1111
logger = logging.getLogger(__name__)
1212
router = APIRouter(
@@ -180,6 +180,71 @@ async def toggle_whatsapp_notifications(
180180
raise e
181181
raise HTTPException(status_code=500, detail="An unexpected error occurred.")
182182

183+
@router.post("/complete-profile", summary="Complete profile for existing users with missing data")
184+
async def complete_profile(
185+
request: CompleteProfileRequest,
186+
payload: dict = Depends(auth_helper.get_decoded_payload_with_claims)
187+
):
188+
user_id = payload.get("sub")
189+
user_email = payload.get("email")
190+
plan = payload.get("plan", "free")
191+
192+
try:
193+
# 1. Validate WhatsApp number
194+
whatsapp_number = request.whatsapp_notifications_number.strip()
195+
validation_result = await check_phone_number_exists(whatsapp_number)
196+
if not validation_result or not validation_result.get("numberExists"):
197+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="This phone number does not appear to be on WhatsApp.")
198+
chat_id = validation_result.get("chatId")
199+
if not chat_id:
200+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not retrieve Chat ID for the number.")
201+
202+
# 2. Fetch existing onboarding data from MongoDB
203+
user_profile = await mongo_manager.get_user_profile(user_id)
204+
if not user_profile:
205+
raise HTTPException(status_code=404, detail="User profile not found.")
206+
207+
existing_onboarding_data = user_profile.get("userData", {}).get("onboardingAnswers", {})
208+
209+
# 3. Consolidate old and new data
210+
consolidated_data = {
211+
**existing_onboarding_data,
212+
"needs-pa": request.needs_pa,
213+
"whatsapp_notifications_number": whatsapp_number
214+
}
215+
216+
# 4. Update MongoDB with new answers and notification prefs
217+
update_payload = {
218+
"userData.onboardingAnswers.needs-pa": request.needs_pa,
219+
"userData.onboardingAnswers.whatsapp_notifications_number": whatsapp_number,
220+
"userData.notificationPreferences.whatsapp": {
221+
"number": whatsapp_number,
222+
"chatId": chat_id,
223+
"enabled": True
224+
}
225+
}
226+
await mongo_manager.update_user_profile(user_id, update_payload)
227+
228+
# 5. Update Google Sheet with the full consolidated data
229+
if user_email:
230+
await update_onboarding_data_in_sheet(user_email, consolidated_data, plan)
231+
else:
232+
logger.warning(f"Could not update GSheet for {user_id} during profile completion: no email.")
233+
234+
# 6. Send conditional WhatsApp message
235+
if request.needs_pa == "yes":
236+
feedback_message = "Hi there, I am Sarthak from team Sentient. Thanks for using our app, are you okay with giving feedback and helping us improve the platform to better suit your needs?"
237+
await send_whatsapp_message(chat_id, feedback_message)
238+
logger.info(f"Sent feedback request to existing user {user_id} after profile completion.")
239+
240+
return JSONResponse(content={"message": "Profile completed successfully."})
241+
242+
except Exception as e:
243+
logger.error(f"Error completing profile for user {user_id}: {e}", exc_info=True)
244+
if isinstance(e, HTTPException):
245+
raise e
246+
raise HTTPException(status_code=500, detail="An unexpected error occurred.")
247+
183248
@router.post("/profile", summary="Update User Profile and Onboarding Data")
184249
async def update_profile_data(
185250
request: ProfileUpdateRequest,

0 commit comments

Comments
 (0)