Skip to content

feat: add Redsys 3DS invoke_hidden_iframe flow via native module bridge#458

Open
ArushKapoorJuspay wants to merge 3 commits into
mainfrom
feat/redsys-3ds-native-module
Open

feat: add Redsys 3DS invoke_hidden_iframe flow via native module bridge#458
ArushKapoorJuspay wants to merge 3 commits into
mainfrom
feat/redsys-3ds-native-module

Conversation

@ArushKapoorJuspay

@ArushKapoorJuspay ArushKapoorJuspay commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Add support for the invoke_hidden_iframe next action type returned by the Hyperswitch backend for Redsys 3DS authentication. This flow was previously silently failing on mobile — falling through to the wildcard _ case in AllPaymentHooks.res, which called handleDefaultPaymentFlows with an empty redirect URL.

The implementation follows the native module bridge pattern (same architecture as Netcetera3dsModule.res), with a DOM-based web fallback for React Native Web.

Note: The native module @juspay-tech/react-native-hyperswitch-3ds-method does not exist yet. This is intentional — the current behavior gracefully degrades: the native module import throws → caught → returns "N" (3DS method not completed). This tells the backend to proceed with a full 3DS challenge flow, which is still valid per the 3DS spec. The native module will be implemented separately for iOS (WKWebView) and Android (WebView) in follow-up PRs.

Motivation

  • Redsys (a major Spanish payment processor) uses invoke_hidden_iframe for 3DS method calls
  • The backend returns next_action.type: "invoke_hidden_iframe" with iframe_data containing three_ds_method_url, three_ds_method_data, and method_key
  • The SDK must perform a hidden form POST to the 3DS method URL, detect completion (load event → "Y", 10s timeout → "N"), then call POST /payments/{id}/complete_authorize with threeds_method_comp_ind
  • Previously this silently failed on mobile — the action type fell through to the _ wildcard in handleApiRes, hitting handleDefaultPaymentFlows which tried a browser redirect with an empty URL

Implementation Details

Architecture: Native Module Bridge + Web Fallback

Two alternative approaches were prototyped and reviewed:

Criteria Approach 1 (WebView+Context) Approach 2 (Native Module) ← This PR
Pattern alignment New pattern (React Context bridge) Matches existing Netcetera3dsModule
Async model Context callbacks, cleanup-heavy Pure async/await
Blast radius Modifies NavigationRouter No root component changes
Web platform N/A (WebView only) Full DOM iframe fallback
Files changed 7 files, 360 insertions 5 files, 318 insertions
Verdict Rejected Recommended and shipped

Files Changed (5 files, 318 insertions)

  1. src/components/modules/ThreeDsMethodModule.res (NEW, 117 lines)

    • Native module binding: @juspay-tech/react-native-hyperswitch-3ds-method — calls performThreeDsMethodPost(url, data, methodKey, timeoutMs) on iOS (WKWebView) / Android (WebView)
    • Graceful fallback: If native module is not installed, returns "N" immediately (3DS proceeds without method completion — still valid per spec)
    • Web fallback (ReactNative.Platform.os == #web): Creates a hidden container div + iframe + form, performs POST, listens for 2nd load event (1st is blank iframe attach), 10s timeout → "N"
    • Bug fix: Uses setAttribute(el, "style", ...) instead of @set external setStyleCssText which compiles to broken bracket notation (elem["style.cssText"])
    • Bug fix: Tracks iframe load count — only treats 2nd+ load as completion (1st fires on DOM attachment)
    • Bug fix: Registers load listener BEFORE DOM attachment to prevent race condition
  2. src/hooks/Redsys3dsHooks.res (NEW, 145 lines)

    • useRedsys3dsFlow hook: Orchestrates the full Redsys 3DS flow
    • Step 1: Extract payment ID from clientSecret (split on _secret_)
    • Step 2: Call ThreeDsMethodModule.performThreeDsMethod (native or web)
    • Step 3: Call POST /payments/{id}/complete_authorize with threeds_method_comp_ind ("Y" or "N")
    • Step 4: Route response — succeeded/processing → success, requires_customer_action → browser redirect, else → error
    • Full logging: THREE_DS_METHOD_CALL (start), COMPLETE_AUTHORIZE_CALL_INIT + COMPLETE_AUTHORIZE_CALL (API call)
  3. src/hooks/AllPaymentHooks.res (+30 lines)

    • Added "invoke_hidden_iframe" case to handleApiRes switch (previously fell through to _ wildcard)
    • Parses iframeData from nextAction, delegates to useRedsys3dsFlow
    • Bridges labeled arguments from Redsys hook to outer useBrowserHook interface
  4. src/types/AllApiDataTypes/PaymentConfirmTypes.res (+25 lines)

    • Added iframeData record type: {threeDsMethodUrl, threeDsMethodData, methodKey}
    • JSON parser: getNextAction now extracts iframe_data from next_action dict
    • Default: methodKey: "threeDSMethodData" (matches web SDK reference)
  5. src/types/LoggerTypes.res (+6 lines)

    • Added THREE_DS_METHOD_CALL, COMPLETE_AUTHORIZE_CALL_INIT, COMPLETE_AUTHORIZE_CALL event types
    • Added COMPLETE_AUTHORIZE_CALL to getApiInitEvent mapping

3DS Method Flow

Backend returns: next_action.type = "invoke_hidden_iframe"
                 next_action.iframe_data = {three_ds_method_url, three_ds_method_data, method_key}
    ↓
SDK extracts iframeData from payment confirm response
    ↓
┌─── Native (iOS/Android) ──────────────────────────────────┐
│ Try: require("@juspay-tech/react-native-hyperswitch-3ds-  │
│       method").performThreeDsMethodPost(url, data, key, ms)│
│ Catch: Module not found → return "N"                       │
└────────────────────────────────────────────────────────────┘
    ↓ (if module not found, or on web platform)
┌─── Web Fallback (React Native Web) ──────────────────────┐
│ 1. Create hidden container div (offscreen, 0×0)          │
│ 2. Create iframe (name="threeDSMethodIframe")            │
│ 3. Register load listener BEFORE DOM attachment          │
│ 4. Append to document.body                               │
│ 5. Create form (POST to three_ds_method_url)             │
│ 6. Submit form targeting iframe                          │
│ 7. Wait for 2nd load event (1st is blank attach) → "Y"  │
│    OR 10s timeout → "N"                                  │
│ 8. Cleanup: remove container, clear timeout              │
└──────────────────────────────────────────────────────────┘
    ↓
POST /payments/{id}/complete_authorize
  body: { threeds_method_comp_ind: "Y" | "N" }
    ↓
Route response: succeeded → success callback
                requires_customer_action → browser redirect (3DS challenge)
                other → error callback

Testing

  • ReScript compilation passes (npm run re:check with -warn-error +a-4-9)
  • Security scan passes (gitleaks)
  • Web fallback testable via React Native Web dev server
  • Native path requires @juspay-tech/react-native-hyperswitch-3ds-method module (not yet implemented — gracefully returns "N")
  • End-to-end testing requires Redsys sandbox connector configuration

- Replace @set 'style.cssText' (compiles to bracket notation) with
  setAttribute('style', ...) for proper DOM styling
- Track iframe load count to prevent premature 'Y' completion on
  initial blank iframe attachment (2nd load = form POST response)
@ArushKapoorJuspay ArushKapoorJuspay added the ai-generated PR generated by AI. Requires human review for correctness and security. label Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated PR generated by AI. Requires human review for correctness and security.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant