Add WhatsApp verification channel with per-country routing - #2618
Add WhatsApp verification channel with per-country routing#2618kaladivo wants to merge 1 commit into
Conversation
Login codes can now be delivered via WhatsApp through Prelude's preferred_channel option. The server picks the channel: clients send channel=auto and prefixes listed in WHATSAPP_PREFERRED_PREFIXES get WhatsApp first (Prelude falls back to sms automatically when the number has no WhatsApp). Everyone else keeps sms, with a switch-channel link shown on the code screen after 20s. Old app builds send no channel field and keep the current sms-only behavior. Also removes the dead VERIFICATION_PROVIDER config and reports the delivery channel in the USER_LOGGED_IN metric.
📝 WalkthroughWalkthroughVerification initialization now supports SMS and WhatsApp channel selection. The backend resolves and propagates channels, persists them in verification state, reports them in metrics, and returns ChangesVerification channel delivery
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant PhoneNumberScreen
participant VerificationCodeScreen
participant initPhoneVerificationAtom
participant initVerificationHandler
participant PreludeService
PhoneNumberScreen->>initPhoneVerificationAtom: submit phone number with auto channel
initPhoneVerificationAtom->>initVerificationHandler: initialize verification
initVerificationHandler->>PreludeService: create verification with resolved channel
PreludeService-->>initVerificationHandler: verification SID
initVerificationHandler-->>VerificationCodeScreen: sentVia and verification ID
VerificationCodeScreen->>initPhoneVerificationAtom: resend via alternate channel
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryAdds WhatsApp as a preferred phone-verification channel with country-based routing.
Confidence Score: 3/5The PR should not merge until fallback deliveries stop being presented and measured as though Prelude used the preferred WhatsApp channel. Prelude may deliver an SMS after receiving a WhatsApp preference, but the service persists and returns the preference as sentVia, causing incorrect mobile guidance and conversion attribution. Files Needing Attention: apps/user-service/src/routes/login/handlers/initVerificationHandler.ts, apps/user-service/src/utils/prelude.ts, apps/mobile/src/components/LoginFlow/components/VerificationCodeScreen.tsx, apps/user-service/src/metrics.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant M as Mobile
participant U as User service
participant P as Prelude
participant R as Redis
participant X as Metrics
M->>U: initVerification(phone, auto)
U->>U: Resolve preferred channel by country
U->>P: create(preferred_channel)
P-->>M: Deliver via preferred channel or fallback
U->>R: Store verification and preferred channel
U-->>M: verificationId, expirationAt, sentVia
M->>U: verifyCode
U->>R: Carry channel into challenge state
M->>U: verifyChallenge
U->>X: USER_LOGGED_IN(country, channel)
Reviews (1): Last reviewed commit: "Add WhatsApp verification channel with p..." | Re-trigger Greptile |
| return new InitPhoneVerificationResponse({ | ||
| expirationAt: fromMilliseconds(expirationAt), | ||
| verificationId: verificationState.id, | ||
| sentVia: channel, |
There was a problem hiding this comment.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 945952d9fa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| timestamp: new Date(), | ||
| name: USER_LOGGED_IN, | ||
| attributes: {countryPrefix}, | ||
| attributes: {countryPrefix, channel: channel ?? 'sms'}, |
There was a problem hiding this comment.
Record the actual delivery channel in login metrics
When a WhatsApp-preferred dispatch falls back to SMS—as prelude.ts lines 68–70 explicitly expect—the stored channel remains whatsapp, so this line records the eventual login as WhatsApp-delivered. Consequently, USER_LOGGED_IN aggregates cannot accurately compare WhatsApp and SMS conversion and will misclassify every fallback; record the provider's actual selected channel if available, or label and analyze this value as the preferred-channel cohort instead.
Useful? React with 👍 / 👎.
📱 Preview on the staging appOpen Vexl (stage) → Account → Scan QR code and scan this: Preview link:
The preview only loads into staging builds with a matching runtime — if this PR changes native code, ship a new staging build first. To go back to the staging channel: debug screen → "Clear PR preview". |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/user-service/src/routes/login/domain.ts (1)
25-44: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
staticCodeVerificationnever persists the resolved channel, silently skewingUSER_LOGGED_INmetrics.Only the
twilioSmsVerificationvariant gainedchannel: Schema.optional(VerificationChannel);staticCodeVerificationdid not. SinceinitVerificationHandler's dummy-code branches compute and returnsentVia: channelbut can't persist it on this variant,verifyCodeHandler'sloginData.type === 'twilioSmsVerification' ? loginData.channel : undefinedalways yieldsundefinedfor dummy-code logins, andreportUserLoggedIninmetrics.tsthen falls back to'sms'regardless of the actually-resolved channel. Any dummy/test number under a WhatsApp-preferred prefix will be miscounted in metrics.🔧 Proposed fix to add and forward the channel field
Schema.Struct({ id: PhoneNumberVerificationId, type: Schema.Literal('staticCodeVerification'), code: Schema.String, phoneNumber: HashedPhoneNumber, countryPrefix: CountryPrefix, expiresAt: UnixMilliseconds, + // optional - states written before channel support was added lack it + channel: Schema.optional(VerificationChannel), })Then forward
channelin bothmakeStaticCodeVerificationStatecalls ininitVerificationHandler.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/user-service/src/routes/login/domain.ts` around lines 25 - 44, Add an optional VerificationChannel field to the staticCodeVerification variant of PhoneVerificationState, then update both makeStaticCodeVerificationState calls in initVerificationHandler to persist the resolved channel alongside the existing dummy-code state fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/user-service/src/utils/prelude.ts`:
- Around line 14-19: Update the PreludeOperations.createVerification contract
and initVerificationHandler flow to return the effective delivery channel
alongside the verification SID. Validate the returned channel with
Schema.decodeUnknown, then use that validated value for persisted channel state
and sentVia instead of the requested channel, ensuring WhatsApp fallback to SMS
is propagated correctly.
In `@packages/localization/en-base.json`:
- Around line 304-307: Remove the manually added verification-code keys from
en-base.json and add them through the repository’s localization workflow, using
base.json as the canonical source and regenerating/uploading the locale seed so
sibling *-base.json files remain workflow-generated.
---
Outside diff comments:
In `@apps/user-service/src/routes/login/domain.ts`:
- Around line 25-44: Add an optional VerificationChannel field to the
staticCodeVerification variant of PhoneVerificationState, then update both
makeStaticCodeVerificationState calls in initVerificationHandler to persist the
resolved channel alongside the existing dummy-code state fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 55f42bc1-9aea-4b49-849c-d3855f66f3b9
📒 Files selected for processing (22)
apps/mobile/src/components/LoginFlow/api/initPhoneVerificationAtom.tsapps/mobile/src/components/LoginFlow/components/PhoneNumberScreen.tsxapps/mobile/src/components/LoginFlow/components/VerificationCodeScreen.tsxapps/user-service/.env.testapps/user-service/src/__tests__/eraseUser/initEraseUser.test.tsapps/user-service/src/__tests__/login/initVerification.test.tsapps/user-service/src/configs.tsapps/user-service/src/metrics.tsapps/user-service/src/routes/eraseUser/handlers/initEraseUser.tsapps/user-service/src/routes/login/domain.tsapps/user-service/src/routes/login/handlers/initVerificationHandler.tsapps/user-service/src/routes/login/handlers/verifyChallengeHandler.tsapps/user-service/src/routes/login/handlers/verifyCodeHandler.tsapps/user-service/src/utils/prelude.tsapps/user-service/src/utils/smsVerificationUtils.tsdev.config.tsexample.env.localpackages/localization/base.jsonpackages/localization/en-base.jsonpackages/rest-api/src/services/user/contracts.tstooling/dev/secrets.tstooling/dev/services.ts
| export interface PreludeOperations { | ||
| createVerification: ( | ||
| phoneNumber: E164PhoneNumber, | ||
| requestHeaders: CommonHeaders | ||
| requestHeaders: CommonHeaders, | ||
| channel: VerificationChannel | ||
| ) => Effect.Effect<SmsVerificationSid, UnableToSendVerificationSmsError> |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
fd -a 'prelude\.ts$|verification.*\.ts$|sms.*\.ts$|whatsapp.*\.ts$' . | sed 's#^\./##' | head -100
echo "== prelude outline =="
ast-grep outline apps/user-service/src/utils/prelude.ts --view expanded || true
echo "== prelude relevant lines =="
cat -n apps/user-service/src/utils/prelude.ts | sed -n '1,180p'
echo "== createVerification usages =="
rg -n "createVerification|sentVia|VerificationChannel|SmsVerificationSid|PreludeOperations" -S apps packages . --glob '!node_modules' --glob '!dist' --glob '!build' | head -250Repository: vexl-it/vexl
Length of output: 31833
🌐 Web query:
Prelude.so verification.create response fields preferred_channel fallback channel WhatsApp SMS
💡 Result:
In the Prelude.so Verify v2 API, the verification.create endpoint supports preferred channel routing via the options object [1][2]. While there is no explicit fallback_channel parameter in the API request schema, Prelude employs intelligent, automatic fallback mechanisms that default to other available channels (such as SMS) if the preferred channel is unavailable [3][2]. Request Structure To specify a preferred channel when initiating a verification, include the preferred_channel field within the options object in your API request [2]. Example JSON request body: { "target": { "type": "phone_number", "value": "+30123456789" }, "options": { "preferred_channel": "whatsapp" } } Supported Channels You can specify several channels for the preferred_channel parameter, including but not limited to: - whatsapp [2] - sms [2] - rcs [2] - telegram [2] - viber [2] - voice [2] Functionality 1. Preferred Channel: When you set a preferred_channel, the system attempts to deliver the verification code through that specific channel first [2]. 2. Intelligent Fallback: If the requested channel is unavailable or delivery fails, Prelude automatically falls back to other available channels to ensure the message is delivered [2]. This process is managed by Prelude's routing engine, which evaluates real-time delivery conditions [2][4]. 3. Dashboard Configuration: In addition to API parameters, you can configure allowed channels and specific fallback strategies (e.g., cost-first or conversion-first) per market directly within the Prelude Dashboard [2]. For more granular control over message delivery, including explicit templates or media attachments, the Notify API also supports the preferred_channel parameter [5][3].
Citations:
- 1: https://github.com/prelude-so/python-sdk/blob/main/src/prelude_python_sdk/types/verification_create_params.py
- 2: https://prelude.so/blog/preferred-channel
- 3: https://docs.prelude.so/notify/v2/documentation/introduction
- 4: https://docs.prelude.so/introduction/concepts/multi-routing
- 5: https://docs.prelude.so/notify/v2/api-reference/send-a-message
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lock/package references =="
for f in package.json turbo.json pnpm-lock.yaml yarn.lock package-lock.json; do
[ -f "$f" ] && { echo "--- $f"; rg -n '"`@prelude.so/sdk`"|`@prelude`\.so/sdk' "$f" -C 3 || true; }
done
echo "== initVerificationHandler relevant lines =="
cat -n apps/user-service/src/routes/login/handlers/initVerificationHandler.ts | sed -n '130,225p'
echo "== smsVerificationUtils relevant lines =="
cat -n apps/user-service/src/utils/smsVerificationUtils.ts | sed -n '1,90p'
echo "== mobile VerificationCodeScreen relevant lines =="
cat -n apps/mobile/src/components/LoginFlow/components/VerificationCodeScreen.tsx | sed -n '90,135p'; cat -n apps/mobile/src/components/LoginFlow/components/VerificationCodeScreen.tsx | sed -n '300,335p'
echo "== tests expected channel logic =="
cat -n apps/user-service/src/__tests__/login/initVerification.test.ts | sed -n '1,155p'
echo "== precise JS behavior probe for requested channel assignment path =="
node - <<'JS'
function presentChannel(requested) {
// Mirrors the request-side channel resolver: if no channel requested or no country prefix, return sms.
return requested && requested.channel ? requested.channel : 'sms'
}
const cases = [
{requested: {channel: 'whatsapp'}},
{requested: {channel: 'sms'}},
{requested: null},
{requested: undefined},
]
for (const c of cases) {
console.log(JSON.stringify({ c, sentVia: presentChannel(c.requested) }))
}
JSRepository: vexl-it/vexl
Length of output: 17552
🌐 Web query:
Prelude.so Node SDK verification.create response object channel sent_channel actual channel
💡 Result:
In the Prelude.so Node.js SDK, the verification.create method does not directly return a response object containing a dedicated "sent_channel" or "actual_channel" field in its standard response object [1][2]. The verification.create response typically contains fields such as id, status, method, metadata, and request_id [2]. Prelude's Verify API is designed to manage the full verification lifecycle on "auto-pilot," automatically selecting the most appropriate channel (e.g., SMS, WhatsApp, Viber, etc.) based on the user's context and country, and managing fallbacks without requiring the client to specify or explicitly track the channel in the immediate response object [3][4]. If you need to know which channel was used, this information is typically delivered via webhook events sent to your configured callback URL [5][6][7]. The webhook events are triggered when verification events occur, such as delivery status changes, and would contain the specific details regarding the channel used for that attempt [5][7]. To inspect the raw HTTP response from the Prelude API—which may contain additional headers or metadata not mapped to the primary response object—you can use the.asResponse or.withResponse methods provided by the SDK [1][8]: const { data: verification, response: raw } = await client.verification.create({ target: { type: 'phone_number', value: '+30123456789' } }).withResponse; For specifying a preference during creation, the API parameters support a preferred_channel field (e.g., "sms", "whatsapp", "viber", "zalo", "telegram", "rcs"), which allows you to suggest a channel, though the system ultimately determines the best route [5][7].
Citations:
- 1: https://github.com/prelude-so/node-sdk
- 2: https://prelude.so/blog/send-sms-verification-code
- 3: https://prelude.so/archive/old-home
- 4: https://docs.prelude.so/verify/v2/documentation/introduction
- 5: https://gemdocs.org/gems/prelude-sdk/0.1.0/PreludeSDK/Models/VerificationCreateParams/Options.html
- 6: https://docs.prelude.so/verify/v2/api-reference/create-or-retry-a-verification
- 7: https://github.com/prelude-so/python-sdk/blob/main/src/prelude_python_sdk/types/verification_create_params.py
- 8: https://registry.npmjs.org/%40prelude.so%2Fsdk
🌐 Web query:
Prelude.so verification create response channel field "method" "channel"
💡 Result:
In the Prelude Verify v2 API, when creating a verification request via the POST /v2/verification endpoint, you can specify a preferred communication channel by using the preferred_channel field [1][2]. This field is typically nested within the request parameters (often under an options object depending on the specific SDK implementation) [3][1][2]. The preferred_channel field allows you to prioritize a specific delivery method for the verification code. If the chosen channel is unavailable, the system will automatically fall back to other configured and available channels [4]. Supported values for preferred_channel generally include [1][2]: - sms - rcs - whatsapp - viber - zalo - telegram Additionally, while preferred_channel controls the communication medium, the method field (also found in the options object) is used to specify the delivery mode, such as auto, voice, or message (which skips silent verification) [3][1]. To use these channels, ensure they are enabled within your Prelude Dashboard under the Verify API settings [5][6].
Citations:
- 1: https://github.com/prelude-so/python-sdk/blob/main/src/prelude_python_sdk/types/verification_create_params.py
- 2: https://gemdocs.org/gems/prelude-sdk/0.1.0/PreludeSDK/Models/VerificationCreateParams/Options/PreferredChannel.html
- 3: https://docs.prelude.so/verify/v2/documentation/integrations/auth0
- 4: https://docs.prelude.so/notify/v2/api-reference/send-a-message
- 5: https://docs.prelude.so/verify/v2/documentation/whatsapp
- 6: https://prelude.so/blog/send-sms-verification-code
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== local npm packages / sdk types =="
for p in node_modules/@prelude.so/sdk node_modules/@types/prelude.so node_modules/prelude.so; do
if [ -d "$p" ]; then
echo "--- $p"
find "$p" -maxdepth 4 -type f \( -name "*.d.ts" -o -name "*.ts" -o -name "*.js" -o -name "package.json" \) | head -80
else
echo "--- $p not present"
fi
done
echo "== all references to prelude sdk types =="
rg -n "verification/create|verificationCreate|PreferredChannel|VerificationCreateParams|withResponse|response.*channel|channel.*response|sent_channel|actual_channel" node_modules packages apps -S --glob '!node_modules/**' || true
echo "== prelude module resolution =="
node - <<'JS'
try {
console.log(require.resolve('`@prelude.so/sdk`'))
} catch (e) {
console.log('not resolvable:', e.code)
}
JSRepository: vexl-it/vexl
Length of output: 10106
Return or fetch Preldue’s actual delivery channel before publishing sentVia.
createVerification returns only a SID, and initVerificationHandler stores channel as both the persisted channel and sentVia. Since preferred_channel: 'whatsapp' can fallback to SMS, the API response can report WhatsApp when the actual delivery method is SMS, affecting resend flow and persisted channel state. Return the effective channel from the response, validate it with Schema.decodeUnknown, and propagate that value; otherwise don’t publish WhatsApp when SMS fallback occurred.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/user-service/src/utils/prelude.ts` around lines 14 - 19, Update the
PreludeOperations.createVerification contract and initVerificationHandler flow
to return the effective delivery channel alongside the verification SID.
Validate the returned channel with Schema.decodeUnknown, then use that validated
value for persisted channel state and sentVia instead of the requested channel,
ensuring WhatsApp fallback to SMS is propagated correctly.
| "loginFlow.v2.verificationCode.textWhatsapp": "We sent it via WhatsApp to", | ||
| "loginFlow.v2.verificationCode.whatsappFallbackHint": "Don’t use WhatsApp? The code will arrive as a text message instead.", | ||
| "loginFlow.v2.verificationCode.sendViaWhatsapp": "Didn’t get the code? Send it via WhatsApp", | ||
| "loginFlow.v2.verificationCode.sendViaSms": "Didn’t get the code? Send it via SMS", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Route this en-base.json change through the localization workflow.
These new keys are being added directly to a sibling *-base.json file. Keep base.json canonical and regenerate/upload this locale seed through the repository’s localization process rather than hand-editing it. As per coding guidelines and retrieved learnings, sibling *-base.json files may only be edited through the localization workflow.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/localization/en-base.json` around lines 304 - 307, Remove the
manually added verification-code keys from en-base.json and add them through the
repository’s localization workflow, using base.json as the canonical source and
regenerating/uploading the locale seed so sibling *-base.json files remain
workflow-generated.
Sources: Coding guidelines, Learnings
What
Login verification codes can now be delivered via WhatsApp (through Prelude's
preferred_channel), targeted per country, with SMS remaining the default everywhere else.How it works
packages/rest-api):InitPhoneVerificationRequestgains optionalchannel: 'auto' | 'sms' | 'whatsapp'; the response gainssentVia. Old app builds send nochanneland keep today's SMS-only behavior — no version gate needed.apps/user-service): newWHATSAPP_PREFERRED_PREFIXESenv var (comma-separated country prefixes, e.g.39for Italy).channel=autoresolves to WhatsApp for listed prefixes, SMS otherwise; an explicit channel (user tapped "send via …") is honored as-is. Prelude routes WhatsApp→SMS automatically when the number has no WhatsApp account, so no per-user detection is needed.auto. The code screen shows channel-aware copy (WhatsApp copy hedges about the SMS fallback), and after 20 s a link appears to re-send via the other channel. Resend keeps the current channel.USER_LOGGED_INnow includes the delivery channel, so WhatsApp vs SMS conversion can be compared per country to tune the prefix list.VERIFICATION_PROVIDERconfig (validated but never read).Rollout notes
WHATSAPP_PREFERRED_PREFIXESempty/unset — everything behaves exactly as today.base.json+en-base.json(en is type-required to be complete) — they need to be uploaded to Crowdin before the nextcrowdin download.channelfield; old in-flight states decode fine.Tests
apps/user-servicejest suite: 41/41 passing, including new channel-resolution cases (legacy/auto/explicit × prefix in/out of list).pnpm turbo:typecheck,turbo:format,turbo:lintall clean.Follow-ups (out of scope)
channelvalue (needs Prelude client SDK +dispatch_idplumbing).sidin Redis and logged in OTel span attributes inprelude.ts(pre-existing) — worth a separate fix.twilioErrors.ts(2.5 MB, zero importers).Summary by CodeRabbit
New Features
Documentation