diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index e844d1900a1a..979214a071a9 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -463,9 +463,8 @@ pub struct PaymentLinkConfig { #[serde(deny_unknown_fields)] pub struct PaymentLinkColorSchema { - pub primary_color: Option, - pub primary_accent_color: Option, - pub secondary_color: Option, + pub background_primary_color: Option, + pub sdk_theme: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 196dd108333b..22579ed6d6ea 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3150,6 +3150,7 @@ pub struct PaymentLinkDetails { pub merchant_logo: String, pub return_url: String, pub merchant_name: String, - pub order_details: Vec, + pub order_details: Option>, pub max_items_visible_after_collapse: i8, + pub sdk_theme: Option, } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 9b748a16782a..b33d9d61f56d 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -12,9 +12,9 @@ dummy_connector = [] [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } -serde = { version = "1.0.160", features = [ "derive" ] } +serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.25", features = [ "derive" ] } +strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 2f517295ae48..7bc248bf8d1b 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -29,3 +29,15 @@ pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; /// Header Key for application overhead of a request pub const X_HS_LATENCY: &str = "x-hs-latency"; + +/// SDK Default Theme const +pub const DEFAULT_SDK_THEME: &str = "#7EA8F6"; + +/// Default Payment Link Background color +pub const DEFAULT_BACKGROUND_COLOR: &str = "#E5E5E5"; + +/// Default product Img Link +pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; + +/// Default Merchant Logo Link +pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index d13c9b6421f4..6b0d46dec8d1 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -290,10 +290,9 @@ impl | api_models::payments::VoucherData::FamilyMart { .. } | api_models::payments::VoucherData::Seicomart { .. } | api_models::payments::VoucherData::PayEasy { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -342,12 +341,8 @@ impl api_models::payments::BankTransferData::Pse { .. } => { ZenPaymentChannels::PclBoacompraPse } - api_models::payments::BankTransferData::SepaBankTransfer { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::BankTransferData::AchBankTransfer { .. } + api_models::payments::BankTransferData::SepaBankTransfer { .. } + | api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::BacsBankTransfer { .. } | api_models::payments::BankTransferData::PermataBankTransfer { .. } | api_models::payments::BankTransferData::BcaBankTransfer { .. } @@ -356,10 +351,9 @@ impl | api_models::payments::BankTransferData::CimbVaBankTransfer { .. } | api_models::payments::BankTransferData::DanamonVaBankTransfer { .. } | api_models::payments::BankTransferData::MandiriVaBankTransfer { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -489,12 +483,8 @@ impl api_models::payments::WalletData::WeChatPayRedirect(_) | api_models::payments::WalletData::PaypalRedirect(_) | api_models::payments::WalletData::ApplePay(_) - | api_models::payments::WalletData::GooglePay(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::WalletData::AliPayQr(_) + | api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::AliPayQr(_) | api_models::payments::WalletData::AliPayRedirect(_) | api_models::payments::WalletData::AliPayHkRedirect(_) | api_models::payments::WalletData::MomoRedirect(_) @@ -514,10 +504,9 @@ impl | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) | api_models::payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; let terminal_uuid = session_data @@ -719,10 +708,9 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } } } @@ -736,13 +724,8 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::Sofort { .. } | api_models::payments::BankRedirectData::BancontactCard { .. } | api_models::payments::BankRedirectData::Blik { .. } - | api_models::payments::BankRedirectData::Trustly { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ) - .into()) - } - api_models::payments::BankRedirectData::Eps { .. } + | api_models::payments::BankRedirectData::Trustly { .. } + | api_models::payments::BankRedirectData::Eps { .. } | api_models::payments::BankRedirectData::Giropay { .. } | api_models::payments::BankRedirectData::Przelewy24 { .. } | api_models::payments::BankRedirectData::Bizum {} @@ -754,10 +737,9 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::OpenBankingUk { .. } | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -776,10 +758,9 @@ impl TryFrom<&api_models::payments::PayLaterData> for ZenPaymentsRequest { | api_models::payments::PayLaterData::WalleyRedirect {} | api_models::payments::PayLaterData::AlmaRedirect {} | api_models::payments::PayLaterData::AtomeRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -794,10 +775,9 @@ impl TryFrom<&api_models::payments::BankDebitData> for ZenPaymentsRequest { | api_models::payments::BankDebitData::SepaBankDebit { .. } | api_models::payments::BankDebitData::BecsBankDebit { .. } | api_models::payments::BankDebitData::BacsBankDebit { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -811,10 +791,9 @@ impl TryFrom<&api_models::payments::CardRedirectData> for ZenPaymentsRequest { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} | api_models::payments::CardRedirectData::MomoAtm {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -825,19 +804,13 @@ impl TryFrom<&api_models::payments::GiftCardData> for ZenPaymentsRequest { type Error = error_stack::Report; fn try_from(value: &api_models::payments::GiftCardData) -> Result { match value { - api_models::payments::GiftCardData::PaySafeCard {} => { + api_models::payments::GiftCardData::PaySafeCard {} + | api_models::payments::GiftCardData::Givex(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ) .into()) } - api_models::payments::GiftCardData::Givex(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } - .into()) - } } } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 0012efc86c9f..2ea6a4d7f219 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,6 +1,12 @@ use api_models::admin as admin_types; +use common_utils::{ + consts::{ + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, + }, + ext_traits::ValueExt, +}; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::{PeekInterface, Secret}; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -76,12 +82,7 @@ pub async fn intiate_payment_link_flow( }) .transpose()?; - let order_details = payment_intent - .order_details - .get_required_value("order_details") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "order_details", - })?; + let order_details = validate_order_details(payment_intent.order_details)?; let return_url = if let Some(payment_create_return_url) = payment_intent.return_url { payment_create_return_url @@ -99,6 +100,9 @@ pub async fn intiate_payment_link_flow( payment_intent.client_secret, )?; + let (default_sdk_theme, default_background_color) = + (DEFAULT_SDK_THEME, DEFAULT_BACKGROUND_COLOR); + let payment_details = api_models::payments::PaymentLinkDetails { amount: payment_intent.amount, currency, @@ -116,13 +120,25 @@ pub async fn intiate_payment_link_flow( client_secret, merchant_logo: payment_link_config .clone() - .map(|pl_metadata| pl_metadata.merchant_logo.unwrap_or_default()) + .map(|pl_config| { + pl_config + .merchant_logo + .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()) + }) .unwrap_or_default(), max_items_visible_after_collapse: 3, + sdk_theme: payment_link_config.clone().and_then(|pl_config| { + pl_config + .color_scheme + .map(|color| color.sdk_theme.unwrap_or(default_sdk_theme.to_string())) + }), }; let js_script = get_js_script(payment_details)?; - let css_script = get_color_scheme_css(payment_link_config.clone()); + let css_script = get_color_scheme_css( + payment_link_config.clone(), + default_background_color.to_string(), + ); let payment_link_data = services::PaymentLinkFormData { js_script, sdk_url: state.conf.payment_link.sdk_url.clone(), @@ -149,38 +165,21 @@ fn get_js_script( fn get_color_scheme_css( payment_link_config: Option, + default_primary_color: String, ) -> String { - let (default_primary_color, default_accent_color, default_secondary_color) = ( - "#C6C7C8".to_string(), - "#6A8EF5".to_string(), - "#0C48F6".to_string(), - ); - - let (primary_color, primary_accent_color, secondary_color) = payment_link_config + let background_primary_color = payment_link_config .and_then(|pl_config| { pl_config.color_scheme.map(|color| { - ( - color.primary_color.unwrap_or(default_primary_color.clone()), - color - .primary_accent_color - .unwrap_or(default_accent_color.clone()), - color - .secondary_color - .unwrap_or(default_secondary_color.clone()), - ) + color + .background_primary_color + .unwrap_or(default_primary_color.clone()) }) }) - .unwrap_or(( - default_primary_color, - default_accent_color, - default_secondary_color, - )); + .unwrap_or(default_primary_color); format!( ":root {{ - --primary-color: {primary_color}; - --primary-accent-color: {primary_accent_color}; - --secondary-color: {secondary_color}; + --primary-color: {background_primary_color}; }}" ) } @@ -203,3 +202,36 @@ fn validate_sdk_requirements( })?; Ok((pub_key, currency, client_secret)) } + +fn validate_order_details( + order_details: Option>>, +) -> Result< + Option>, + error_stack::Report, +> { + let order_details = order_details + .map(|order_details| { + order_details + .iter() + .map(|data| { + data.to_owned() + .parse_value("OrderDetailsWithAmount") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "OrderDetailsWithAmount", + }) + .attach_printable("Unable to parse OrderDetailsWithAmount") + }) + .collect::, _>>() + }) + .transpose()?; + + let updated_order_details = order_details.map(|mut order_details| { + for order in order_details.iter_mut() { + if order.product_img_link.is_none() { + order.product_img_link = Some(DEFAULT_PRODUCT_IMG.to_string()); + } + } + order_details + }); + Ok(updated_order_details) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 462a11d2567e..67410cac8418 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -112,8 +112,8 @@ } #hyper-checkout-merchant-image > img { - height: 48px; - width: 48px; + height: 40px; + width: 40px; } #hyper-checkout-cart-image { @@ -175,8 +175,8 @@ } .hyper-checkout-cart-product-image { - height: 72px; - width: 72px; + height: 56px; + width: 56px; } .hyper-checkout-card-item-name { @@ -234,13 +234,21 @@ background-color: var(--primary-color); box-shadow: 0px 1px 10px #f2f2f2; display: flex; + flex-flow: column; align-items: center; justify-content: center; } #payment-form-wrap { - min-width: 584px; - padding: 50px; + min-width: 300px; + width: 30vw; + padding: 20px; + background-color: white; + border-radius: 3px; + } + + .powered-by-hyper { + margin-top: 20px; } #hyper-checkout-sdk-header { @@ -295,28 +303,13 @@ margin-top: 10px; } - .checkoutButton { - height: 48px; - border-radius: 25px; - width: 100%; - border: transparent; - background: var(--secondary-color); - color: #ffffff; - font-weight: 600; - cursor: pointer; - } - .page-spinner, .page-spinner::before, - .page-spinner::after, - .spinner, - .spinner:before, - .spinner:after { + .page-spinner::after { border-radius: 50%; } - .page-spinner, - .spinner { + .page-spinner { color: #ffffff; font-size: 22px; text-indent: -99999px; @@ -331,9 +324,7 @@ } .page-spinner::before, - .page-spinner::after, - .spinner:before, - .spinner:after { + .page-spinner::after { position: absolute; content: ""; } @@ -405,19 +396,6 @@ } } - .spinner:before { - width: 10.4px; - height: 20.4px; - background: var(--primary-color); - border-radius: 20.4px 0 0 20.4px; - top: -0.2px; - left: -0.2px; - -webkit-transform-origin: 10.4px 10.2px; - transform-origin: 10.4px 10.2px; - -webkit-animation: loading 2s infinite ease 1.5s; - animation: loading 2s infinite ease 1.5s; - } - #payment-message { font-size: 12px; font-weight: 500; @@ -426,19 +404,6 @@ font-family: "Montserrat"; } - .spinner:after { - width: 10.4px; - height: 10.2px; - background: var(--primary-color); - border-radius: 0 10.2px 10.2px 0; - top: -0.1px; - left: 10.2px; - -webkit-transform-origin: 0px 10.2px; - transform-origin: 0px 10.2px; - -webkit-animation: loading 2s infinite ease; - animation: loading 2s infinite ease; - } - #payment-form { max-width: 560px; width: 100%; @@ -447,11 +412,6 @@ } @media only screen and (max-width: 1200px) { - .checkoutButton { - width: 95%; - background-color: var(--primary-color); - } - .hyper-checkout { flex-flow: column; margin: 0; @@ -627,16 +587,16 @@ @@ -700,7 +660,7 @@
-
+
-
+ +
+ + + + + + + + + + + + + + + + +
- - + function showSDK(e) { + if (window.state.isMobileView) { + hide("#hyper-checkout-cart"); + } else { + show("#hyper-checkout-cart"); + } + setPageLoading(true); + checkStatus() + .then((res) => { + if (res.showSdk) { + renderPaymentDetails(); + renderCart(); + renderSDKHeader(); + show("#hyper-checkout-sdk"); + show("#hyper-checkout-details"); + } else { + show("#hyper-checkout-status"); + show("#hyper-footer"); + } + }) + .catch((err) => {}) + .finally(() => { + setPageLoading(false); + }); + } + + window.addEventListener("resize", (event) => { + const currentHeight = window.innerHeight; + const currentWidth = window.innerWidth; + if (currentWidth <= 1200 && window.state.prevWidth > 1200) { + hide("#hyper-checkout-cart"); + } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { + show("#hyper-checkout-cart"); + } + + window.state.prevHeight = currentHeight; + window.state.prevWidth = currentWidth; + window.state.isMobileView = currentWidth <= 1200; + }); + + diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 6e61f2eb614e..23f8f1b3628b 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7809,15 +7809,11 @@ "PaymentLinkColorSchema": { "type": "object", "properties": { - "primary_color": { + "background_primary_color": { "type": "string", "nullable": true }, - "primary_accent_color": { - "type": "string", - "nullable": true - }, - "secondary_color": { + "sdk_theme": { "type": "string", "nullable": true }