Bug: [FEATURE] emit payment status before rendering the UI



### Feature Description

Payment link to emit the latest payment intent status before mounting the SDK and rendering UI.

Usecase - this can be used by merchants who embed these links as an iframe to take decision based on the latest intent status.

### Possible Implementation

Before mounting SDK or rendering the UI, emit an event via `postMessage` which can be listened on by the parent window.

Message format
```
{
  "payment": {
      "status": "IntentStatus"
    }
}
```

### Have you spent some time checking if this feature request has been raised before?

- [x] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [x] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index 27d7b82e547..840d4274709 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -7944,6 +7944,7 @@ pub struct PaymentLinkDetails {
     pub background_colour: Option<String>,
     pub sdk_ui_rules: Option<HashMap<String, HashMap<String, String>>>,
     pub payment_link_ui_rules: Option<HashMap<String, HashMap<String, String>>>,
+    pub status: api_enums::IntentStatus,
     pub enable_button_only_on_form_ready: bool,
 }
 
diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs
index c13e087947b..48004dd740b 100644
--- a/crates/router/src/core/payment_link.rs
+++ b/crates/router/src/core/payment_link.rs
@@ -191,7 +191,7 @@ pub async fn form_payment_link_data(
     let merchant_name = capitalize_first_char(&payment_link_config.seller_name);
     let payment_link_status = check_payment_link_status(session_expiry);
 
-    let is_terminal_state = check_payment_link_invalid_conditions(
+    let is_payment_link_terminal_state = check_payment_link_invalid_conditions(
         payment_intent.status,
         &[
             storage_enums::IntentStatus::Cancelled,
@@ -201,9 +201,11 @@ pub async fn form_payment_link_data(
             storage_enums::IntentStatus::RequiresMerchantAction,
             storage_enums::IntentStatus::Succeeded,
             storage_enums::IntentStatus::PartiallyCaptured,
+            storage_enums::IntentStatus::RequiresCustomerAction,
         ],
     );
-    if is_terminal_state || payment_link_status == api_models::payments::PaymentLinkStatus::Expired
+    if is_payment_link_terminal_state
+        || payment_link_status == api_models::payments::PaymentLinkStatus::Expired
     {
         let status = match payment_link_status {
             api_models::payments::PaymentLinkStatus::Active => {
@@ -211,7 +213,7 @@ pub async fn form_payment_link_data(
                 PaymentLinkStatusWrap::IntentStatus(payment_intent.status)
             }
             api_models::payments::PaymentLinkStatus::Expired => {
-                if is_terminal_state {
+                if is_payment_link_terminal_state {
                     logger::info!("displaying status page as the requested payment link has reached terminal state with payment status as {:?}", payment_intent.status);
                     PaymentLinkStatusWrap::IntentStatus(payment_intent.status)
                 } else {
@@ -292,6 +294,7 @@ pub async fn form_payment_link_data(
         payment_button_text_colour: payment_link_config.payment_button_text_colour.clone(),
         sdk_ui_rules: payment_link_config.sdk_ui_rules.clone(),
         payment_link_ui_rules: payment_link_config.payment_link_ui_rules.clone(),
+        status: payment_intent.status,
         enable_button_only_on_form_ready: payment_link_config.enable_button_only_on_form_ready,
     };
 
diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js
index e73e23076a9..5c6f3a6b373 100644
--- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js
+++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js
@@ -181,6 +181,30 @@ var hyper = null;
 
 const translations = getTranslations(window.__PAYMENT_DETAILS.locale);
 
+var isFramed = false;
+try {
+  isFramed = window.parent.location !== window.location;
+
+  // If parent's window object is restricted, DOMException is
+  // thrown which concludes that the webpage is iframed
+} catch (err) {
+  isFramed = true;
+}
+
+/**
+ * Trigger - on boot
+ * Use - emit latest payment status to parent window
+ */
+function emitPaymentStatus(paymentDetails) {
+  var message = {
+    payment: {
+      status: paymentDetails.status,
+    }
+  };
+
+  window.parent.postMessage(message, "*");
+}
+
 /**
  * Trigger - init function invoked once the script tag is loaded
  * Use
@@ -190,13 +214,16 @@ const translations = getTranslations(window.__PAYMENT_DETAILS.locale);
  *  - Initialize event listeners for updating UI on screen size changes
  *  - Initialize SDK
  **/
-
-
 function boot() {
 
   // @ts-ignore
   var paymentDetails = window.__PAYMENT_DETAILS;
 
+  // Emit latest payment status
+  if (isFramed) {
+    emitPaymentStatus(paymentDetails);
+  }
+
   if (paymentDetails.display_sdk_only) {
     hide(".checkout-page")
     var sdkDisplayWidth = document.querySelector('.hyper-checkout-sdk');
diff --git a/crates/router/src/core/payment_link/payment_link_status/status.css b/crates/router/src/core/payment_link/payment_link_status/status.css
index 113f1353138..65b7b11d956 100644
--- a/crates/router/src/core/payment_link/payment_link_status/status.css
+++ b/crates/router/src/core/payment_link/payment_link_status/status.css
@@ -137,6 +137,10 @@ body > div {
   font-size: 13px;
 }
 
+.hidden {
+  display: none;
+}
+
 .ellipsis-container-2 {
   height: 2.5em;
   overflow: hidden;
diff --git a/crates/router/src/core/payment_link/payment_link_status/status.html b/crates/router/src/core/payment_link/payment_link_status/status.html
index c78a0bb556d..7531c8df444 100644
--- a/crates/router/src/core/payment_link/payment_link_status/status.html
+++ b/crates/router/src/core/payment_link/payment_link_status/status.html
@@ -16,7 +16,7 @@
       {{ rendered_js }}
     </script>
   </head>
-  <body onload="boot()">
+  <body onload="boot()" class="hidden">
     <div>
       <div class="hyper-checkout-status-wrap">
         <div id="hyper-checkout-status-header"></div>
diff --git a/crates/router/src/core/payment_link/payment_link_status/status.js b/crates/router/src/core/payment_link/payment_link_status/status.js
index 8c9697fd53e..16949d546a6 100644
--- a/crates/router/src/core/payment_link/payment_link_status/status.js
+++ b/crates/router/src/core/payment_link/payment_link_status/status.js
@@ -52,8 +52,8 @@ function invertToBW(color, bw, asArr) {
       ? hexToRgbArray(options.black)
       : options.black
     : asArr
-    ? hexToRgbArray(options.white)
-    : options.white;
+      ? hexToRgbArray(options.white)
+      : options.white;
 }
 function invert(color, bw) {
   if (bw === void 0) {
@@ -87,6 +87,31 @@ window.state = {
 };
 
 const translations = getTranslations(window.__PAYMENT_DETAILS.locale);
+
+var isFramed = false;
+try {
+  isFramed = window.parent.location !== window.location;
+
+  // If parent's window object is restricted, DOMException is
+  // thrown which concludes that the webpage is iframed
+} catch (err) {
+  isFramed = true;
+}
+
+/**
+ * Trigger - on boot
+ * Use - emit latest payment status to parent window
+ */
+function emitPaymentStatus(paymentDetails) {
+  var message = {
+    payment: {
+      status: paymentDetails.status,
+    }
+  };
+
+  window.parent.postMessage(message, "*");
+}
+
 /**
  * Trigger - init function invoked once the script tag is loaded
  * Use
@@ -100,20 +125,43 @@ function boot() {
   // @ts-ignore
   var paymentDetails = window.__PAYMENT_DETAILS;
 
-  // Attach document icon
-  if (paymentDetails.merchant_logo) {
-    var link = document.createElement("link");
-    link.rel = "icon";
-    link.href = paymentDetails.merchant_logo;
-    link.type = "image/x-icon";
-    document.head.appendChild(link);
+  // Emit latest payment status
+  if (isFramed) {
+    emitPaymentStatus(paymentDetails);
   }
 
-  // Render status details
-  renderStatusDetails(paymentDetails);
+  if (shouldRenderUI(paymentDetails)) {
+    removeClass("body", "hidden");
+    // Attach document icon
+    if (paymentDetails.merchant_logo) {
+      var link = document.createElement("link");
+      link.rel = "icon";
+      link.href = paymentDetails.merchant_logo;
+      link.type = "image/x-icon";
+      document.head.appendChild(link);
+    }
+
+    // Render status details
+    renderStatusDetails(paymentDetails);
 
-  // Add event listeners
-  initializeEventListeners(paymentDetails);
+    // Add event listeners
+    initializeEventListeners(paymentDetails);
+  }
+}
+
+/**
+ * Trigger - on boot
+ * Use - Check if UI should be rendered based on some conditions
+ * @returns {Boolean}
+ */
+function shouldRenderUI(paymentDetails) {
+  var status = paymentDetails.status;
+  if (isFramed) {
+    switch (status) {
+      case "requires_customer_action": return false;
+    }
+  }
+  return true;
 }
 
 /**
@@ -158,6 +206,7 @@ function renderStatusDetails(paymentDetails) {
       ).toTimeString();
       break;
 
+    case "requires_customer_action":
     case "processing":
       statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/pending.png";
       statusDetails.message = translations.paymentTakingLonger;
@@ -279,7 +328,7 @@ function renderStatusDetails(paymentDetails) {
           var innerText =
             secondsLeft === 0
               ? translations.redirecting
-              : translations.redirectingIn + secondsLeft + " "+translations.seconds;
+              : translations.redirectingIn + secondsLeft + " " + translations.seconds;
           // @ts-ignore
           statusRedirectTextNode.innerText = innerText;
           if (secondsLeft === 0) {
@@ -341,5 +390,18 @@ function initializeEventListeners(paymentDetails) {
   if (statusRedirectTextNode instanceof HTMLDivElement) {
     statusRedirectTextNode.style.color = contrastBWColor;
   }
-  };
+};
 
+function addClass(id, className) {
+  var element = document.querySelector(id);
+  if (element instanceof HTMLElement) {
+    element.classList.add(className);
+  }
+}
+
+function removeClass(id, className) {
+  var element = document.querySelector(id);
+  if (element instanceof HTMLElement) {
+    element.classList.remove(className);
+  }
+}
\ No newline at end of file

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [ ] New feature
- [x] Enhancement
- [x] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
This PR adds functionality for payment links to
- emit latest payment intent status before rendering status UI or payment widget
- consider `requires_customer_action` as payment link's terminal state


### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
Helps merchants take action for `requires_customer_action` state in case user re-renders the link for the mentioned intent status.

## How did you test it?
Tested locally using an iframe.

<details>
    <summary>1. Use below template for payment links</summary>

```
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> -->
        <title>Document</title>
        <style>
            html, body {
                margin: 0;
                display: flex;
                align-items: center;
                justify-content: center;
                background-color: bisque;
                height: 100vh;
                width: 100vw;
            }
            iframe {
                border-radius: 4px;
                height: 90vh;
                width: 90vw;
                background-color: white;
            }
        </style>
    </head>
    <body>
        <iframe
            src="http://localhost:8080/payment_link/merchant_1742192403/pay_1aHs36MZD0zffE4bO538?locale=en"
            frameborder="0"></iframe>
    </body>
    <script>
        window.addEventListener("message", function(event) {
            if (event.origin === "http://localhost:8080") {
                var status = event.data.payment.status;
                switch (status) {
                    case "expired":
                    case "requires_customer_action": {
                        window.location.href = "https://google.com";
                    }
                }
            }
        });
    </script>
</html>
```

</details>

<details>
    <summary>2. Create a payment link</summary>

cURL

    curl --location --request POST 'http://localhost:8080/payments' \
        --header 'Content-Type: application/json' \
        --header 'Accept: application/json' \
        --header 'api-key: dev_4AE12pRff5kcWKXt1Dn3CfMCHOtgfXPyAO92s8kWae1xALFonMSs2hzpAGZ1ov6Q' \
        --data-raw '{"customer_id":"cus_ALQFPJO2ZOSIg4XOUIZo","profile_id":"pro_qukvQ1whBBb6pdKnzvvN","customer_acceptance":{"acceptance_type":"online","accepted_at":"1963-05-03T04:07:52.723Z","online":{"ip_address":"127.0.0.1","user_agent":"amet irure esse"}},"amount":100,"currency":"USD","payment_link":true,"capture_method":"automatic","email":"john.doe@example.co.in","session_expiry":100000,"return_url":"https://example.com","payment_link_config_id":"style1"}'
    
Response

    {"payment_id":"pay_yzSbFQNh8oJoa6cSkxQM","merchant_id":"merchant_1742192403","status":"requires_payment_method","amount":100,"net_amount":100,"shipping_cost":null,"amount_capturable":100,"amount_received":null,"connector":null,"client_secret":"pay_yzSbFQNh8oJoa6cSkxQM_secret_tPrHESV8PXuLApSxMxZj","created":"2025-03-17T08:06:06.305Z","currency":"USD","customer_id":"cus_ALQFPJO2ZOSIg4XOUIZo","customer":{"id":"cus_ALQFPJO2ZOSIg4XOUIZo","name":"John Nether","email":"john.doe@example.co.in","phone":"6168205362","phone_country_code":"+1"},"description":null,"refunds":null,"disputes":null,"mandate_id":null,"mandate_data":null,"setup_future_usage":null,"off_session":null,"capture_on":null,"capture_method":"automatic","payment_method":null,"payment_method_data":null,"payment_token":null,"shipping":null,"billing":null,"order_details":null,"email":"john.doe@example.co.in","name":"John Nether","phone":"6168205362","return_url":"https://example.com/","authentication_type":null,"statement_descriptor_name":null,"statement_descriptor_suffix":null,"next_action":null,"cancellation_reason":null,"error_code":null,"error_message":null,"unified_code":null,"unified_message":null,"payment_experience":null,"payment_method_type":null,"connector_label":null,"business_country":null,"business_label":"default","business_sub_label":null,"allowed_payment_method_types":null,"ephemeral_key":{"customer_id":"cus_ALQFPJO2ZOSIg4XOUIZo","created_at":1742198766,"expires":1742202366,"secret":"epk_db9da1a985644cd08c4bcb3bb152ebd1"},"manual_retry_allowed":null,"connector_transaction_id":null,"frm_message":null,"metadata":null,"connector_metadata":null,"feature_metadata":null,"reference_id":null,"payment_link":{"link":"http://localhost:8080/payment_link/merchant_1742192403/pay_yzSbFQNh8oJoa6cSkxQM?locale=en","secure_link":null,"payment_link_id":"plink_h1btlpLkVr0030VJXs1g"},"profile_id":"pro_qukvQ1whBBb6pdKnzvvN","surcharge_details":null,"attempt_count":1,"merchant_decision":null,"merchant_connector_id":null,"incremental_authorization_allowed":null,"authorization_count":null,"incremental_authorizations":null,"external_authentication_details":null,"external_3ds_authentication_attempted":false,"expires_on":"2025-03-18T11:52:46.283Z","fingerprint":null,"browser_info":null,"payment_method_id":null,"payment_method_status":null,"updated":"2025-03-17T08:06:06.355Z","split_payments":null,"frm_metadata":null,"extended_authorization_applied":null,"capture_before":null,"merchant_order_reference_id":null,"order_tax_amount":null,"connector_mandate_id":null,"card_discovery":null}

</details>

3. Update the payment link in above HTML template

##### Things to test

[expected behavior in both the cases - payment link should redirect to the link mentioned in HTML template]

- Make a redirection payment, and come back to the iframe
- Wait for payment link to be expired, and come back to the iframe

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
