mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(payment_link): add status page for payment link (#3213)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Kashif <mohammed.kashif@juspay.in> Co-authored-by: Kashif <kashif.dev@protonmail.com> Co-authored-by: hrithikeshvm <hrithikeshmylatty@gmail.com> Co-authored-by: hrithikeshvm <vmhrithikesh@gmail.com> Co-authored-by: Sahkal Poddar <sahkalpoddar@Sahkals-MacBook-Air.local>
This commit is contained in:
@ -473,7 +473,7 @@ apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Cer
|
||||
apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm
|
||||
|
||||
[payment_link]
|
||||
sdk_url = "http://localhost:9090/dist/HyperLoader.js"
|
||||
sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js"
|
||||
|
||||
[payment_method_auth]
|
||||
redis_expiry = 900
|
||||
|
||||
@ -494,7 +494,7 @@ apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE"
|
||||
apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY"
|
||||
|
||||
[payment_link]
|
||||
sdk_url = "http://localhost:9090/dist/HyperLoader.js"
|
||||
sdk_url = "http://localhost:9050/HyperLoader.js"
|
||||
|
||||
[payment_method_auth]
|
||||
redis_expiry = 900
|
||||
|
||||
@ -3358,6 +3358,13 @@ pub struct PaymentLinkInitiateRequest {
|
||||
pub payment_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PaymentLinkData {
|
||||
PaymentLinkDetails(PaymentLinkDetails),
|
||||
PaymentLinkStatusDetails(PaymentLinkStatusDetails),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct PaymentLinkDetails {
|
||||
pub amount: String,
|
||||
@ -3376,6 +3383,21 @@ pub struct PaymentLinkDetails {
|
||||
pub merchant_description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct PaymentLinkStatusDetails {
|
||||
pub amount: String,
|
||||
pub currency: api_enums::Currency,
|
||||
pub payment_id: String,
|
||||
pub merchant_logo: String,
|
||||
pub merchant_name: String,
|
||||
#[serde(with = "common_utils::custom_serde::iso8601")]
|
||||
pub created: PrimitiveDateTime,
|
||||
pub intent_status: api_enums::IntentStatus,
|
||||
pub payment_link_status: PaymentLinkStatus,
|
||||
pub error_code: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
||||
@ -3451,7 +3473,8 @@ pub struct OrderDetailsWithStringAmount {
|
||||
pub product_img_link: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
#[derive(PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PaymentLinkStatus {
|
||||
Active,
|
||||
Expired,
|
||||
|
||||
@ -133,19 +133,34 @@ where
|
||||
.map_into_boxed_body()
|
||||
}
|
||||
|
||||
Ok(api::ApplicationResponse::PaymenkLinkForm(payment_link_data)) => {
|
||||
match api::build_payment_link_html(*payment_link_data) {
|
||||
Ok(rendered_html) => api::http_response_html_data(rendered_html),
|
||||
Err(_) => api::http_response_err(
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Error while rendering payment link html page"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
Ok(api::ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => {
|
||||
match *boxed_payment_link_data {
|
||||
api::PaymentLinkAction::PaymentLinkFormData(payment_link_data) => {
|
||||
match api::build_payment_link_html(payment_link_data) {
|
||||
Ok(rendered_html) => api::http_response_html_data(rendered_html),
|
||||
Err(_) => api::http_response_err(
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Error while rendering payment link html page"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
}
|
||||
}
|
||||
api::PaymentLinkAction::PaymentLinkStatus(payment_link_data) => {
|
||||
match api::get_payment_link_status(payment_link_data) {
|
||||
Ok(rendered_html) => api::http_response_html_data(rendered_html),
|
||||
Err(_) => api::http_response_err(
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Error while rendering payment link status page"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(error) => api::log_and_return_error_response(error),
|
||||
};
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ use time::PrimitiveDateTime;
|
||||
|
||||
use super::errors::{self, RouterResult, StorageErrorExt};
|
||||
use crate::{
|
||||
core::payments::helpers,
|
||||
errors::RouterResponse,
|
||||
routes::AppState,
|
||||
services,
|
||||
@ -68,18 +67,6 @@ pub async fn intiate_payment_link_flow(
|
||||
.get_required_value("payment_link_id")
|
||||
.change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?;
|
||||
|
||||
helpers::validate_payment_status_against_not_allowed_statuses(
|
||||
&payment_intent.status,
|
||||
&[
|
||||
storage_enums::IntentStatus::Cancelled,
|
||||
storage_enums::IntentStatus::Succeeded,
|
||||
storage_enums::IntentStatus::Processing,
|
||||
storage_enums::IntentStatus::RequiresCapture,
|
||||
storage_enums::IntentStatus::RequiresMerchantAction,
|
||||
],
|
||||
"use payment link for",
|
||||
)?;
|
||||
|
||||
let merchant_name_from_merchant_account = merchant_account
|
||||
.merchant_name
|
||||
.clone()
|
||||
@ -101,7 +88,7 @@ pub async fn intiate_payment_link_flow(
|
||||
}
|
||||
};
|
||||
|
||||
let return_url = if let Some(payment_create_return_url) = payment_intent.return_url {
|
||||
let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() {
|
||||
payment_create_return_url
|
||||
} else {
|
||||
merchant_account
|
||||
@ -114,23 +101,73 @@ pub async fn intiate_payment_link_flow(
|
||||
let (pub_key, currency, client_secret) = validate_sdk_requirements(
|
||||
merchant_account.publishable_key,
|
||||
payment_intent.currency,
|
||||
payment_intent.client_secret,
|
||||
payment_intent.client_secret.clone(),
|
||||
)?;
|
||||
let order_details = validate_order_details(payment_intent.order_details, currency)?;
|
||||
let amount = currency
|
||||
.to_currency_base_unit(payment_intent.amount)
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?;
|
||||
let order_details = validate_order_details(payment_intent.order_details.clone(), currency)?;
|
||||
|
||||
let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| {
|
||||
common_utils::date_time::now()
|
||||
payment_intent
|
||||
.created_at
|
||||
.saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY))
|
||||
});
|
||||
|
||||
// converting first letter of merchant name to upperCase
|
||||
let merchant_name = capitalize_first_char(&payment_link_config.seller_name);
|
||||
let css_script = get_color_scheme_css(payment_link_config.clone());
|
||||
let payment_link_status = check_payment_link_status(session_expiry);
|
||||
|
||||
if check_payment_link_invalid_conditions(
|
||||
&payment_intent.status,
|
||||
&[
|
||||
storage_enums::IntentStatus::Cancelled,
|
||||
storage_enums::IntentStatus::Failed,
|
||||
storage_enums::IntentStatus::Processing,
|
||||
storage_enums::IntentStatus::RequiresCapture,
|
||||
storage_enums::IntentStatus::RequiresMerchantAction,
|
||||
storage_enums::IntentStatus::Succeeded,
|
||||
],
|
||||
) || payment_link_status == api_models::payments::PaymentLinkStatus::Expired
|
||||
{
|
||||
let attempt_id = payment_intent.active_attempt.get_id().clone();
|
||||
let payment_attempt = db
|
||||
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
|
||||
&payment_intent.payment_id,
|
||||
&merchant_id,
|
||||
&attempt_id.clone(),
|
||||
merchant_account.storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
let payment_details = api_models::payments::PaymentLinkStatusDetails {
|
||||
amount,
|
||||
currency,
|
||||
payment_id: payment_intent.payment_id,
|
||||
merchant_name,
|
||||
merchant_logo: payment_link_config.clone().logo,
|
||||
created: payment_link.created_at,
|
||||
intent_status: payment_intent.status,
|
||||
payment_link_status,
|
||||
error_code: payment_attempt.error_code,
|
||||
error_message: payment_attempt.error_message,
|
||||
};
|
||||
let js_script = get_js_script(
|
||||
api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details),
|
||||
)?;
|
||||
let payment_link_error_data = services::PaymentLinkStatusData {
|
||||
js_script,
|
||||
css_script,
|
||||
};
|
||||
return Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new(
|
||||
services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
|
||||
)));
|
||||
};
|
||||
|
||||
let payment_details = api_models::payments::PaymentLinkDetails {
|
||||
amount: currency
|
||||
.to_currency_base_unit(payment_intent.amount)
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?,
|
||||
amount,
|
||||
currency,
|
||||
payment_id: payment_intent.payment_id,
|
||||
merchant_name,
|
||||
@ -145,15 +182,16 @@ pub async fn intiate_payment_link_flow(
|
||||
merchant_description: payment_intent.description,
|
||||
};
|
||||
|
||||
let js_script = get_js_script(payment_details)?;
|
||||
let css_script = get_color_scheme_css(payment_link_config.clone());
|
||||
let js_script = get_js_script(api_models::payments::PaymentLinkData::PaymentLinkDetails(
|
||||
payment_details,
|
||||
))?;
|
||||
let payment_link_data = services::PaymentLinkFormData {
|
||||
js_script,
|
||||
sdk_url: state.conf.payment_link.sdk_url.clone(),
|
||||
css_script,
|
||||
};
|
||||
Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new(
|
||||
payment_link_data,
|
||||
services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data),
|
||||
)))
|
||||
}
|
||||
|
||||
@ -161,13 +199,11 @@ pub async fn intiate_payment_link_flow(
|
||||
The get_js_script function is used to inject dynamic value to payment_link sdk, which is unique to every payment.
|
||||
*/
|
||||
|
||||
fn get_js_script(
|
||||
payment_details: api_models::payments::PaymentLinkDetails,
|
||||
) -> RouterResult<String> {
|
||||
fn get_js_script(payment_details: api_models::payments::PaymentLinkData) -> RouterResult<String> {
|
||||
let payment_details_str = serde_json::to_string(&payment_details)
|
||||
.into_report()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to serialize PaymentLinkDetails")?;
|
||||
.attach_printable("Failed to serialize PaymentLinkData")?;
|
||||
Ok(format!("window.__PAYMENT_DETAILS = {payment_details_str};"))
|
||||
}
|
||||
|
||||
@ -218,11 +254,11 @@ pub async fn list_payment_link(
|
||||
}
|
||||
|
||||
pub fn check_payment_link_status(
|
||||
max_age: PrimitiveDateTime,
|
||||
payment_link_expiry: PrimitiveDateTime,
|
||||
) -> api_models::payments::PaymentLinkStatus {
|
||||
let curr_time = common_utils::date_time::now();
|
||||
|
||||
if curr_time > max_age {
|
||||
if curr_time > payment_link_expiry {
|
||||
api_models::payments::PaymentLinkStatus::Expired
|
||||
} else {
|
||||
api_models::payments::PaymentLinkStatus::Active
|
||||
@ -369,3 +405,10 @@ fn capitalize_first_char(s: &str) -> String {
|
||||
s.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn check_payment_link_invalid_conditions(
|
||||
intent_status: &storage_enums::IntentStatus,
|
||||
not_allowed_statuses: &[storage_enums::IntentStatus],
|
||||
) -> bool {
|
||||
not_allowed_statuses.contains(intent_status)
|
||||
}
|
||||
|
||||
@ -63,7 +63,6 @@
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
@ -133,7 +132,6 @@
|
||||
#hyper-checkout-cart-image {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
@ -344,10 +342,6 @@
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.payNow {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.page-spinner {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
@ -605,9 +599,38 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#submit {
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
background-color: var(--primary-color);
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#submit.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#submit-spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 4px solid #fff;
|
||||
border-bottom-color: #ff3d00;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: loading 1s linear infinite;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1400px) {
|
||||
body {
|
||||
overflow: scroll;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.hyper-checkout {
|
||||
@ -720,6 +743,7 @@
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
min-width: 300px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#payment-form-wrap {
|
||||
@ -748,7 +772,7 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<body class="hide-scrollbar">
|
||||
<!-- SVG ICONS -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" display="none">
|
||||
<defs>
|
||||
@ -920,7 +944,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hyper-checkout">
|
||||
<div 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>
|
||||
@ -1035,8 +1059,12 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<form id="payment-form">
|
||||
<form id="payment-form" onclick="handleSubmit(); return false;">
|
||||
<div id="unified-checkout"></div>
|
||||
<button id="submit" class="hidden">
|
||||
<span id="submit-spinner" class="hidden"></span>
|
||||
<span id="submit-button-text">Pay now</span>
|
||||
</button>
|
||||
<div id="payment-message" class="hidden"></div>
|
||||
</form>
|
||||
</div>
|
||||
@ -1060,7 +1088,7 @@
|
||||
window.state = {
|
||||
prevHeight: window.innerHeight,
|
||||
prevWidth: window.innerWidth,
|
||||
isMobileView: window.innerWidth <= 1200,
|
||||
isMobileView: window.innerWidth <= 1400,
|
||||
currentScreen: "payment_link",
|
||||
};
|
||||
|
||||
@ -1088,9 +1116,9 @@
|
||||
}
|
||||
|
||||
// Render UI
|
||||
renderPaymentDetails();
|
||||
renderSDKHeader();
|
||||
renderCart();
|
||||
renderPaymentDetails(paymentDetails);
|
||||
renderSDKHeader(paymentDetails);
|
||||
renderCart(paymentDetails);
|
||||
|
||||
// Deal w loaders
|
||||
show("#sdk-spinner");
|
||||
@ -1098,7 +1126,7 @@
|
||||
hide("#unified-checkout");
|
||||
|
||||
// Add event listeners
|
||||
initializeEventListeners();
|
||||
initializeEventListeners(paymentDetails);
|
||||
|
||||
// Initialize SDK
|
||||
if (window.Hyper) {
|
||||
@ -1115,30 +1143,46 @@
|
||||
}
|
||||
boot();
|
||||
|
||||
function initializeEventListeners() {
|
||||
var primaryColor = window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--primary-color");
|
||||
function initializeEventListeners(paymentDetails) {
|
||||
var primaryColor = paymentDetails.theme;
|
||||
var lighterColor = adjustLightness(primaryColor, 1.4);
|
||||
var darkerColor = adjustLightness(primaryColor, 0.8);
|
||||
var contrastBWColor = invert(primaryColor, true);
|
||||
var contrastingTone =
|
||||
Array.isArray(a) && a.length > 4 ? darkerColor : lighterColor;
|
||||
var hyperCheckoutNode = document.getElementById(
|
||||
"hyper-checkout-payment"
|
||||
);
|
||||
var hyperCheckoutCartImageNode = document.getElementById(
|
||||
"hyper-checkout-cart-image"
|
||||
);
|
||||
var hyperCheckoutFooterNode = document.getElementById(
|
||||
"hyper-checkout-payment-footer"
|
||||
);
|
||||
var statusRedirectTextNode = document.getElementById(
|
||||
"hyper-checkout-status-redirect-message"
|
||||
);
|
||||
var submitButtonNode = document.getElementById("submit");
|
||||
var submitButtonLoaderNode = document.getElementById("submit-spinner");
|
||||
|
||||
if (window.innerWidth <= 1200) {
|
||||
if (submitButtonLoaderNode instanceof HTMLSpanElement) {
|
||||
submitButtonLoaderNode.style.borderBottomColor = contrastingTone;
|
||||
}
|
||||
|
||||
if (submitButtonNode instanceof HTMLButtonElement) {
|
||||
submitButtonNode.style.color = contrastBWColor;
|
||||
}
|
||||
|
||||
if (hyperCheckoutCartImageNode instanceof HTMLDivElement) {
|
||||
hyperCheckoutCartImageNode.style.backgroundColor = contrastingTone;
|
||||
}
|
||||
|
||||
if (window.innerWidth <= 1400) {
|
||||
statusRedirectTextNode.style.color = "#333333";
|
||||
hyperCheckoutNode.style.color = contrastBWColor;
|
||||
var a = lighterColor.match(/[fF]/gi);
|
||||
hyperCheckoutFooterNode.style.backgroundColor =
|
||||
Array.isArray(a) && a.length > 4 ? darkerColor : lighterColor;
|
||||
} else if (window.innerWidth > 1200) {
|
||||
hyperCheckoutFooterNode.style.backgroundColor = contrastingTone;
|
||||
} else if (window.innerWidth > 1400) {
|
||||
statusRedirectTextNode.style.color = contrastBWColor;
|
||||
hyperCheckoutNode.style.color = "#333333";
|
||||
hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5";
|
||||
@ -1147,7 +1191,7 @@
|
||||
window.addEventListener("resize", function (event) {
|
||||
var currentHeight = window.innerHeight;
|
||||
var currentWidth = window.innerWidth;
|
||||
if (currentWidth <= 1200 && window.state.prevWidth > 1200) {
|
||||
if (currentWidth <= 1400 && window.state.prevWidth > 1400) {
|
||||
hide("#hyper-checkout-cart");
|
||||
if (window.state.currentScreen === "payment_link") {
|
||||
show("#hyper-footer");
|
||||
@ -1162,7 +1206,7 @@
|
||||
error
|
||||
);
|
||||
}
|
||||
} else if (currentWidth > 1200 && window.state.prevWidth <= 1200) {
|
||||
} else if (currentWidth > 1400 && window.state.prevWidth <= 1400) {
|
||||
if (window.state.currentScreen === "payment_link") {
|
||||
hide("#hyper-footer");
|
||||
}
|
||||
@ -1178,16 +1222,17 @@
|
||||
|
||||
window.state.prevHeight = currentHeight;
|
||||
window.state.prevWidth = currentWidth;
|
||||
window.state.isMobileView = currentWidth <= 1200;
|
||||
window.state.isMobileView = currentWidth <= 1400;
|
||||
});
|
||||
}
|
||||
|
||||
function showSDK() {
|
||||
checkStatus()
|
||||
function showSDK(paymentDetails) {
|
||||
checkStatus(paymentDetails)
|
||||
.then(function (res) {
|
||||
if (res.showSdk) {
|
||||
show("#hyper-checkout-sdk");
|
||||
show("#hyper-checkout-details");
|
||||
show("#submit");
|
||||
} else {
|
||||
hide("#hyper-checkout-details");
|
||||
hide("#hyper-checkout-sdk");
|
||||
@ -1195,6 +1240,8 @@
|
||||
hide("#hyper-footer");
|
||||
window.state.currentScreen = "status";
|
||||
}
|
||||
show("#unified-checkout");
|
||||
hide("#sdk-spinner");
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("Failed to check status", err);
|
||||
@ -1217,14 +1264,13 @@
|
||||
colorBackground: "rgb(255, 255, 255)",
|
||||
},
|
||||
};
|
||||
hyper = window.Hyper(pub_key);
|
||||
hyper = window.Hyper(pub_key, { isPreloadEnabled: false });
|
||||
widgets = hyper.widgets({
|
||||
appearance: appearance,
|
||||
clientSecret: client_secret,
|
||||
});
|
||||
var unifiedCheckoutOptions = {
|
||||
layout: "tabs",
|
||||
sdkHandleConfirmPayment: true,
|
||||
branding: "never",
|
||||
wallets: {
|
||||
walletReturnUrl: paymentDetails.return_url,
|
||||
@ -1237,35 +1283,7 @@
|
||||
};
|
||||
unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions);
|
||||
mountUnifiedCheckout("#unified-checkout");
|
||||
|
||||
// Add event listener for SDK iframe mutations
|
||||
var orcaIFrame = document.getElementById(
|
||||
"orca-payment-element-iframeRef-unified-checkout"
|
||||
);
|
||||
var callback = function (mutationList, observer) {
|
||||
for (var i = 0; i < mutationList.length; i++) {
|
||||
var mutation = mutationList[i];
|
||||
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "style"
|
||||
) {
|
||||
show("#unified-checkout");
|
||||
hide("#sdk-spinner");
|
||||
}
|
||||
}
|
||||
};
|
||||
var observer = new MutationObserver(callback);
|
||||
observer.observe(orcaIFrame, { attributes: true });
|
||||
|
||||
// Handle button press callback
|
||||
var paymentElement = widgets.getElement("payment");
|
||||
if (paymentElement) {
|
||||
paymentElement.on("confirmTriggered", function (event) {
|
||||
handleSubmit(event);
|
||||
});
|
||||
}
|
||||
showSDK();
|
||||
showSDK(paymentDetails);
|
||||
}
|
||||
|
||||
// Util functions
|
||||
@ -1277,6 +1295,14 @@
|
||||
|
||||
function handleSubmit(e) {
|
||||
var paymentDetails = window.__PAYMENT_DETAILS;
|
||||
|
||||
// Update button loader
|
||||
hide("#submit-button-text");
|
||||
show("#submit-spinner");
|
||||
var submitButtonNode = document.getElementById("submit");
|
||||
submitButtonNode.disabled = true;
|
||||
submitButtonNode.classList.add("disabled");
|
||||
|
||||
hyper
|
||||
.confirmPayment({
|
||||
widgets: widgets,
|
||||
@ -1293,9 +1319,6 @@
|
||||
} else {
|
||||
showMessage("An unexpected error occurred.");
|
||||
}
|
||||
|
||||
// Re-initialize SDK
|
||||
mountUnifiedCheckout("#unified-checkout");
|
||||
} else {
|
||||
// This point will only be reached if there is an immediate error occurring while confirming the payment. Otherwise, your customer will be redirected to your 'return_url'.
|
||||
// For some payment flows such as Sofort, iDEAL, your customer will be redirected to an intermediate page to complete authorization of the payment, and then redirected to the 'return_url'.
|
||||
@ -1320,13 +1343,18 @@
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("Error confirming payment_intent", error);
|
||||
})
|
||||
.finally(() => {
|
||||
hide("#submit-spinner");
|
||||
show("#submit-button-text");
|
||||
submitButtonNode.disabled = false;
|
||||
submitButtonNode.classList.remove("disabled");
|
||||
});
|
||||
}
|
||||
|
||||
// Fetches the payment status after payment submission
|
||||
function checkStatus() {
|
||||
function checkStatus(paymentDetails) {
|
||||
return new window.Promise(function (resolve, reject) {
|
||||
var paymentDetails = window.__PAYMENT_DETAILS;
|
||||
var res = {
|
||||
showSdk: true,
|
||||
};
|
||||
@ -1669,9 +1697,7 @@
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function renderPaymentDetails() {
|
||||
var paymentDetails = window.__PAYMENT_DETAILS;
|
||||
|
||||
function renderPaymentDetails(paymentDetails) {
|
||||
// Create price node
|
||||
var priceNode = document.createElement("div");
|
||||
priceNode.className = "hyper-checkout-payment-price";
|
||||
@ -1720,8 +1746,7 @@
|
||||
footerNode.append(paymentExpiryNode);
|
||||
}
|
||||
|
||||
function renderCart() {
|
||||
var paymentDetails = window.__PAYMENT_DETAILS;
|
||||
function renderCart(paymentDetails) {
|
||||
var orderDetails = paymentDetails.order_details;
|
||||
|
||||
// Cart items
|
||||
@ -1749,7 +1774,9 @@
|
||||
if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) {
|
||||
var expandButtonNode = document.createElement("div");
|
||||
expandButtonNode.className = "hyper-checkout-cart-button";
|
||||
expandButtonNode.onclick = handleCartView;
|
||||
expandButtonNode.onclick = () => {
|
||||
handleCartView(paymentDetails);
|
||||
};
|
||||
var buttonImageNode = document.createElement("svg");
|
||||
buttonImageNode.id = "hyper-checkout-cart-button-arrow";
|
||||
buttonImageNode.innerHTML =
|
||||
@ -1822,8 +1849,7 @@
|
||||
cartItemsNode.append(itemWrapperNode);
|
||||
}
|
||||
|
||||
function handleCartView() {
|
||||
var paymentDetails = window.__PAYMENT_DETAILS;
|
||||
function handleCartView(paymentDetails) {
|
||||
var orderDetails = paymentDetails.order_details;
|
||||
var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE =
|
||||
paymentDetails.max_items_visible_after_collapse;
|
||||
@ -1911,9 +1937,7 @@
|
||||
show("#hyper-checkout-cart");
|
||||
}
|
||||
|
||||
function renderSDKHeader() {
|
||||
var paymentDetails = window.__PAYMENT_DETAILS;
|
||||
|
||||
function renderSDKHeader(paymentDetails) {
|
||||
// SDK headers' items
|
||||
var sdkHeaderItemNode = document.createElement("div");
|
||||
sdkHeaderItemNode.className = "hyper-checkout-sdk-items";
|
||||
|
||||
355
crates/router/src/core/payment_link/status.html
Normal file
355
crates/router/src/core/payment_link/status.html
Normal file
@ -0,0 +1,355 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>404 Not Found</title>
|
||||
<style>
|
||||
{{ css_color_scheme }}
|
||||
|
||||
body,
|
||||
body > div {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Montserrat";
|
||||
background-color: var(--primary-color);
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body > div {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-wrap {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
font-family: "Montserrat";
|
||||
width: auto;
|
||||
min-width: 400px;
|
||||
max-width: 800px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#hyper-checkout-status-header {
|
||||
max-width: 1200px;
|
||||
border-radius: 3px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
#hyper-checkout-status-header,
|
||||
#hyper-checkout-status-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-amount {
|
||||
font-family: "Montserrat";
|
||||
font-size: 35px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-merchant-logo {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 5px;
|
||||
padding: 9px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
#hyper-checkout-status-content {
|
||||
height: 100%;
|
||||
flex-flow: column;
|
||||
min-height: 500px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-image {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-text {
|
||||
text-align: center;
|
||||
font-size: 21px;
|
||||
font-weight: 600;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-message {
|
||||
text-align: center;
|
||||
font-size: 12px !important;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-details {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e6e6e6;
|
||||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
|
||||
.hyper-checkout-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.hyper-checkout-status-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.hyper-checkout-item-header {
|
||||
min-width: 13ch;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hyper-checkout-item-value {
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ellipsis-container-2 {
|
||||
height: 2.5em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1136px) {
|
||||
.info {
|
||||
flex-flow: column;
|
||||
align-self: flex-start;
|
||||
align-items: flex-start;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800"
|
||||
/>
|
||||
<script>
|
||||
{{ payment_details_js_script }}
|
||||
|
||||
function boot() {
|
||||
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);
|
||||
}
|
||||
|
||||
var statusDetails = {
|
||||
imageSource: "",
|
||||
message: "",
|
||||
status: "",
|
||||
items: [],
|
||||
};
|
||||
|
||||
var paymentId = createItem("Ref Id", paymentDetails.payment_id);
|
||||
statusDetails.items.push(paymentId);
|
||||
|
||||
// Decide screen to render
|
||||
switch (paymentDetails.payment_link_status) {
|
||||
case "expired": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png";
|
||||
statusDetails.status = "Payment Link Expired!";
|
||||
statusDetails.message = "This payment link is expired.";
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
statusDetails.status = paymentDetails.intent_status;
|
||||
// Render status screen
|
||||
switch (paymentDetails.intent_status) {
|
||||
case "succeeded": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/5BOmYVl.png";
|
||||
statusDetails.message =
|
||||
"We have successfully received your payment";
|
||||
break;
|
||||
}
|
||||
|
||||
case "processing": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png";
|
||||
statusDetails.message =
|
||||
"Sorry! Your payment is taking longer than expected. Please check back again in sometime.";
|
||||
statusDetails.status = "Payment Pending";
|
||||
break;
|
||||
}
|
||||
|
||||
case "failed": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png";
|
||||
statusDetails.status = "Payment Failed!";
|
||||
var errorCodeNode = createItem(
|
||||
"Error code",
|
||||
paymentDetails.error_code
|
||||
);
|
||||
var errorMessageNode = createItem(
|
||||
"Error message",
|
||||
paymentDetails.error_message
|
||||
);
|
||||
// @ts-ignore
|
||||
statusDetails.items.push(errorMessageNode, errorCodeNode);
|
||||
break;
|
||||
}
|
||||
|
||||
case "cancelled": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png";
|
||||
statusDetails.status = "Payment Cancelled";
|
||||
break;
|
||||
}
|
||||
|
||||
case "requires_merchant_action": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png";
|
||||
statusDetails.status = "Payment under review";
|
||||
break;
|
||||
}
|
||||
|
||||
case "requires_capture": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png";
|
||||
statusDetails.status = "Payment Pending";
|
||||
break;
|
||||
}
|
||||
|
||||
case "partially_captured": {
|
||||
statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png";
|
||||
statusDetails.message = "Partial payment was captured.";
|
||||
statusDetails.status = "Partial Payment Pending";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png";
|
||||
statusDetails.status = "Something went wrong";
|
||||
// Error details
|
||||
if (typeof paymentDetails.error === "object") {
|
||||
var errorCodeNode = createItem(
|
||||
"Error Code",
|
||||
paymentDetails.error.code
|
||||
);
|
||||
var errorMessageNode = createItem(
|
||||
"Error Message",
|
||||
paymentDetails.error.message
|
||||
);
|
||||
// @ts-ignore
|
||||
statusDetails.items.push(errorMessageNode, errorCodeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form header
|
||||
var hyperCheckoutImageNode = document.createElement("img");
|
||||
var hyperCheckoutAmountNode = document.createElement("div");
|
||||
|
||||
hyperCheckoutImageNode.src = paymentDetails.merchant_logo;
|
||||
hyperCheckoutImageNode.className =
|
||||
"hyper-checkout-status-merchant-logo";
|
||||
hyperCheckoutAmountNode.innerText =
|
||||
paymentDetails.currency + " " + paymentDetails.amount;
|
||||
hyperCheckoutAmountNode.className = "hyper-checkout-status-amount";
|
||||
var hyperCheckoutHeaderNode = document.getElementById(
|
||||
"hyper-checkout-status-header"
|
||||
);
|
||||
if (hyperCheckoutHeaderNode instanceof HTMLDivElement) {
|
||||
hyperCheckoutHeaderNode.append(
|
||||
hyperCheckoutAmountNode,
|
||||
hyperCheckoutImageNode
|
||||
);
|
||||
}
|
||||
|
||||
// Form and append items
|
||||
var hyperCheckoutStatusTextNode = document.createElement("div");
|
||||
hyperCheckoutStatusTextNode.innerText = statusDetails.status;
|
||||
hyperCheckoutStatusTextNode.className = "hyper-checkout-status-text";
|
||||
|
||||
var merchantLogoNode = document.createElement("img");
|
||||
merchantLogoNode.src = statusDetails.imageSource;
|
||||
merchantLogoNode.className = "hyper-checkout-status-image";
|
||||
|
||||
var hyperCheckoutStatusMessageNode = document.createElement("div");
|
||||
hyperCheckoutStatusMessageNode.innerText = statusDetails.message;
|
||||
|
||||
var hyperCheckoutDetailsNode = document.createElement("div");
|
||||
hyperCheckoutDetailsNode.className = "hyper-checkout-status-details";
|
||||
if (hyperCheckoutDetailsNode instanceof HTMLDivElement) {
|
||||
hyperCheckoutDetailsNode.append(...statusDetails.items);
|
||||
}
|
||||
|
||||
var hyperCheckoutContentNode = document.getElementById(
|
||||
"hyper-checkout-status-content"
|
||||
);
|
||||
if (hyperCheckoutContentNode instanceof HTMLDivElement) {
|
||||
hyperCheckoutContentNode.prepend(
|
||||
merchantLogoNode,
|
||||
hyperCheckoutStatusTextNode,
|
||||
hyperCheckoutDetailsNode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createItem(heading, value) {
|
||||
var itemNode = document.createElement("div");
|
||||
itemNode.className = "hyper-checkout-status-item";
|
||||
var headerNode = document.createElement("div");
|
||||
headerNode.className = "hyper-checkout-item-header";
|
||||
headerNode.innerText = heading;
|
||||
var valueNode = document.createElement("div");
|
||||
valueNode.classList.add("hyper-checkout-item-value");
|
||||
// valueNode.classList.add("ellipsis-container-2");
|
||||
valueNode.innerText = value;
|
||||
itemNode.append(headerNode);
|
||||
itemNode.append(valueNode);
|
||||
return itemNode;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body onload="boot()">
|
||||
<div>
|
||||
<div class="hyper-checkout-status-wrap">
|
||||
<div id="hyper-checkout-status-header"></div>
|
||||
<div id="hyper-checkout-status-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -733,11 +733,17 @@ pub enum ApplicationResponse<R> {
|
||||
TextPlain(String),
|
||||
JsonForRedirection(api::RedirectionResponse),
|
||||
Form(Box<RedirectionFormData>),
|
||||
PaymenkLinkForm(Box<PaymentLinkFormData>),
|
||||
PaymenkLinkForm(Box<PaymentLinkAction>),
|
||||
FileData((Vec<u8>, mime::Mime)),
|
||||
JsonWithHeaders((R, Vec<(String, String)>)),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum PaymentLinkAction {
|
||||
PaymentLinkFormData(PaymentLinkFormData),
|
||||
PaymentLinkStatus(PaymentLinkStatusData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PaymentLinkFormData {
|
||||
pub js_script: String,
|
||||
@ -745,6 +751,12 @@ pub struct PaymentLinkFormData {
|
||||
pub sdk_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PaymentLinkStatusData {
|
||||
pub js_script: String,
|
||||
pub css_script: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct RedirectionFormData {
|
||||
pub redirect_form: RedirectForm,
|
||||
@ -1051,16 +1063,32 @@ where
|
||||
.map_into_boxed_body()
|
||||
}
|
||||
|
||||
Ok(ApplicationResponse::PaymenkLinkForm(payment_link_data)) => {
|
||||
match build_payment_link_html(*payment_link_data) {
|
||||
Ok(rendered_html) => http_response_html_data(rendered_html),
|
||||
Err(_) => http_response_err(
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Error while rendering payment link html page"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
Ok(ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => {
|
||||
match *boxed_payment_link_data {
|
||||
PaymentLinkAction::PaymentLinkFormData(payment_link_data) => {
|
||||
match build_payment_link_html(payment_link_data) {
|
||||
Ok(rendered_html) => http_response_html_data(rendered_html),
|
||||
Err(_) => http_response_err(
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Error while rendering payment link html page"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
}
|
||||
}
|
||||
PaymentLinkAction::PaymentLinkStatus(payment_link_data) => {
|
||||
match get_payment_link_status(payment_link_data) {
|
||||
Ok(rendered_html) => http_response_html_data(rendered_html),
|
||||
Err(_) => http_response_err(
|
||||
r#"{
|
||||
"error": {
|
||||
"message": "Error while rendering payment link status page"
|
||||
}
|
||||
}"#,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1634,6 +1662,26 @@ fn get_hyper_loader_sdk(sdk_url: &str) -> String {
|
||||
format!("<script src=\"{sdk_url}\" onload=\"initializeSDK()\"></script>")
|
||||
}
|
||||
|
||||
pub fn get_payment_link_status(
|
||||
payment_link_data: PaymentLinkStatusData,
|
||||
) -> CustomResult<String, errors::ApiErrorResponse> {
|
||||
let html_template = include_str!("../core/payment_link/status.html").to_string();
|
||||
let mut tera = Tera::default();
|
||||
let _ = tera.add_raw_template("payment_link_status", &html_template);
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("css_color_scheme", &payment_link_data.css_script);
|
||||
context.insert("payment_details_js_script", &payment_link_data.js_script);
|
||||
|
||||
match tera.render("payment_link_status", &context) {
|
||||
Ok(rendered_html) => Ok(rendered_html),
|
||||
Err(tera_error) => {
|
||||
crate::logger::warn!("{tera_error}");
|
||||
Err(errors::ApiErrorResponse::InternalServerError)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
|
||||
@ -15,7 +15,8 @@ pub(crate) trait PaymentLinkResponseExt: Sized {
|
||||
impl PaymentLinkResponseExt for RetrievePaymentLinkResponse {
|
||||
async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult<Self> {
|
||||
let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| {
|
||||
common_utils::date_time::now()
|
||||
payment_link
|
||||
.created_at
|
||||
.saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY))
|
||||
});
|
||||
let status = payment_link::check_payment_link_status(session_expiry);
|
||||
|
||||
@ -8760,8 +8760,8 @@
|
||||
"PaymentLinkStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Active",
|
||||
"Expired"
|
||||
"active",
|
||||
"expired"
|
||||
]
|
||||
},
|
||||
"PaymentListConstraints": {
|
||||
|
||||
Reference in New Issue
Block a user