feat(core): payment links - add support for custom background image and layout in details section (#6725)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-12-12 20:50:33 +05:30
committed by GitHub
parent 1564ad72b8
commit d11d87408d
19 changed files with 775 additions and 87 deletions

View File

@ -35,7 +35,7 @@ use crate::{
api::payment_link::PaymentLinkResponseExt,
domain,
storage::{enums as storage_enums, payment_link::PaymentLink},
transformers::ForeignFrom,
transformers::{ForeignFrom, ForeignInto},
},
};
@ -129,6 +129,9 @@ pub async fn form_payment_link_data(
show_card_form_by_default: DEFAULT_SHOW_CARD_FORM,
allowed_domains: DEFAULT_ALLOWED_DOMAINS,
transaction_details: None,
background_image: None,
details_layout: None,
branding_visibility: None,
}
};
@ -271,6 +274,9 @@ pub async fn form_payment_link_data(
show_card_form_by_default: payment_link_config.show_card_form_by_default,
locale,
transaction_details: payment_link_config.transaction_details.clone(),
background_image: payment_link_config.background_image.clone(),
details_layout: payment_link_config.details_layout,
branding_visibility: payment_link_config.branding_visibility,
};
Ok((
@ -586,18 +592,16 @@ pub fn get_payment_link_config_based_on_priority(
default_domain_name: String,
payment_link_config_id: Option<String>,
) -> Result<(PaymentLinkConfig, String), error_stack::Report<errors::ApiErrorResponse>> {
let (domain_name, business_theme_configs, allowed_domains) =
let (domain_name, business_theme_configs, allowed_domains, branding_visibility) =
if let Some(business_config) = business_link_config {
logger::info!(
"domain name set to custom domain https://{:?}",
business_config.domain_name
);
(
business_config
.domain_name
.clone()
.map(|d_name| format!("https://{}", d_name))
.map(|d_name| {
logger::info!("domain name set to custom domain https://{:?}", d_name);
format!("https://{}", d_name)
})
.unwrap_or_else(|| default_domain_name.clone()),
payment_link_config_id
.and_then(|id| {
@ -608,9 +612,10 @@ pub fn get_payment_link_config_based_on_priority(
})
.or(business_config.default_config),
business_config.allowed_domains,
business_config.branding_visibility,
)
} else {
(default_domain_name, None, None)
(default_domain_name, None, None, None)
};
let (
@ -637,19 +642,45 @@ pub fn get_payment_link_config_based_on_priority(
(hide_card_nickname_field, DEFAULT_HIDE_CARD_NICKNAME_FIELD),
(show_card_form_by_default, DEFAULT_SHOW_CARD_FORM)
);
let payment_link_config = PaymentLinkConfig {
theme,
logo,
seller_name,
sdk_layout,
display_sdk_only,
enabled_saved_payment_method,
hide_card_nickname_field,
show_card_form_by_default,
allowed_domains,
transaction_details: payment_create_link_config
.and_then(|payment_link_config| payment_link_config.theme_config.transaction_details),
};
let payment_link_config =
PaymentLinkConfig {
theme,
logo,
seller_name,
sdk_layout,
display_sdk_only,
enabled_saved_payment_method,
hide_card_nickname_field,
show_card_form_by_default,
allowed_domains,
branding_visibility,
transaction_details: payment_create_link_config.as_ref().and_then(
|payment_link_config| payment_link_config.theme_config.transaction_details.clone(),
),
details_layout: payment_create_link_config
.as_ref()
.and_then(|payment_link_config| payment_link_config.theme_config.details_layout)
.or_else(|| {
business_theme_configs
.as_ref()
.and_then(|business_theme_config| business_theme_config.details_layout)
}),
background_image: payment_create_link_config
.as_ref()
.and_then(|payment_link_config| {
payment_link_config.theme_config.background_image.clone()
})
.or_else(|| {
business_theme_configs
.as_ref()
.and_then(|business_theme_config| {
business_theme_config
.background_image
.as_ref()
.map(|background_image| background_image.clone().foreign_into())
})
}),
};
Ok((payment_link_config, domain_name))
}
@ -752,6 +783,9 @@ pub async fn get_payment_link_status(
show_card_form_by_default: DEFAULT_SHOW_CARD_FORM,
allowed_domains: DEFAULT_ALLOWED_DOMAINS,
transaction_details: None,
background_image: None,
details_layout: None,
branding_visibility: None,
}
};

View File

@ -71,6 +71,7 @@ body {
#hyper-checkout-details {
font-family: "Montserrat";
background-repeat: no-repeat;
}
.hyper-checkout-payment {
@ -144,7 +145,7 @@ body {
#hyper-checkout-merchant-image,
#hyper-checkout-cart-image {
height: 64px;
width: 64px;
padding: 0 10px;
border-radius: 4px;
display: flex;
align-self: flex-start;
@ -153,8 +154,7 @@ body {
}
#hyper-checkout-merchant-image > img {
height: 48px;
width: 48px;
height: 40px;
}
#hyper-checkout-cart-image {
@ -279,7 +279,7 @@ body {
#hyper-checkout-merchant-description {
font-size: 13px;
color: #808080;
margin: 10px 0 20px 0;
}
.powered-by-hyper {
@ -292,7 +292,6 @@ body {
min-width: 584px;
z-index: 2;
background-color: var(--primary-color);
box-shadow: 0px 1px 10px #f2f2f2;
display: flex;
flex-flow: column;
align-items: center;
@ -665,6 +664,12 @@ body {
animation: loading 1s linear infinite;
}
@media only screen and (min-width: 1199px) {
#hyper-checkout-merchant-description {
color: #808080;
}
}
@media only screen and (max-width: 1199px) {
body {
overflow-y: scroll;

View File

@ -185,7 +185,7 @@
<div></div>
</div>
</div>
<div class="hyper-checkout hide-scrollbar">
<div id="hyper-checkout" class="hyper-checkout hide-scrollbar">
<div class="main hidden" id="hyper-checkout-status-canvas">
<div class="hyper-checkout-status-wrap">
<div id="hyper-checkout-status-header"></div>
@ -270,18 +270,6 @@
</svg>
</div>
<div id="hyper-checkout-cart-items" class="hide-scrollbar"></div>
<div id="hyper-checkout-merchant-description"></div>
<div class="powered-by-hyper">
<svg class="fill-current" height="18" width="130">
<use
xlink:href="#hyperswitch-brand"
x="0"
y="0"
height="18"
width="130"
></use>
</svg>
</div>
</div>
</div>
<div class="hyper-checkout-sdk" id="hyper-checkout-sdk">
@ -313,17 +301,6 @@
</div>
</div>
</div>
<div id="hyper-footer" class="hidden">
<svg class="fill-current" height="18" width="130">
<use
xlink:href="#hyperswitch-brand"
x="0"
y="0"
height="18"
width="130"
></use>
</svg>
</div>
<script>
{{logging_template}}
{{locale_template}}

View File

@ -139,8 +139,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) {
@ -204,7 +204,7 @@ function boot() {
}
else {
var orderDetails = paymentDetails.order_details;
if (orderDetails!==null) {
if (orderDetails !== null) {
var charges = 0;
for (var i = 0; i < orderDetails.length; i++) {
@ -213,8 +213,8 @@ function boot() {
orderDetails.push({
"amount": (paymentDetails.amount - charges).toFixed(2),
"product_img_link": "https://live.hyperswitch.io/payment-link-assets/cart_placeholder.png",
"product_name": translations.miscellaneousCharges+"\n" +
translations.miscellaneousChargesDetail,
"product_name": translations.miscellaneousCharges + "\n" +
translations.miscellaneousChargesDetail,
"quantity": null
});
}
@ -233,13 +233,17 @@ function boot() {
}
// Render UI
if (paymentDetails.display_sdk_only){
if (paymentDetails.display_sdk_only) {
renderSDKHeader(paymentDetails);
renderBranding(paymentDetails);
}
else{
else {
renderBackgroundImage(paymentDetails);
renderPaymentDetails(paymentDetails);
renderDynamicMerchantDetails(paymentDetails);
renderCart(paymentDetails);
renderDescription(paymentDetails);
renderBranding(paymentDetails);
renderSDKHeader(paymentDetails);
}
@ -295,12 +299,12 @@ function initializeEventListeners(paymentDetails) {
submitButtonLoaderNode.style.borderBottomColor = contrastingTone;
}
// Get locale for pay now
var payNowButtonText = document.createElement("div");
var payNowButtonText = document.getElementById('submit-button-text');
if (payNowButtonText) {
payNowButtonText.textContent = translations.payNow;
}
// Get locale for pay now
var payNowButtonText = document.createElement("div");
var payNowButtonText = document.getElementById('submit-button-text');
if (payNowButtonText) {
payNowButtonText.textContent = translations.payNow;
}
if (submitButtonNode instanceof HTMLButtonElement) {
submitButtonNode.style.color = contrastBWColor;
@ -473,12 +477,16 @@ function addText(id, msg) {
function addClass(id, className) {
var element = document.querySelector(id);
element.classList.add(className);
if (element instanceof HTMLElement) {
element.classList.add(className);
}
}
function removeClass(id, className) {
var element = document.querySelector(id);
element.classList.remove(className);
if (element instanceof HTMLElement) {
element.classList.remove(className);
}
}
/**
@ -570,7 +578,6 @@ function renderPaymentDetails(paymentDetails) {
// Create merchant logo's node
var merchantLogoNode = document.createElement("img");
merchantLogoNode.src = paymentDetails.merchant_logo;
merchantLogoNode.setAttribute("width", "48"); // Set width to 100 pixels
merchantLogoNode.setAttribute("height", "48");
// Create expiry node
@ -685,6 +692,113 @@ function appendMerchantDetails(paymentDetails, merchantDynamicDetails) {
}
}
/**
* Uses
* - Creates and appends description below the cart section (LAYOUT 1 / DEFAULT LAYOUT specification)
* @param {String} merchantDescription
*/
function renderDefaultLayout(merchantDescription) {
var cartItemNode = document.getElementById("hyper-checkout-cart");
if (cartItemNode instanceof HTMLDivElement) {
var merchantDescriptionNode = document.createElement("div");
merchantDescriptionNode.id = "hyper-checkout-merchant-description";
merchantDescriptionNode.innerText = merchantDescription;
cartItemNode.appendChild(merchantDescriptionNode);
show("#hyper-checkout-merchant-description");
}
}
/**
* Uses
* - Renders description in the appropriate section based on the specified layout
* @param {PaymentDetails} paymentDetails
*/
function renderDescription(paymentDetails) {
var detailsLayout = paymentDetails.details_layout;
if (typeof paymentDetails.merchant_description === "string" && paymentDetails.merchant_description.length > 0) {
switch (detailsLayout) {
case "layout1": {
renderDefaultLayout(paymentDetails.merchant_description);
break;
}
case "layout2": {
var paymentContextNode = document.getElementById("hyper-checkout-payment-context");
if (paymentContextNode instanceof HTMLDivElement) {
var merchantDescriptionNode = document.createElement("div");
merchantDescriptionNode.id = "hyper-checkout-merchant-description";
merchantDescriptionNode.innerText = paymentDetails.merchant_description;
var merchantDetailsNode = document.getElementById("hyper-checkout-payment-merchant-details");
if (merchantDetailsNode instanceof HTMLDivElement) {
paymentContextNode.insertBefore(merchantDescriptionNode, merchantDetailsNode);
show("#hyper-checkout-merchant-description");
}
}
break;
}
default: {
renderDefaultLayout(paymentDetails.merchant_description);
}
}
}
}
/**
* Uses
* - Creates and returns a div element with the HyperSwitch branding SVG
* @param {String} wrapperId
* @returns {HTMLDivElement} brandingWrapperNode
*/
function createHyperSwitchBrandingSVGElement(wrapperId) {
var brandingWrapperNode = document.createElement("div");
brandingWrapperNode.id = wrapperId;
brandingWrapperNode.innerHTML = '<svg class="fill-current" height="18" width="130"><use xlink:href="#hyperswitch-brand" x="0" y="0" height="18" width="130"></use></svg>';
return brandingWrapperNode;
}
/**
* Uses
* - Creates and appends HyperSwitch branding in appropriate sections based on the viewport dimensions (web vs mobile views)
* @param {PaymentDetails} paymentDetails
*/
function renderBranding(paymentDetails) {
if (paymentDetails.branding_visibility !== false) {
// Append below cart section for web views
var cartItemNode = document.getElementById("hyper-checkout-cart");
if (cartItemNode instanceof HTMLDivElement) {
var brandingWrapper = createHyperSwitchBrandingSVGElement("powered-by-hyper");
cartItemNode.appendChild(brandingWrapper);
}
// Append in document's body for mobile views
var mobileBrandingWrapper = createHyperSwitchBrandingSVGElement("hyper-footer");
document.body.appendChild(mobileBrandingWrapper);
if (!window.state.isMobileView) {
hide("#hyper-footer");
}
}
}
/**
* Uses
* - Renders background image in the payment details section
* @param {PaymentDetails} paymentDetails
*/
function renderBackgroundImage(paymentDetails) {
var backgroundImage = paymentDetails.background_image;
if (typeof backgroundImage === "object" && backgroundImage !== null) {
var paymentDetailsNode = document.getElementById("hyper-checkout-details");
if (paymentDetailsNode instanceof HTMLDivElement) {
paymentDetailsNode.style.backgroundImage = "url(" + backgroundImage.url + ")";
if (typeof backgroundImage.size === "string") {
paymentDetailsNode.style.backgroundSize = backgroundImage.size;
}
if (typeof backgroundImage.position === "string") {
paymentDetailsNode.style.backgroundPosition = backgroundImage.position;
}
}
}
}
/**
* Trigger - on boot
* Uses
@ -737,7 +851,7 @@ function renderCart(paymentDetails) {
buttonTextNode.id = "hyper-checkout-cart-button-text";
var hiddenItemsCount =
orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE;
buttonTextNode.innerText = translations.showMore+" (" + hiddenItemsCount + ")";
buttonTextNode.innerText = translations.showMore + " (" + hiddenItemsCount + ")";
expandButtonNode.append(buttonTextNode, buttonImageNode);
if (cartNode instanceof HTMLDivElement) {
cartNode.insertBefore(expandButtonNode, cartNode.lastElementChild);
@ -747,18 +861,6 @@ function renderCart(paymentDetails) {
hide("#hyper-checkout-cart-header");
hide("#hyper-checkout-cart-items");
hide("#hyper-checkout-cart-image");
if (
typeof paymentDetails.merchant_description === "string" &&
paymentDetails.merchant_description.length > 0
) {
var merchantDescriptionNode = document.getElementById(
"hyper-checkout-merchant-description"
);
if (merchantDescriptionNode instanceof HTMLDivElement) {
merchantDescriptionNode.innerText = paymentDetails.merchant_description;
}
show("#hyper-checkout-merchant-description");
}
}
}
@ -801,8 +903,8 @@ function renderCartItem(
if (item.quantity !== null) {
var quantityNode = document.createElement("div");
quantityNode.className = "hyper-checkout-card-item-quantity";
quantityNode.innerText = translations.quantity+": " + item.quantity;
}
quantityNode.innerText = translations.quantity + ": " + item.quantity;
}
// Product price
var priceNode = document.createElement("div");
priceNode.className = "hyper-checkout-card-item-price";
@ -869,9 +971,9 @@ function handleCartView(paymentDetails) {
);
});
}
if (cartItemsNode instanceof HTMLDivElement){
if (cartItemsNode instanceof HTMLDivElement) {
cartItemsNode.style.maxHeight = cartItemsNode.scrollHeight + "px";
cartItemsNode.style.height = cartItemsNode.scrollHeight + "px";
}
@ -914,7 +1016,7 @@ function handleCartView(paymentDetails) {
var hiddenItemsCount =
orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE;
if (cartButtonTextNode instanceof HTMLSpanElement) {
cartButtonTextNode.innerText = translations.showMore+" (" + hiddenItemsCount + ")";
cartButtonTextNode.innerText = translations.showMore + " (" + hiddenItemsCount + ")";
}
var arrowDownImage = document.getElementById("arrow-down");
if (

View File

@ -27,6 +27,7 @@ function initializeSDK() {
// @ts-ignore
hyper = window.Hyper(pub_key, {
isPreloadEnabled: false,
shouldUseTopRedirection: true,
});
// @ts-ignore
widgets = hyper.widgets({

View File

@ -3647,6 +3647,7 @@ impl ForeignFrom<api_models::admin::PaymentLinkConfigRequest>
enabled_saved_payment_method: config.enabled_saved_payment_method,
hide_card_nickname_field: config.hide_card_nickname_field,
show_card_form_by_default: config.show_card_form_by_default,
details_layout: config.details_layout,
transaction_details: config.transaction_details.map(|transaction_details| {
transaction_details
.iter()
@ -3655,6 +3656,11 @@ impl ForeignFrom<api_models::admin::PaymentLinkConfigRequest>
})
.collect()
}),
background_image: config.background_image.map(|background_image| {
diesel_models::business_profile::PaymentLinkBackgroundImageConfig::foreign_from(
background_image.clone(),
)
}),
}
}
}
@ -3701,6 +3707,7 @@ impl ForeignFrom<diesel_models::PaymentLinkConfigRequestForPayments>
enabled_saved_payment_method: config.enabled_saved_payment_method,
hide_card_nickname_field: config.hide_card_nickname_field,
show_card_form_by_default: config.show_card_form_by_default,
details_layout: config.details_layout,
transaction_details: config.transaction_details.map(|transaction_details| {
transaction_details
.iter()
@ -3711,6 +3718,11 @@ impl ForeignFrom<diesel_models::PaymentLinkConfigRequestForPayments>
})
.collect()
}),
background_image: config.background_image.map(|background_image| {
api_models::admin::PaymentLinkBackgroundImageConfig::foreign_from(
background_image.clone(),
)
}),
}
}
}

View File

@ -1921,6 +1921,7 @@ impl ForeignFrom<api_models::admin::BusinessPaymentLinkConfig>
.collect()
}),
allowed_domains: item.allowed_domains,
branding_visibility: item.branding_visibility,
}
}
}
@ -1938,6 +1939,7 @@ impl ForeignFrom<diesel_models::business_profile::BusinessPaymentLinkConfig>
.collect()
}),
allowed_domains: item.allowed_domains,
branding_visibility: item.branding_visibility,
}
}
}
@ -1955,6 +1957,10 @@ impl ForeignFrom<api_models::admin::PaymentLinkConfigRequest>
enabled_saved_payment_method: item.enabled_saved_payment_method,
hide_card_nickname_field: item.hide_card_nickname_field,
show_card_form_by_default: item.show_card_form_by_default,
details_layout: item.details_layout,
background_image: item
.background_image
.map(|background_image| background_image.foreign_into()),
}
}
}
@ -1973,6 +1979,36 @@ impl ForeignFrom<diesel_models::business_profile::PaymentLinkConfigRequest>
hide_card_nickname_field: item.hide_card_nickname_field,
show_card_form_by_default: item.show_card_form_by_default,
transaction_details: None,
details_layout: item.details_layout,
background_image: item
.background_image
.map(|background_image| background_image.foreign_into()),
}
}
}
impl ForeignFrom<diesel_models::business_profile::PaymentLinkBackgroundImageConfig>
for api_models::admin::PaymentLinkBackgroundImageConfig
{
fn foreign_from(
item: diesel_models::business_profile::PaymentLinkBackgroundImageConfig,
) -> Self {
Self {
url: item.url,
position: item.position,
size: item.size,
}
}
}
impl ForeignFrom<api_models::admin::PaymentLinkBackgroundImageConfig>
for diesel_models::business_profile::PaymentLinkBackgroundImageConfig
{
fn foreign_from(item: api_models::admin::PaymentLinkBackgroundImageConfig) -> Self {
Self {
url: item.url,
position: item.position,
size: item.size,
}
}
}