Skip to content

feat: pix automatico#1435

Open
aritro2002 wants to merge 3 commits intomainfrom
pix-automatico
Open

feat: pix automatico#1435
aritro2002 wants to merge 3 commits intomainfrom
pix-automatico

Conversation

@aritro2002
Copy link
Copy Markdown
Contributor

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

This PR introduces comprehensive support for Pix Automatico payments (both QR and Push variants) in the Hyperswitch Web SDK by implementing a new wait_screen_information next action flow. This enables Brazilian merchants to offer seamless Pix payment experiences where users can complete transactions through their banking applications with real-time status polling and customizable waiting screen messaging.

What Changed

1. Wait Screen Information Flow Implementation

The core of this PR is the implementation of the wait_screen_information next action type, which provides a robust mechanism for handling asynchronous payment confirmations that require user action outside the SDK iframe.

Key Components:

  • Enhanced Type System: Extended the nextAction type to support poll_config with configurable delay_in_secs and frequency parameters, along with display_to_timestamp for timeout management.

  • Intelligent Polling Mechanism: Implemented a sophisticated polling system in pollRetrievePaymentIntent that:

    • Respects configurable delays between status checks
    • Uses nanosecond-precision timestamps for accurate timeout calculation
    • Gracefully handles timeout by performing a final status retrieval
    • Continues polling until either payment completion or timeout
  • Metadata-Driven Loader: The payment loader now receives payment method information through metadata, enabling contextual messaging based on the specific payment method being used.

Flow Architecture:

When a payment returns wait_screen_information as the next action:

  1. The SDK displays a fullscreen payment loader with method-specific messaging
  2. Begins polling the payment intent status at intervals defined by poll_config.delay_in_secs
  3. Each poll checks if the payment has reached a terminal state (succeeded or failed)
  4. Polling continues until either:
    • Payment status becomes terminal (success/failure)
    • Current time exceeds display_to_timestamp
  5. Upon timeout, a final retrievePaymentIntent call fetches the latest status
  6. The loader closes and the final result is communicated back to the merchant

2. Contextual Loader Messaging

A significant UX improvement in this PR is the introduction of payment-method-specific loader messages. Rather than showing generic "Processing payment" text for all flows, the loader now displays contextual information based on the payment method.

For Pix Automatico Push:

  • Title: "Please confirm the payment in your banking app"
  • Subtitle: "Open your banking application and authorize the payment request. The status will be updated automatically once confirmed."

This approach:

  • Sets clear expectations for users about what action they need to take
  • Reduces confusion by explaining the external authorization flow

How did you test it?

I have tested it locally by hardcoding confirm and PML response
PML

Screen.Recording.2026-03-23.at.5.13.33.pm.mov

Checklist

  • I ran npm run re:build
  • I reviewed submitted code
  • I added unit tests for my changes where possible

@aritro2002 aritro2002 self-assigned this Mar 23, 2026
@aritro2002 aritro2002 added the Ready for Review PR with label Ready for Review should only be reviewed. label Mar 23, 2026
@semanticdiff-com
Copy link
Copy Markdown

Review changes with  SemanticDiff

@aritro2002 aritro2002 linked an issue Mar 23, 2026 that may be closed by this pull request
]->getJsonFromArrayOfJson
resolve(response)
} else if intent.nextAction.type_ === "wait_screen_information" {
let displayToTimestamp = intent.nextAction.display_to_timestamp->Option.getOr(0.0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When display_to_timestamp is None, it defaults to 0.0. Since currentTime >= 0.0 is always true, the polling terminates immediately on the first check, defeating the purpose of the wait screen.

Suggested Fix:

let displayToTimestamp = intent.nextAction.display_to_timestamp->Option.getOr(
  Date.now() *. 1000000.0 +. 300000000000.0  // 5 minutes from now in nanoseconds
)

posted by PR reviewer bot

)
->ignore
}
resolve(data)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

resolve(data) executes immediately after starting the async polling chain (line 909), not waiting for polling to complete. The promise resolves before the polling result is known.

Suggested Fix:

if !isPaymentSession {
  let metaData =
    [("paymentMethod", paymentMethod->JSON.Encode.string)]->getJsonFromArrayOfJson
  messageParentWindow([
    ("fullscreen", true->JSON.Encode.bool),
    ("param", `paymentloader`->JSON.Encode.string),
    ("iframeId", iframeId->JSON.Encode.string),
    ("metadata", metaData),
  ])

  pollRetrievePaymentIntent(
    clientSecret,
    ~headers=headersDict,
    ~publishableKey=confirmParam.publishableKey,
    ~logger=optLogger->Option.getOr(LoggerUtils.defaultLoggerConfig),
    ~customPodUri,
    ~sdkAuthorization,
    ~delayInMs=pollConfig.delay_in_secs * 1000,
    ~endTimestamp=Some(displayToTimestamp),
  )
  ->Promise.then(retrievedData => {
    closePaymentLoaderIfAny()
    postSubmitResponse(~jsonData=retrievedData, ~url=url.href)
    resolve(retrievedData)
    Promise.resolve()
  })
  ->Promise.catch(_ => {
    closePaymentLoaderIfAny()
    postSubmitResponse(~jsonData=data, ~url=url.href)
    resolve(data)
    Promise.resolve()
  })
  ->ignore
} else {
  resolve(data)
}

posted by PR reviewer bot


let parsePaymentMethod = methodString => {
switch methodString {
| "pix_automatico_push" => PixAutomaticoPush
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The loader checks for "pix_automatico_push", but PaymentMethodsRecord.res and DynamicFieldsUtils.res use "pix_automatico_push_transfer". The loader will never match and always show generic "Other" text for Pix Automatico Push payments.

Suggested Fix:

let parsePaymentMethod = methodString => {
  switch methodString {
  | "pix_automatico_push_transfer" => PixAutomaticoPush
  | _ => Other
  }
}

posted by PR reviewer bot

reference: string,
}

type pollConfig = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The frequency field is parsed from the API response but never referenced in the polling logic. This is either dead code or a missing implementation.

Suggested Fix:
Either implement frequency-based polling limits or remove the field:

// If intended to limit poll count, add to pollRetrievePaymentIntent:
if pollConfig.frequency > 0 && currentAttempt >= pollConfig.frequency {
  // Stop polling, return current status
}

posted by PR reviewer bot


if status === "succeeded" || status === "failed" {
resolve(json)
switch endTimestamp {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Some(timestamp) and None branches contain ~50 lines of nearly identical polling logic, violating DRY principle.

Suggested Fix:

let rec pollRetrievePaymentIntent = (...) => {
  open Promise

  let isExpired = switch endTimestamp {
  | Some(timestamp) =>
    let currentTime = Date.now() *. 1000000.0
    currentTime >= timestamp
  | None => false
  }

  if isExpired {
    retrievePaymentIntent(...)
    ->then(json => resolve(json))
    ->catch(_ => resolve(JSON.Encode.null))
  } else {
    retrievePaymentIntent(...)
    ->then(json => {
      let dict = json->getDictFromJson
      let status = dict->getString("status", "")
      if status === "succeeded" || status === "failed" {
        resolve(json)
      } else {
        delay(delayInMs)
        ->then(_val => pollRetrievePaymentIntent(..., ~endTimestamp))
      }
    })
    ->catch(e => {
      Console.error2("Unable to retrieve payment due to following error", e)
      pollRetrievePaymentIntent(..., ~endTimestamp)
    })
  }
}

posted by PR reviewer bot


let getLoaderTextConfig = (paymentMethod: loaderPaymentMethod) => {
switch paymentMethod {
| PixAutomaticoPush => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

User-facing loader text is hardcoded in English, violating the codebase's i18n pattern used elsewhere (see PaymentMethodsRecord.res and locale files).

Suggested Fix:
Add to LocaleStringTypes.res:

type localeStrings = {
  ...
  loaderProcessingTitle: string,
  loaderProcessingSubtitle: string,
  loaderPixPushTitle: string,
  loaderPixPushSubtitle: string,
}

Then pass localeString prop to Loader component.

posted by PR reviewer bot

}
}

let getLoaderTextConfig = (paymentMethod: loaderPaymentMethod) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ReScript can infer the type from pattern matching; explicit annotation is unnecessary per best practices.

Suggested Fix:

let getLoaderTextConfig = paymentMethod => {

posted by PR reviewer bot

resolve(response)
} else if intent.nextAction.type_ === "wait_screen_information" {
let displayToTimestamp = intent.nextAction.display_to_timestamp->Option.getOr(0.0)
let pollConfig =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The default {delay_in_secs: 2, frequency: 0} duplicates PaymentConfirmTypes.defaultPollConfig.

Suggested Fix:

let pollConfig =
  intent.nextAction.poll_config->Option.getOr(PaymentConfirmTypes.defaultPollConfig)

posted by PR reviewer bot

->catch(e => {
Console.error2("Unable to retrieve payment due to following error", e)
pollRetrievePaymentIntent(
| None =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The None branch has no termination condition. If payment status never becomes "succeeded" or "failed", the function recurses indefinitely, causing a stack overflow or hanging the browser.

Suggested Fix:

| None =>
  // Add max retries or require endTimestamp to always be Some
  let maxRetries = 30
  let calculatedEndTimestamp = Date.now() *. 1000.0 +. maxRetries *. delayInMs
  pollRetrievePaymentIntent(
    clientSecret,
    ~headers,
    ~publishableKey,
    ~logger,
    ~customPodUri,
    ~isForceSync,
    ~sdkAuthorization,
    ~delayInMs,
    ~endTimestamp=Some(calculatedEndTimestamp),
  )

posted by PR reviewer bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for Review PR with label Ready for Review should only be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: pix automatico

2 participants