Skip to content

Comments

Phone number bottom sheet#48

Merged
fortune710 merged 6 commits intodevfrom
cursor/DEF-98-phone-number-bottom-sheet-eca6
Feb 5, 2026
Merged

Phone number bottom sheet#48
fortune710 merged 6 commits intodevfrom
cursor/DEF-98-phone-number-bottom-sheet-eca6

Conversation

@fortune710
Copy link
Owner

@fortune710 fortune710 commented Feb 5, 2026

Implement a phone number verification bottom sheet on the /capture page to prompt users for their phone number, including OTP verification and skip tracking.


Linear Issue: DEF-98

Open in Cursor Open in Web

Summary by CodeRabbit

Release Notes

  • New Features
    • Added phone number verification using one-time password (OTP) authentication via SMS
    • Added phone number input component with automatic country code detection
    • Added conditional verification prompts during the capture workflow
    • Added resendable OTP functionality with cooldown protection

cursoragent and others added 6 commits February 5, 2026 02:40
Co-authored-by: Fortune Oluwasemilore Alebiosu <fortunealebiosu710@gmail.com>
Co-authored-by: Fortune Oluwasemilore Alebiosu <fortunealebiosu710@gmail.com>
Co-authored-by: Fortune Oluwasemilore Alebiosu <fortunealebiosu710@gmail.com>
Co-authored-by: Fortune Oluwasemilore Alebiosu <fortunealebiosu710@gmail.com>
Co-authored-by: Fortune Oluwasemilore Alebiosu <fortunealebiosu710@gmail.com>
Co-authored-by: Fortune Oluwasemilore Alebiosu <fortunealebiosu710@gmail.com>
@cursor
Copy link

cursor bot commented Feb 5, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link

vercel bot commented Feb 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
keepsafe Ready Ready Preview, Comment Feb 5, 2026 2:59am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

Walkthrough

A comprehensive phone number verification system with OTP (one-time password) via Twilio SMS is introduced across backend and frontend. The backend adds configuration, two OTP endpoints (start and resend), and database schema. The frontend contributes reusable input components, a two-step verification bottom sheet, OTP hashing, and device-level prompt state management integrated into the capture screen.

Changes

Cohort / File(s) Summary
Backend Phone Verification
backend/config.py, backend/main.py, backend/routers/phone_number.py
Adds Twilio API credentials to Settings, registers phone_number router, and introduces two endpoints: POST /otp/start (initiate OTP) and POST /otp/resend (regenerate OTP) with SMS delivery, error handling for config/upsert failures, and 404 for missing records.
Backend Service Updates
backend/services/ingestion_service.py, backend/services/notification_service.py
Implements lazy Pinecone index initialization via _get_index() method and removes explicit schema path from Supabase push_tokens query.
Frontend Phone Input Components
frontend/components/phone-number-bottom-sheet.tsx, frontend/components/profile/phone-number-input.tsx, frontend/components/profile/phone-update-form.tsx, frontend/components/ui/otp-input.tsx
Adds a two-step phone-verification bottom sheet with OTP workflow, a reusable phone-input component with country selection, a refactored phone-update form using the new input component, and a digit-box OTP entry component with multi-box paste support.
Frontend Hooks & Services
frontend/hooks/use-otp-hash.ts, frontend/hooks/use-phone-number-update-record.ts, frontend/hooks/use-profile-operations.ts, frontend/services/phone-number-prompt-service.ts
Provides SHA-256 hashing for OTP verification, fetches pending phone-update records from Supabase, renames phone field to phone_number, and manages per-user prompt scheduling and skip state via device storage.
Frontend Configuration & Types
frontend/.eslintrc.cjs, frontend/constants/supabase.ts, frontend/types/database.ts, frontend/package.json
Adds ESLint config for Expo/React Native, extends Supabase table constants, defines phone_number_updates table type signatures, and adds ESLint/eslint-config-expo dev dependencies.
Frontend Screen Integration
frontend/app/capture/index.tsx
Integrates PhoneNumberBottomSheet into CaptureScreen with state-driven visibility based on pending OTP records and prompt-scheduling logic.
Database Schema
frontend/supabase/migrations/20260205120000_phone_number_updates.sql
Creates phone_number_updates table with user_id, phone_number, otp_hash, and created_at; enforces per-user uniqueness and row-level security policies for authenticated users.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend<br/>(React Native)
    participant Backend as Backend<br/>(FastAPI)
    participant Supabase as Supabase<br/>(Database)
    participant Twilio as Twilio<br/>(SMS API)

    User->>Frontend: Opens capture screen
    Frontend->>Frontend: Check phone_number_updates for pending OTP
    Frontend->>Frontend: Evaluate prompt state (skip/schedule)
    alt Show phone prompt
        Frontend->>User: Display PhoneNumberBottomSheet
        User->>Frontend: Enter phone number
        Frontend->>Backend: POST /otp/start (phone_number)
    end
    
    Backend->>Backend: Generate 6-digit OTP
    Backend->>Backend: Hash OTP with SHA-256
    Backend->>Supabase: Upsert phone_number_updates record
    Supabase-->>Backend: Record stored
    Backend->>Twilio: Send SMS with OTP
    Twilio-->>Backend: SMS sent
    Backend-->>Frontend: Success response
    
    Frontend->>User: Prompt for OTP input
    User->>Frontend: Enter OTP digits
    Frontend->>Frontend: Hash OTP client-side
    Frontend->>Frontend: Compare hash with stored hash
    
    alt Hash matches
        Frontend->>Supabase: Update phone_number in profile
        Frontend->>Supabase: Delete phone_number_updates record
        Frontend->>Frontend: Clear prompt state
        Frontend->>User: Verification complete
    else Hash mismatch
        Frontend->>User: Show error, allow resend
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • improvement #3 — Modifies frontend/components/profile/phone-update-form.tsx for phone-number input UI; this PR refactors that component to use the new reusable PhoneNumberInput.

Poem

🐰 A rabbit's ode to verification flows—
One-time codes in SMS rows,
Twilio hops, hashes bloom,
Phone numbers no longer loom!
Prompts persist with prompt-y glow. ✨📱

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Phone number bottom sheet' is vague and refers to only a UI component implementation detail rather than the main objective of the PR, which is to implement a complete phone number verification system with OTP. Consider a more descriptive title that captures the primary feature, such as 'Implement phone number verification with OTP and bottom sheet' or 'Add phone verification flow with Twilio OTP integration'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/DEF-98-phone-number-bottom-sheet-eca6

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@fortune710 fortune710 marked this pull request as ready for review February 5, 2026 03:29
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@backend/routers/phone_number.py`:
- Around line 103-117: The PR stores an otp_hash but lacks an OTP verification
endpoint; add a new route (e.g., verify_phone_otp) in
backend/routers/phone_number.py that: 1) accepts user_id/phone_number and the
plaintext otp, 2) looks up the corresponding row in the phone_number_updates
table, 3) checks created_at against now (use an expiration window like
EXPIRATION_MINUTES = 10) and returns 400 if expired, 4) verifies the plaintext
otp against the stored otp_hash using the same hashing/verify routine used when
generating otp_hash, 5) on success mark the verification complete (delete or
flag the row) and update the user’s phone verification status, and 6) return
appropriate HTTPExceptions for not found, invalid, or expired OTPs; reference
the existing upsert logic and the phone_number_updates table and reuse the same
hashing/verification helper so comparisons are consistent.

In `@backend/services/ingestion_service.py`:
- Around line 24-33: The synchronous _get_index() causes blocking and race
conditions; make it async (async def _get_index(self)) and protect
initialization with an instance-level asyncio.Lock (e.g., self._index_lock) so
concurrent coroutines serialize; inside the locked section, call
get_pinecone_index() via asyncio.to_thread(...) to offload blocking network I/O;
assign the result to self._pinecone_index only once and return it; finally,
update all call sites (ingest_entry, delete_entry, etc.) to await
self._get_index() instead of calling it synchronously.

In `@frontend/app/capture/index.tsx`:
- Around line 140-160: The Supabase query for pending OTPs in
checkShouldShowPhonePrompt uses maybeSingle() but ignores the returned error,
and the final catch silently swallows failures; update the
supabase.from('phone_number_updates').select(...).maybeSingle() call to
destructure both { data: pendingRecord, error } and handle/log the error (e.g.,
processLogger / console.error) so RLS/network issues are observable, then only
proceed to check pendingRecord?.id and call setShowPhoneSheet(…) when not
cancelled; also avoid swallowing errors in the catch by logging or rethrowing to
aid debugging.

In `@frontend/components/phone-number-bottom-sheet.tsx`:
- Around line 202-244: The client currently verifies OTP in verifyOtp using
matchesHash and then calls updateProfile, which allows bypass; instead implement
a server-side RPC/endpoint (e.g., rpc_verify_and_update_phone) that accepts user
id, phone_number and the raw OTP, looks up phone_number_updates, validates the
OTP hash server-side, performs the profile update and deletes the
phone_number_updates record inside a single transaction, and enforces
RLS/permission checks; update verifyOtp to call this RPC via supabase.rpc
(remove client-side matchesHash logic and avoid exposing otp_hash to the client)
and handle RPC errors/toasts accordingly, and keep clearPhonePromptState/onClose
usage after successful RPC.

In `@frontend/components/profile/phone-number-input.tsx`:
- Around line 45-62: The effect watching initialValue currently returns early
when initialValue is falsy, leaving previous input state intact; update the
useEffect so that when initialValue is empty/undefined it clears the component
state (call setCountryCode(''), setCountryIso(''), and setValue('')) before
returning, otherwise preserve the existing matching-country logic that uses
countries, formatPhoneNumber, extractPhoneNumber to set the derived values.
- Around line 45-61: The seeding logic in the useEffect can truncate non‑US
numbers because formatPhoneNumber/extractPhoneNumber assume 10‑digit US
formatting; update the fallback so that when no matching country is found you
preserve all digits except for US (+1) numbers: extract all digits from
initialValue (but only trim the leading +1 if you detect a +1 country) and call
setValue with the untruncated digits for non‑US numbers; keep the existing
behavior for detected countries by using longestMatch to
setCountryCode/setCountryIso and formatting only the national part, and ensure
you reference the same functions/variables (useEffect, countries, initialValue,
longestMatch, setCountryCode, setCountryIso, setValue, formatPhoneNumber,
extractPhoneNumber) when implementing the conditional.
🧹 Nitpick comments (6)
frontend/hooks/use-otp-hash.ts (1)

16-16: Consider replacing deprecated unescape with TextEncoder.

The unescape(encodeURIComponent(message)) pattern works but relies on the deprecated unescape function. If TextEncoder is available in your target React Native/Expo environment, it would be a more modern approach.

♻️ Alternative using TextEncoder (if available)
- const utf8 = unescape(encodeURIComponent(message));
+ const encoder = new TextEncoder();
+ const utf8Bytes = encoder.encode(message);
+ const utf8 = String.fromCharCode(...utf8Bytes);

Note: Verify TextEncoder availability in your Expo/React Native environment first.

frontend/components/profile/phone-update-form.tsx (1)

1-1: Remove unused useEffect import.

The useEffect import is no longer used after the refactor to PhoneNumberInput.

🧹 Proposed fix
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
frontend/components/ui/otp-input.tsx (1)

24-28: Potential re-render loop if onChange is not memoized by the parent.

The useEffect depends on onChange and calls it when cleaned !== value. If the parent component doesn't wrap onChange in useCallback, each render creates a new function reference, potentially causing unnecessary effect runs. The cleaned !== value guard should prevent infinite loops, but consider documenting that consumers should memoize onChange, or exclude it from the dependency array with an eslint disable comment if intentional.

💡 Option: Store onChange in a ref to avoid dependency
+import React, { useEffect, useMemo, useRef, useCallback } from 'react';
...

export function OtpInput({ value, onChange, length = 6 }: OtpInputProps) {
  const inputs = useRef<Array<TextInput | null>>([]);
+ const onChangeRef = useRef(onChange);
+ onChangeRef.current = onChange;

  ...

  useEffect(() => {
    // Keep the underlying value normalized (digits only).
    const cleaned = value.replace(/\D/g, '').slice(0, length);
-   if (cleaned !== value) onChange(cleaned);
+   if (cleaned !== value) onChangeRef.current(cleaned);
- }, [length, onChange, value]);
+ }, [length, value]);
backend/routers/phone_number.py (2)

19-22: Consider validating E.164 phone number format.

The docstring mentions E.164 format, but there's no validation. Invalid phone numbers will result in SMS delivery failures at Twilio's end, wasting API calls and returning unclear errors to users.

🛡️ Add phone number validation
+import re
+
+E164_PATTERN = re.compile(r"^\+[1-9]\d{6,14}$")
+
 class StartPhoneOtpRequest(BaseModel):
     """Request payload for starting a phone-number OTP verification flow."""

-    phone_number: str = Field(..., description="E.164 phone number (e.g. +15551234567).")
+    phone_number: str = Field(
+        ...,
+        description="E.164 phone number (e.g. +15551234567).",
+        pattern=r"^\+[1-9]\d{6,14}$",
+    )

Alternatively, use Pydantic's field_validator for more complex validation or clearer error messages.


88-121: Implement rate limiting on OTP endpoints to prevent abuse.

The /otp/start and /otp/resend endpoints lack any rate limiting protection. An authenticated user could repeatedly generate OTPs and trigger SMS sends, causing excessive Twilio costs or user lockout scenarios.

Recommended protections:

  1. Per-user rate limit (e.g., max 3–5 OTP requests per 10 minutes)
  2. Cooldown period between requests (e.g., minimum 30–60 seconds)

Consider using slowapi or a custom Redis-based rate limiter integrated with FastAPI.

frontend/hooks/use-phone-number-update-record.ts (1)

38-41: Prefer TABLES constant to avoid string drift.

Using the shared constant prevents typos and keeps table names consistent across the codebase.

♻️ Suggested tweak
-import { supabase } from '@/lib/supabase';
+import { supabase } from '@/lib/supabase';
+import { TABLES } from '@/constants/supabase';
@@
-      const { data, error } = await supabase
-        .from('phone_number_updates')
+      const { data, error } = await supabase
+        .from(TABLES.PHONE_NUMBER_UPDATES)

Comment on lines +140 to +160
// If the user already has a pending OTP record, always show the sheet.
const { data: pendingRecord } = await supabase
.from('phone_number_updates')
.select('id')
.eq('user_id', user.id)
.maybeSingle();

if (pendingRecord?.id) {
if (!cancelled) setShowPhoneSheet(true);
return;
}

const state = await getPhonePromptState(user.id);
const now = Date.now();
const shouldShow =
!state.dontAskAgain && (!state.nextPromptAtMs || now >= state.nextPromptAtMs);

if (!cancelled) setShowPhoneSheet(shouldShow);
};

checkShouldShowPhonePrompt().catch(() => {});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find schema/migration files and phone_number_updates references
rg -l "phone_number_updates" --type ts --type tsx --type sql --type js --type jsx

Repository: fortune710/keepsafe

Length of output: 90


🏁 Script executed:

# Search for schema or migrations directory
fd -t d "schema|migration|supabase" | head -20

Repository: fortune710/keepsafe

Length of output: 121


🏁 Script executed:

# Look for phone_number_updates table definition
rg "phone_number_updates" -A 5 -B 2

Repository: fortune710/keepsafe

Length of output: 11654


Improve error handling for pending OTP record query.

The Supabase query uses maybeSingle() but doesn't destructure the error, and the catch block silently swallows errors. While a unique constraint on user_id guarantees at most one row per user, unhandled errors (e.g., RLS or network issues) can silently prevent the phone prompt from appearing. The hook usePhoneNumberUpdateRecord handles this correctly by checking the error.

Consider destructuring and logging errors for observability:

Suggested error handling
-      const { data: pendingRecord } = await supabase
+      const { data: pendingRecord, error: pendingError } = await supabase
         .from('phone_number_updates')
         .select('id')
         .eq('user_id', user.id)
         .maybeSingle();
+      if (pendingError) {
+        logger.warn('Failed to check pending phone updates', pendingError);
+      }
-    checkShouldShowPhonePrompt().catch(() => {});
+    checkShouldShowPhonePrompt().catch((error) => {
+      logger.warn('Failed to determine phone prompt visibility', error);
+    });
🤖 Prompt for AI Agents
In `@frontend/app/capture/index.tsx` around lines 140 - 160, The Supabase query
for pending OTPs in checkShouldShowPhonePrompt uses maybeSingle() but ignores
the returned error, and the final catch silently swallows failures; update the
supabase.from('phone_number_updates').select(...).maybeSingle() call to
destructure both { data: pendingRecord, error } and handle/log the error (e.g.,
processLogger / console.error) so RLS/network issues are observable, then only
proceed to check pendingRecord?.id and call setShowPhoneSheet(…) when not
cancelled; also avoid swallowing errors in the catch by logging or rethrowing to
aid debugging.

@fortune710 fortune710 merged commit 89721e3 into dev Feb 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants