Bug: [BUG] Improper use of innerHTML in payment links



Payment links make use of innerHTML property for rendering a bunch a user's text based inputs.

This is considered unsafe since innerHTML evaluates the input as HTML and can lead to cross side scripting.

Update this to use textContent rather than using innerHTML for rendering text based user inputs.
diff --git a/crates/hyperswitch_domain_models/src/api.rs b/crates/hyperswitch_domain_models/src/api.rs
index 571371dc63c..f6d93497627 100644
--- a/crates/hyperswitch_domain_models/src/api.rs
+++ b/crates/hyperswitch_domain_models/src/api.rs
@@ -72,7 +72,7 @@ pub enum PaymentLinkAction {
 pub struct PaymentLinkFormData {
     pub js_script: String,
     pub css_script: String,
-    pub sdk_url: String,
+    pub sdk_url: url::Url,
     pub html_meta_tags: String,
 }
 
@@ -127,7 +127,7 @@ pub struct GenericExpiredLinkData {
 pub struct GenericLinkFormData {
     pub js_data: String,
     pub css_data: String,
-    pub sdk_url: String,
+    pub sdk_url: url::Url,
     pub html_meta_tags: String,
 }
 
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index fb40451888f..8e1fb35ddce 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -444,9 +444,19 @@ impl Default for GenericLinkEnvUiConfig {
     }
 }
 
-#[derive(Debug, Deserialize, Clone, Default)]
+#[derive(Debug, Deserialize, Clone)]
 pub struct PaymentLink {
-    pub sdk_url: String,
+    pub sdk_url: url::Url,
+}
+
+impl Default for PaymentLink {
+    fn default() -> Self {
+        Self {
+            #[allow(clippy::expect_used)]
+            sdk_url: url::Url::parse("https://beta.hyperswitch.io/v0/HyperLoader.js")
+                .expect("Failed to parse default SDK URL"),
+        }
+    }
 }
 
 #[derive(Debug, Deserialize, Clone, Default)]
diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs
index adcfe8046de..545966968bd 100644
--- a/crates/router/src/core/payment_link.rs
+++ b/crates/router/src/core/payment_link.rs
@@ -387,12 +387,11 @@ pub async fn initiate_secure_payment_link_flow(
                 payment_form_label_type: payment_link_config.payment_form_label_type,
                 show_card_terms: payment_link_config.show_card_terms,
             };
-            let js_script = format!(
-                "window.__PAYMENT_DETAILS = {}",
-                serde_json::to_string(&secure_payment_link_details)
-                    .change_context(errors::ApiErrorResponse::InternalServerError)
-                    .attach_printable("Failed to serialize PaymentLinkData")?
-            );
+            let payment_details_str = serde_json::to_string(&secure_payment_link_details)
+                .change_context(errors::ApiErrorResponse::InternalServerError)
+                .attach_printable("Failed to serialize PaymentLinkData")?;
+            let url_encoded_str = urlencoding::encode(&payment_details_str);
+            let js_script = format!("window.__PAYMENT_DETAILS = '{}';", url_encoded_str);
             let html_meta_tags = get_meta_tags_html(&link_details);
             let payment_link_data = services::PaymentLinkFormData {
                 js_script,
@@ -491,7 +490,8 @@ fn get_js_script(payment_details: &PaymentLinkData) -> RouterResult<String> {
     let payment_details_str = serde_json::to_string(payment_details)
         .change_context(errors::ApiErrorResponse::InternalServerError)
         .attach_printable("Failed to serialize PaymentLinkData")?;
-    Ok(format!("window.__PAYMENT_DETAILS = {payment_details_str};"))
+    let url_encoded_str = urlencoding::encode(&payment_details_str);
+    Ok(format!("window.__PAYMENT_DETAILS = '{url_encoded_str}';"))
 }
 
 fn get_color_scheme_css(payment_link_config: &PaymentLinkConfig) -> String {
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 bdfdfa8a8bf..1e96731402f 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
@@ -1,9 +1,19 @@
-// @ts-nocheck
+// @ts-check
 
 /**
  * UTIL FUNCTIONS
  */
 
+function decodeUri(uri) {
+  try {
+    var uriStr = decodeURIComponent(uri);
+    return JSON.parse(uriStr);
+  } catch (e) {
+    console.error("Error decoding and parsing string URI:", e);
+    return uri;
+  }
+}
+
 function adjustLightness(hexColor, factor) {
   // Convert hex to RGB
   var r = parseInt(hexColor.slice(1, 3), 16);
@@ -176,10 +186,13 @@ window.state = {
 var widgets = null;
 var unifiedCheckout = null;
 // @ts-ignore
-var pub_key = window.__PAYMENT_DETAILS.pub_key;
+var encodedPaymentDetails = window.__PAYMENT_DETAILS;
+var paymentDetails = decodeUri(encodedPaymentDetails);
+var pub_key = paymentDetails.pub_key;
 var hyper = null;
 
-const translations = getTranslations(window.__PAYMENT_DETAILS.locale);
+// @ts-ignore
+const translations = getTranslations(paymentDetails.locale);
 
 var isFramed = false;
 try {
@@ -215,10 +228,6 @@ function emitPaymentStatus(paymentDetails) {
  *  - Initialize SDK
  **/
 function boot() {
-
-  // @ts-ignore
-  var paymentDetails = window.__PAYMENT_DETAILS;
-
   // Emit latest payment status
   if (isFramed) {
     emitPaymentStatus(paymentDetails);
@@ -435,7 +444,7 @@ function initializeEventListeners(paymentDetails) {
   if (paymentForm instanceof HTMLFormElement) {
     paymentForm.addEventListener("submit", function (event) {
       event.preventDefault();
-      handleSubmit(event);
+      handleSubmit(event, paymentDetails);
     })
   }
 
@@ -447,18 +456,20 @@ function initializeEventListeners(paymentDetails) {
 function handleFormReadyForSubmission() {
   window.addEventListener("message", function (event) {
     // Event listener for updating the button rules
-    if (isObject(event.data) && event.data["isFormReadyForSubmission"] !== null) {
-      let isFormReadyForSubmission = event.data["isFormReadyForSubmission"];
-      var submitButtonNode = document.getElementById("submit");
-      if (submitButtonNode instanceof HTMLButtonElement) {
-        if (isFormReadyForSubmission === false) {
-          submitButtonNode.disabled = true;
-          addClass("#submit", "not-ready");
-          addClass("#submit", "disabled");
-        } else if (isFormReadyForSubmission === true) {
-          submitButtonNode.disabled = false;
-          removeClass("#submit", "not-ready");
-          removeClass("#submit", "disabled");
+    if (event.origin == "{{sdk_origin}}") {
+      if (isObject(event.data) && event.data["isFormReadyForSubmission"] !== null) {
+        let isFormReadyForSubmission = event.data["isFormReadyForSubmission"];
+        var submitButtonNode = document.getElementById("submit");
+        if (submitButtonNode instanceof HTMLButtonElement) {
+          if (isFormReadyForSubmission === false) {
+            submitButtonNode.disabled = true;
+            addClass("#submit", "not-ready");
+            addClass("#submit", "disabled");
+          } else if (isFormReadyForSubmission === true) {
+            submitButtonNode.disabled = false;
+            removeClass("#submit", "not-ready");
+            removeClass("#submit", "disabled");
+          }
         }
       }
     }
@@ -504,12 +515,10 @@ function mountUnifiedCheckout(id) {
  *    - Toggle UI loaders appropriately
  *    - Handle errors and redirect to status page
  * @param {Event} e
+ * @param {PaymentDetails} paymentDetails
  */
 // @ts-ignore
-function handleSubmit(e) {
-  // @ts-ignore
-  var paymentDetails = window.__PAYMENT_DETAILS;
-
+function handleSubmit(e, paymentDetails) {
   // Update button loader
   hide("#submit-button-text");
   show("#submit-spinner");
@@ -553,7 +562,7 @@ function handleSubmit(e) {
         url.search = params.toString();
         window.top.location.href = url.toString();
       } else {
-        redirectToStatus();
+        redirectToStatus(paymentDetails);
       }
     })
     .catch(function (error) {
@@ -772,25 +781,33 @@ function appendMerchantDetails(paymentDetails, merchantDynamicDetails) {
       );
       var horizontalLine = document.createElement("hr");
       horizontalLine.className = "hyper-checkout-payment-horizontal-line";
-      horizontalLineContainer.append(horizontalLine);
+      if (horizontalLineContainer instanceof HTMLDivElement) {
+        horizontalLineContainer.append(horizontalLine);
+      }
 
       // max number of items to show in the merchant details
       let maxItemsInDetails = 50;
       for (var item of merchantDetailsObject) {
         var merchantData = document.createElement("div");
         merchantData.className = "hyper-checkout-payment-merchant-dynamic-data";
+        var keyNode = document.createElement("span");
+        keyNode.textContent = item.key;
+        var valueNode = document.createElement("span");
+        valueNode.textContent = item.value;
         // make the key and value bold if specified in the ui_configuration
-        var key = item.ui_configuration
-          ? item.ui_configuration.is_key_bold
-            ? item.key.bold()
-            : item.key
-          : item.key;
-        var value = item.ui_configuration
-          ? item.ui_configuration.is_value_bold
-            ? item.value.bold()
-            : item.value
-          : item.value;
-        merchantData.innerHTML = key + " : " + value;
+        if (isObject(item.ui_configuration)) {
+          if (item.ui_configuration.is_key_bold) {
+            keyNode.style.fontWeight = "bold";
+          }
+          if (item.ui_configuration.is_value_bold) {
+            valueNode.style.fontWeight = "bold";
+          }
+        }
+        var separatorNode = document.createElement("span");
+        separatorNode.textContent = " : ";
+        merchantData.appendChild(keyNode);
+        merchantData.appendChild(separatorNode);
+        merchantData.appendChild(valueNode);
 
         merchantDynamicDetails.append(merchantData);
         if (--maxItemsInDetails === 0) {
diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js
index effd27cc64a..33758131aa2 100644
--- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js
+++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js
@@ -9,7 +9,8 @@
  **/
 function initializeSDK() {
   // @ts-ignore
-  var paymentDetails = window.__PAYMENT_DETAILS;
+  var encodedPaymentDetails = window.__PAYMENT_DETAILS;
+  var paymentDetails = decodeUri(encodedPaymentDetails);
   var clientSecret = paymentDetails.client_secret;
   var sdkUiRules = paymentDetails.sdk_ui_rules;
   var labelType = paymentDetails.payment_form_label_type;
@@ -99,8 +100,7 @@ function initializeSDK() {
 /**
  * Use - redirect to /payment_link/status
  */
-function redirectToStatus() {
-  var paymentDetails = window.__PAYMENT_DETAILS;
+function redirectToStatus(paymentDetails) {
   var arr = window.location.pathname.split("/");
 
   // NOTE - This code preserves '/api' in url for integ and sbx
diff --git a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js
index 446428e6ff3..3db5eb94de2 100644
--- a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js
+++ b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js
@@ -123,8 +123,7 @@ if (!isFramed) {
   /**
    * Use - redirect to /payment_link/status
    */
-  function redirectToStatus() {
-    var paymentDetails = window.__PAYMENT_DETAILS;
+  function redirectToStatus(paymentDetails) {
     var arr = window.location.pathname.split("/");
 
     // NOTE - This code preserves '/api' in url for integ and sbx envs
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 16949d546a6..371fdf1d2df 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
@@ -4,6 +4,16 @@
  * UTIL FUNCTIONS
  */
 
+function decodeUri(uri) {
+  try {
+    var uriStr = decodeURIComponent(uri);
+    return JSON.parse(uriStr);
+  } catch (e) {
+    console.error("Error decoding and parsing string URI:", e);
+    return uri;
+  }
+}
+
 /**
  * Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js
  */
@@ -86,7 +96,12 @@ window.state = {
   isMobileView: window.innerWidth <= 1400,
 };
 
-const translations = getTranslations(window.__PAYMENT_DETAILS.locale);
+// @ts-ignore
+var encodedPaymentDetails = window.__PAYMENT_DETAILS;
+var paymentDetails = decodeUri(encodedPaymentDetails);
+
+// @ts-ignore
+const translations = getTranslations(paymentDetails.locale);
 
 var isFramed = false;
 try {
@@ -122,9 +137,6 @@ function emitPaymentStatus(paymentDetails) {
  *  - Initialize SDK
  **/
 function boot() {
-  // @ts-ignore
-  var paymentDetails = window.__PAYMENT_DETAILS;
-
   // Emit latest payment status
   if (isFramed) {
     emitPaymentStatus(paymentDetails);
@@ -272,7 +284,7 @@ function renderStatusDetails(paymentDetails) {
   var merchantLogoNode = document.createElement("img");
   merchantLogoNode.className = "hyper-checkout-status-merchant-logo";
   // @ts-ignore
-  merchantLogoNode.src = window.__PAYMENT_DETAILS.merchant_logo;
+  merchantLogoNode.src = paymentDetails.merchant_logo;
   merchantLogoNode.alt = "";
 
   // Form content items
diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs
index 4a2b3d75204..7e13dbbed16 100644
--- a/crates/router/src/core/payment_methods.rs
+++ b/crates/router/src/core/payment_methods.rs
@@ -400,7 +400,7 @@ pub async fn render_pm_collect_link(
                 let generic_form_data = services::GenericLinkFormData {
                     js_data: serialized_js_content,
                     css_data: serialized_css_content,
-                    sdk_url: default_config.sdk_url.to_string(),
+                    sdk_url: default_config.sdk_url.clone(),
                     html_meta_tags: String::new(),
                 };
                 Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs
index 063529e21fe..06edca897dc 100644
--- a/crates/router/src/core/payout_link.rs
+++ b/crates/router/src/core/payout_link.rs
@@ -260,7 +260,7 @@ pub async fn initiate_payout_link(
             let generic_form_data = services::GenericLinkFormData {
                 js_data: serialized_js_content,
                 css_data: serialized_css_content,
-                sdk_url: default_config.sdk_url.to_string(),
+                sdk_url: default_config.sdk_url.clone(),
                 html_meta_tags: String::new(),
             };
             Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs
index 76a2b9e50e6..2ae0c68ccef 100644
--- a/crates/router/src/services/api.rs
+++ b/crates/router/src/services/api.rs
@@ -1877,6 +1877,30 @@ fn build_payment_link_template(
     let _ = tera.add_raw_template("payment_link_js", &js_template);
 
     context.insert("payment_details_js_script", &payment_link_data.js_script);
+    let sdk_origin = payment_link_data
+        .sdk_url
+        .host_str()
+        .ok_or_else(|| {
+            logger::error!("Host missing for payment link SDK URL");
+            report!(errors::ApiErrorResponse::InternalServerError)
+        })
+        .and_then(|host| {
+            if host == "localhost" {
+                let port = payment_link_data.sdk_url.port().ok_or_else(|| {
+                    logger::error!("Port missing for localhost in SDK URL");
+                    report!(errors::ApiErrorResponse::InternalServerError)
+                })?;
+                Ok(format!(
+                    "{}://{}:{}",
+                    payment_link_data.sdk_url.scheme(),
+                    host,
+                    port
+                ))
+            } else {
+                Ok(format!("{}://{}", payment_link_data.sdk_url.scheme(), host))
+            }
+        })?;
+    context.insert("sdk_origin", &sdk_origin);
 
     let rendered_js = match tera.render("payment_link_js", &context) {
         Ok(rendered_js) => rendered_js,
@@ -1954,11 +1978,11 @@ pub fn build_secure_payment_link_html(
         .attach_printable("Error while rendering secure payment link's HTML template")
 }
 
-fn get_hyper_loader_sdk(sdk_url: &str) -> String {
+fn get_hyper_loader_sdk(sdk_url: &url::Url) -> String {
     format!("<script src=\"{sdk_url}\" onload=\"initializeSDK()\"></script>")
 }
 
-fn get_preload_link_html_template(sdk_url: &str) -> String {
+fn get_preload_link_html_template(sdk_url: &url::Url) -> String {
     format!(
         r#"<link rel="preload" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800" as="style">
             <link rel="preload" href="{sdk_url}" as="script">"#,

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

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

## Description
This PR addresses below issues specifically for payment links
- improper use of innerHTML for API inputs - for rendering transaction details
- improper use of receiving event (isFormReadyForSubmission) from HS SDK

Overall, this PR sanitizes API text based inputs which are rendered in the payment link UI using URI encoding before rendering the payment link HTML response. The URI encoded data is decoded in JS runtime (browser) and consumed whenever required.

### 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
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
Tested locally

<details>
    <summary>1. Create payment links and process redirection based txns (3DS)</summary>

cURL

    curl --location --request POST 'http://localhost:8080/payments' \
        --header 'Content-Type: application/json' \
        --header 'Accept: application/json' \
        --header 'api-key: dev_ML2p8kRGcMvi4oQ3cPnIfz5R2rO31efjWKXzgMnbRCnj5pUpjiki8K2HONlmMAb4' \
        --data-raw '{"authentication_type":"three_ds","customer_id":"cus_9SCrW9x19m2WbGYHprJS","profile_id":"pro_jZ4ix0hYxn8qi2NSSYih","amount":100,"currency":"EUR","payment_link":true,"setup_future_usage":"off_session","capture_method":"automatic","billing":{"address":{"line1":"1467","line2":"Harrison Street","line3":"Harrison Street","city":"San Fransico","state":"CA","zip":"94122","country":"NL","first_name":"John","last_name":"Doe"},"phone":{"number":"8056594427","country_code":"+91"},"email":"random@juspay.in"},"return_url":"https://example.com","payment_link_config":{"theme":"#003264","skip_status_screen":true,"transaction_details":[{"key":"a","value":"<script href=\"https://www.somesite.com\"></script>","ui_configuration":{"is_key_bold":true,"is_value_bold":true}}],"background_image":{"url":"https://img.freepik.com/free-photo/abstract-blue-geometric-shapes-background_24972-1841.jpg","position":"bottom","size":"cover"}}}'
    
Response

    {"payment_id":"pay_DrqIRGsc6hs4BP3GVe56","merchant_id":"merchant_1743758708","status":"requires_payment_method","amount":100,"net_amount":100,"shipping_cost":null,"amount_capturable":100,"amount_received":null,"connector":null,"client_secret":"pay_DrqIRGsc6hs4BP3GVe56_secret_5g2zaDCBZlK5ebBSQnF7","created":"2025-04-07T10:11:02.890Z","currency":"EUR","customer_id":"cus_9SCrW9x19m2WbGYHprJS","customer":{"id":"cus_9SCrW9x19m2WbGYHprJS","name":null,"email":null,"phone":null,"phone_country_code":null},"description":null,"refunds":null,"disputes":null,"mandate_id":null,"mandate_data":null,"setup_future_usage":"off_session","off_session":null,"capture_on":null,"capture_method":"automatic","payment_method":null,"payment_method_data":null,"payment_token":null,"shipping":null,"billing":{"address":{"city":"San Fransico","country":"NL","line1":"1467","line2":"Harrison Street","line3":"Harrison Street","zip":"94122","state":"CA","first_name":"John","last_name":"Doe"},"phone":{"number":"8056594427","country_code":"+91"},"email":"random@juspay.in"},"order_details":null,"email":null,"name":null,"phone":null,"return_url":"https://example.com/","authentication_type":"three_ds","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_9SCrW9x19m2WbGYHprJS","created_at":1744020662,"expires":1744024262,"secret":"epk_e9bec308a59f4550adf8dd6b7b57506f"},"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_1743758708/pay_DrqIRGsc6hs4BP3GVe56?locale=en","secure_link":"http://localhost:8080/payment_link/s/merchant_1743758708/pay_DrqIRGsc6hs4BP3GVe56?locale=en","payment_link_id":"plink_qOUNfvUvtkTK7K8sXBoU"},"profile_id":"pro_jZ4ix0hYxn8qi2NSSYih","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-04-07T10:26:02.875Z","fingerprint":null,"browser_info":null,"payment_method_id":null,"payment_method_status":null,"updated":"2025-04-07T10:11:02.944Z","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,"issuer_error_code":null,"issuer_error_message":null}

Verify below things

1. Redirection works as expected
2. Payment journey is as expected

</details>

<details>
    <summary>2. Create payment links and process redirection based txns (3DS)</summary>

cURL

    curl --location --request POST 'http://localhost:8080/payments' \
        --header 'Content-Type: application/json' \
        --header 'Accept: application/json' \
        --header 'api-key: dev_ML2p8kRGcMvi4oQ3cPnIfz5R2rO31efjWKXzgMnbRCnj5pUpjiki8K2HONlmMAb4' \
        --data-raw '{"authentication_type":"three_ds","customer_id":"cus_9SCrW9x19m2WbGYHprJS","profile_id":"pro_jZ4ix0hYxn8qi2NSSYih","amount":100,"currency":"EUR","payment_link":true,"setup_future_usage":"off_session","capture_method":"automatic","billing":{"address":{"line1":"1467","line2":"Harrison Street","line3":"Harrison Street","city":"San Fransico","state":"CA","zip":"94122","country":"NL","first_name":"John","last_name":"Doe"},"phone":{"number":"8056594427","country_code":"+91"},"email":"random@juspay.in"},"return_url":"https://example.com","payment_link_config":{"theme":"#003264","skip_status_screen":true,"transaction_details":[{"key":"a","value":"<script href=\"https://www.somesite.com\"></script>","ui_configuration":{"is_key_bold":true,"is_value_bold":true}}],"background_image":{"url":"https://img.freepik.com/free-photo/abstract-blue-geometric-shapes-background_24972-1841.jpg","position":"bottom","size":"cover"}}}'
    
Response

    {"payment_id":"pay_Gw2avLXsTOJUSvESS1ig","merchant_id":"merchant_1743758708","status":"requires_payment_method","amount":100,"net_amount":100,"shipping_cost":null,"amount_capturable":100,"amount_received":null,"connector":null,"client_secret":"pay_Gw2avLXsTOJUSvESS1ig_secret_XthFTGPAmHOvIG4wf0lf","created":"2025-04-07T10:12:36.809Z","currency":"EUR","customer_id":"cus_9SCrW9x19m2WbGYHprJS","customer":{"id":"cus_9SCrW9x19m2WbGYHprJS","name":null,"email":null,"phone":null,"phone_country_code":null},"description":null,"refunds":null,"disputes":null,"mandate_id":null,"mandate_data":null,"setup_future_usage":"off_session","off_session":null,"capture_on":null,"capture_method":"automatic","payment_method":null,"payment_method_data":null,"payment_token":null,"shipping":null,"billing":{"address":{"city":"San Fransico","country":"NL","line1":"1467","line2":"Harrison Street","line3":"Harrison Street","zip":"94122","state":"CA","first_name":"John","last_name":"Doe"},"phone":{"number":"8056594427","country_code":"+91"},"email":"random@juspay.in"},"order_details":null,"email":null,"name":null,"phone":null,"return_url":"https://example.com/","authentication_type":"three_ds","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_9SCrW9x19m2WbGYHprJS","created_at":1744020756,"expires":1744024356,"secret":"epk_4baa462e06a34161875cda39626e0612"},"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_1743758708/pay_Gw2avLXsTOJUSvESS1ig?locale=en","secure_link":"http://localhost:8080/payment_link/s/merchant_1743758708/pay_Gw2avLXsTOJUSvESS1ig?locale=en","payment_link_id":"plink_kN6v8N8kffrl3cdX3sf7"},"profile_id":"pro_jZ4ix0hYxn8qi2NSSYih","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-04-07T10:27:36.806Z","fingerprint":null,"browser_info":null,"payment_method_id":null,"payment_method_status":null,"updated":"2025-04-07T10:12:36.831Z","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,"issuer_error_code":null,"issuer_error_message":null}

Verify below things

1. Payment journey is as expected

</details>


<details>
    <summary>3. Give improper data for the text fields to be rendered in transaction_details</summary>

cURL

    curl --location --request POST 'http://localhost:8080/payments' \
        --header 'Content-Type: application/json' \
        --header 'Accept: application/json' \
        --header 'api-key: dev_ML2p8kRGcMvi4oQ3cPnIfz5R2rO31efjWKXzgMnbRCnj5pUpjiki8K2HONlmMAb4' \
        --data-raw '{"authentication_type":"three_ds","customer_id":"cus_9SCrW9x19m2WbGYHprJS","profile_id":"pro_jZ4ix0hYxn8qi2NSSYih","amount":100,"currency":"EUR","payment_link":true,"setup_future_usage":"off_session","capture_method":"automatic","billing":{"address":{"line1":"1467","line2":"Harrison Street","line3":"Harrison Street","city":"San Fransico","state":"CA","zip":"94122","country":"NL","first_name":"John","last_name":"Doe"},"phone":{"number":"8056594427","country_code":"+91"},"email":"random@juspay.in"},"return_url":"https://example.com","payment_link_config":{"theme":"#003264","skip_status_screen":true,"transaction_details":[{"key":"a","value":"<script href=\"https://www.somesite.com\"></script>","ui_configuration":{"is_key_bold":true,"is_value_bold":true}}],"background_image":{"url":"https://img.freepik.com/free-photo/abstract-blue-geometric-shapes-background_24972-1841.jpg","position":"bottom","size":"cover"}}}'
    
Response

    {"payment_id":"pay_U31G1jOtO9PLlEAhaCnh","merchant_id":"merchant_1743758708","status":"requires_payment_method","amount":100,"net_amount":100,"shipping_cost":null,"amount_capturable":100,"amount_received":null,"connector":null,"client_secret":"pay_U31G1jOtO9PLlEAhaCnh_secret_pdVAtxEgm8M2EXyvQekZ","created":"2025-04-07T10:13:54.256Z","currency":"EUR","customer_id":"cus_9SCrW9x19m2WbGYHprJS","customer":{"id":"cus_9SCrW9x19m2WbGYHprJS","name":null,"email":null,"phone":null,"phone_country_code":null},"description":null,"refunds":null,"disputes":null,"mandate_id":null,"mandate_data":null,"setup_future_usage":"off_session","off_session":null,"capture_on":null,"capture_method":"automatic","payment_method":null,"payment_method_data":null,"payment_token":null,"shipping":null,"billing":{"address":{"city":"San Fransico","country":"NL","line1":"1467","line2":"Harrison Street","line3":"Harrison Street","zip":"94122","state":"CA","first_name":"John","last_name":"Doe"},"phone":{"number":"8056594427","country_code":"+91"},"email":"random@juspay.in"},"order_details":null,"email":null,"name":null,"phone":null,"return_url":"https://example.com/","authentication_type":"three_ds","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_9SCrW9x19m2WbGYHprJS","created_at":1744020834,"expires":1744024434,"secret":"epk_4e5e8c040ef24d22841731b62003d0cb"},"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_1743758708/pay_U31G1jOtO9PLlEAhaCnh?locale=en","secure_link":"http://localhost:8080/payment_link/s/merchant_1743758708/pay_U31G1jOtO9PLlEAhaCnh?locale=en","payment_link_id":"plink_PH1XXvtv2yl3N9Kh9It8"},"profile_id":"pro_jZ4ix0hYxn8qi2NSSYih","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-04-07T10:28:54.250Z","fingerprint":null,"browser_info":null,"payment_method_id":null,"payment_method_status":null,"updated":"2025-04-07T10:13:54.276Z","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,"issuer_error_code":null,"issuer_error_message":null}

Verify below things

1. Giving any malformed script or any other tags in `transaction_details` renders it as a text

<img width="658" alt="Screenshot 2025-04-07 at 3 44 37 PM" src="https://github.com/user-attachments/assets/973c69d4-c429-45af-9d06-9559faffca26" />

</details>

## 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
