diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 97352ead78..59791cc750 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -15306,6 +15306,14 @@ "type": "string", "description": "The url for Qr code given by the connector" }, + "display_text": { + "type": "string", + "nullable": true + }, + "border_color": { + "type": "string", + "nullable": true + }, "type": { "type": "string", "enum": [ diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 59ea6e97fb..12da310ac6 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4406,6 +4406,8 @@ pub enum NextActionData { #[schema(value_type = String)] /// The url for Qr code given by the connector qr_code_url: Option, + display_text: Option, + border_color: Option, }, /// Contains url to fetch Qr code data FetchQrCodeInformation { @@ -4493,6 +4495,12 @@ pub enum QrCodeInformation { qr_code_url: Url, display_to_timestamp: Option, }, + QrColorDataUrl { + color_image_data_url: Url, + display_to_timestamp: Option, + display_text: Option, + border_color: Option, + }, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, ToSchema)] diff --git a/crates/common_utils/src/errors.rs b/crates/common_utils/src/errors.rs index e62606b458..9f65361673 100644 --- a/crates/common_utils/src/errors.rs +++ b/crates/common_utils/src/errors.rs @@ -103,6 +103,9 @@ pub enum QrCodeError { /// Failed to encode data into Qr code #[error("Failed to create Qr code")] FailedToCreateQrCode, + /// Failed to parse hex color + #[error("Invalid hex color code supplied")] + InvalidHexColor, } /// Api Models construction error diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 2f8a979ba9..5c38a55dc0 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs @@ -41,6 +41,7 @@ const GOOGLEPAY_API_VERSION_MINOR: u8 = 0; const GOOGLEPAY_API_VERSION: u8 = 2; use crate::{ + constants, types::{ PaymentsCancelResponseRouterData, PaymentsCaptureResponseRouterData, PaymentsSyncResponseRouterData, RefundsResponseRouterData, ResponseRouterData, @@ -1725,16 +1726,21 @@ impl From for enums::RefundStatus { pub fn get_qr_metadata( response: &DuitNowQrCodeResponse, ) -> CustomResult, errors::ConnectorError> { - let image_data = QrImage::new_from_data(response.txn_data.request_data.qr_data.peek().clone()) - .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + let image_data = QrImage::new_colored_from_data( + response.txn_data.request_data.qr_data.peek().clone(), + constants::DUIT_NOW_BRAND_COLOR, + ) + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; let image_data_url = Url::parse(image_data.data.clone().as_str()).ok(); let display_to_timestamp = None; - if let Some(image_data_url) = image_data_url { - let qr_code_info = payments::QrCodeInformation::QrDataUrl { - image_data_url, + if let Some(color_image_data_url) = image_data_url { + let qr_code_info = payments::QrCodeInformation::QrColorDataUrl { + color_image_data_url, display_to_timestamp, + display_text: Some(constants::DUIT_NOW_BRAND_TEXT.to_string()), + border_color: Some(constants::DUIT_NOW_BRAND_COLOR.to_string()), }; Some(qr_code_info.encode_to_value()) diff --git a/crates/hyperswitch_connectors/src/constants.rs b/crates/hyperswitch_connectors/src/constants.rs index bf2f4c2584..51767bf327 100644 --- a/crates/hyperswitch_connectors/src/constants.rs +++ b/crates/hyperswitch_connectors/src/constants.rs @@ -43,3 +43,7 @@ pub const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the co pub const REFUND_VOIDED: &str = "Refund request has been voided."; pub const LOW_BALANCE_ERROR_MESSAGE: &str = "Insufficient balance in the payment method"; + +pub const DUIT_NOW_BRAND_COLOR: &str = "#ED2E67"; + +pub const DUIT_NOW_BRAND_TEXT: &str = "MALAYSIA NATIONAL QR"; diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 67529f14d9..ac5a7714b5 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -52,7 +52,7 @@ use hyperswitch_domain_models::{ types::OrderDetailsWithAmount, }; use hyperswitch_interfaces::{api, consts, errors, types::Response}; -use image::Luma; +use image::{DynamicImage, ImageBuffer, ImageFormat, Luma, Rgba}; use masking::{ExposeInterface, PeekInterface, Secret}; use once_cell::sync::Lazy; use regex::Regex; @@ -4992,12 +4992,12 @@ impl QrImage { .change_context(common_utils::errors::QrCodeError::FailedToCreateQrCode)?; let qrcode_image_buffer = qr_code.render::>().build(); - let qrcode_dynamic_image = image::DynamicImage::ImageLuma8(qrcode_image_buffer); + let qrcode_dynamic_image = DynamicImage::ImageLuma8(qrcode_image_buffer); let mut image_bytes = std::io::BufWriter::new(std::io::Cursor::new(Vec::new())); // Encodes qrcode_dynamic_image and write it to image_bytes - let _ = qrcode_dynamic_image.write_to(&mut image_bytes, image::ImageFormat::Png); + let _ = qrcode_dynamic_image.write_to(&mut image_bytes, ImageFormat::Png); let image_data_source = format!( "{},{}", @@ -5008,6 +5008,58 @@ impl QrImage { data: image_data_source, }) } + + pub fn new_colored_from_data( + data: String, + hex_color: &str, + ) -> Result> { + let qr_code = qrcode::QrCode::new(data.as_bytes()) + .change_context(common_utils::errors::QrCodeError::FailedToCreateQrCode)?; + + let qrcode_image_buffer = qr_code.render::>().build(); + let (width, height) = qrcode_image_buffer.dimensions(); + let mut colored_image = ImageBuffer::new(width, height); + let rgb = Self::parse_hex_color(hex_color)?; + + for (x, y, pixel) in qrcode_image_buffer.enumerate_pixels() { + let luminance = pixel.0[0]; + let color = if luminance == 0 { + Rgba([rgb.0, rgb.1, rgb.2, 255]) + } else { + Rgba([255, 255, 255, 255]) + }; + colored_image.put_pixel(x, y, color); + } + + let qrcode_dynamic_image = DynamicImage::ImageRgba8(colored_image); + let mut image_bytes = std::io::Cursor::new(Vec::new()); + qrcode_dynamic_image + .write_to(&mut image_bytes, ImageFormat::Png) + .change_context(common_utils::errors::QrCodeError::FailedToCreateQrCode)?; + + let image_data_source = format!( + "{},{}", + QR_IMAGE_DATA_SOURCE_STRING, + BASE64_ENGINE.encode(image_bytes.get_ref()) + ); + + Ok(Self { + data: image_data_source, + }) + } + + pub fn parse_hex_color(hex: &str) -> Result<(u8, u8, u8), common_utils::errors::QrCodeError> { + let hex = hex.trim_start_matches('#'); + if hex.len() == 6 { + let r = u8::from_str_radix(&hex[0..2], 16).ok(); + let g = u8::from_str_radix(&hex[2..4], 16).ok(); + let b = u8::from_str_radix(&hex[4..6], 16).ok(); + if let (Some(r), Some(g), Some(b)) = (r, g, b) { + return Ok((r, g, b)); + } + } + Err(common_utils::errors::QrCodeError::InvalidHexColor) + } } #[cfg(test)] diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 545c058ce1..70051bdd50 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -824,6 +824,8 @@ pub enum StripeNextAction { image_data_url: Option, display_to_timestamp: Option, qr_code_url: Option, + border_color: Option, + display_text: Option, }, FetchQrCodeInformation { qr_code_fetch_url: url::Url, @@ -869,10 +871,14 @@ pub(crate) fn into_stripe_next_action( image_data_url, display_to_timestamp, qr_code_url, + border_color, + display_text, } => StripeNextAction::QrCodeInformation { image_data_url, display_to_timestamp, qr_code_url, + border_color, + display_text, }, payments::NextActionData::FetchQrCodeInformation { qr_code_fetch_url } => { StripeNextAction::FetchQrCodeInformation { qr_code_fetch_url } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index 03cf9742f7..0b3c6cdd59 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -377,6 +377,8 @@ pub enum StripeNextAction { image_data_url: Option, display_to_timestamp: Option, qr_code_url: Option, + border_color: Option, + display_text: Option, }, FetchQrCodeInformation { qr_code_fetch_url: url::Url, @@ -422,10 +424,14 @@ pub(crate) fn into_stripe_next_action( image_data_url, display_to_timestamp, qr_code_url, + display_text, + border_color, } => StripeNextAction::QrCodeInformation { image_data_url, display_to_timestamp, qr_code_url, + display_text, + border_color, }, payments::NextActionData::FetchQrCodeInformation { qr_code_fetch_url } => { StripeNextAction::FetchQrCodeInformation { qr_code_fetch_url } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index ad56394c5f..12483aac8e 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -2960,6 +2960,8 @@ impl ForeignFrom for api_models::paymen image_data_url: Some(image_data_url), qr_code_url: Some(qr_code_url), display_to_timestamp, + border_color: None, + display_text: None, }, api_models::payments::QrCodeInformation::QrDataUrl { image_data_url, @@ -2968,6 +2970,8 @@ impl ForeignFrom for api_models::paymen image_data_url: Some(image_data_url), display_to_timestamp, qr_code_url: None, + border_color: None, + display_text: None, }, api_models::payments::QrCodeInformation::QrCodeImageUrl { qr_code_url, @@ -2976,6 +2980,20 @@ impl ForeignFrom for api_models::paymen qr_code_url: Some(qr_code_url), image_data_url: None, display_to_timestamp, + border_color: None, + display_text: None, + }, + api_models::payments::QrCodeInformation::QrColorDataUrl { + color_image_data_url, + display_to_timestamp, + border_color, + display_text, + } => Self::QrCodeInformation { + qr_code_url: None, + image_data_url: Some(color_image_data_url), + display_to_timestamp, + border_color, + display_text, }, } }