Skip to content

feat: handle the MFA inbound-SMS link in the host app - #119

Draft
CAMOBAP wants to merge 1 commit into
masterfrom
feat/sms-link-handling
Draft

CAMOBAP wants to merge 1 commit into
masterfrom
feat/sms-link-handling

Conversation

@CAMOBAP

@CAMOBAP CAMOBAP commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Handles the sms: link from the MFA inbound-SMS challenge: parses it and opens the messaging app
with the recipient and body prefilled.

Changes

  • hcaptchaSmsLink.js — parser for sms:/smsto: links. Behaviour ported from
    HCaptchaSmsLink.java in hcaptcha-android-sdk#279 (behaviour and test table, not the Java).
    Splits on ? before ; so subscriber params don't eat the body, skips empty query pairs
    (?&body=), decodes without form semantics so a literal + survives.
  • Hcaptcha.js — adds onOpenWindow alongside onShouldStartLoadWithRequest, both going
    through one handler. The challenge opens the link with target="_blank", which on Android goes
    to onCreateWindow and only surfaces as onOpenWindow. Verified on a Pixel 7: without it no JS
    callback fires and the URL leaves the app. Same applies to the hCaptcha policy links.
  • The body is re-encoded unchanged, so the one-time code stays byte-exact.
  • Linking.openURL's rejection message embeds the URL, so it's no longer forwarded through
    onMessage — the body carries the one-time code and host apps log that event.

Note: smsto: is not a useful alternative — WhatsApp claims sms: and smsto: identically, so
both show the same chooser.

Not verified

  • Live MFA challenge never run — testing used a page reproducing the link shapes, not an
    enterprise sitekey.
  • Body prefill unverified: Messages rejected the dummy recipient for both link shapes.
  • iOS untested on hardware. onOpenWindow makes iOS route _blank there instead of loading
    in the WebView; unhandled URLs now open externally.

Test plan

npm test (76 passing) and npm run lint are green. Both need __e2e__/host absent — when that
generated dir is present, jest picks up a second React and 32 tests fail on master too.

🤖 Generated with Claude Code

The challenge opens its `sms:` link with `target="_blank"`. On Android
`setSupportMultipleWindows` is always on in react-native-webview, so that
navigation goes to `WebChromeClient.onCreateWindow` and never reaches
`onShouldStartLoadWithRequest`. Measured on a Pixel 7: with no `onOpenWindow`
prop set, no JS callback fires at all and the URL is handed to the system,
which shows an app chooser listing WhatsApp - the app the challenge tells
users not to use. The same applies to the `https://www.hcaptcha.com` policy
links, which left the app the same way.

Route both callbacks through one handler and add `onOpenWindow`, which
recovers every `target="_blank"` case in JS.

Parse the link rather than passing it through. The recipient can carry
formatting a messaging app rejects, and the challenge emits an empty leading
query parameter (`?&body=`). Splitting on `?` before `;` keeps the body of an
RFC 5724 link with subscriber params, and percent-decoding is done without
form semantics so a literal `+` survives. The body is re-encoded unchanged so
the one-time code reaches the composer byte-exact.

The rejection message from `Linking.openURL` embeds the URL, and the body
carries the one-time code, so it is no longer forwarded to `onMessage`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ghost

ghost commented Oct 4, 2026 via email

Copy link
Copy Markdown

@CAMOBAP CAMOBAP changed the title fix: handle the MFA sms link on the callback the challenge actually uses feat: handle the MFA inbound-SMS link in the host app Oct 4, 2026
Comment thread hcaptchaSmsLink.js
while (start < url.length && url[start] === '/') {
start++;
}
const payload = url.slice(start);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Similar as the one in IOS and Android.

Copied and paste from the other PR comment
"I left a similar message to the IOS PR, we strip literal fragments before parsing and preserve %23. So sms:+15551234567#123?body=code changes the outtput recipient to +15551234567123."

Comment thread Hcaptcha.js
}

if (candidate.slice(0, 24) === 'https://www.hcaptcha.com') {
Linking.openURL(candidate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_blank privacy/terms links return through this branch before the fallback catch, leaving Linking.openURL() failures unhandled.

Comment thread Hcaptcha.js
return true;
}

if (candidate.toLowerCase().startsWith('sms:')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry I am a bit confused with this, if the smsto is to be supported can it use openSmslink() too or no? I was reading it yesterday and the parser accepts it but this branch bypasses it normalization and sms failure reporting.

it('handles target="_blank" links through onOpenWindow', () => {
// The challenge opens its sms link with target="_blank", which never reaches
// onShouldStartLoadWithRequest on Android.
const openURL = jest.spyOn(Linking, 'openURL').mockResolvedValue(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

How can we verify the body prefill and the returned challenege on mobile for both the IOS and android? I see it checking the outgoing URL but I don't see the messaging app test.

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