refactor(payment_link): segregated payment link in html css js files, sdk over flow issue, surcharge bug, block SPM customer call for payment link (#3410)

Co-authored-by: Kashif <mohammed.kashif@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Kashif <kashif.dev@protonmail.com>
Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com>
This commit is contained in:
Sahkal Poddar
2024-01-30 16:12:01 +05:30
committed by GitHub
parent 46c1822d0e
commit a7bc8c655f
16 changed files with 2743 additions and 2509 deletions

View File

@ -1,4 +1,4 @@
use api_models::admin as admin_types;
use api_models::{admin as admin_types, payments::PaymentLinkStatusWrap};
use common_utils::{
consts::{
DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT,
@ -89,10 +89,24 @@ pub async fn intiate_payment_link_flow(
}
};
let profile_id = payment_link
.profile_id
.or(payment_intent.profile_id)
.ok_or(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Profile id missing in payment link and payment intent")?;
let business_profile = db
.find_business_profile_by_profile_id(&profile_id)
.await
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
id: profile_id.to_string(),
})?;
let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() {
payment_create_return_url
} else {
merchant_account
business_profile
.return_url
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
field_name: "return_url",
@ -121,7 +135,7 @@ pub async fn intiate_payment_link_flow(
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(
let is_terminal_state = check_payment_link_invalid_conditions(
&payment_intent.status,
&[
storage_enums::IntentStatus::Cancelled,
@ -130,9 +144,26 @@ pub async fn intiate_payment_link_flow(
storage_enums::IntentStatus::RequiresCapture,
storage_enums::IntentStatus::RequiresMerchantAction,
storage_enums::IntentStatus::Succeeded,
storage_enums::IntentStatus::PartiallyCaptured,
],
) || payment_link_status == api_models::payments::PaymentLinkStatus::Expired
);
if is_terminal_state || payment_link_status == api_models::payments::PaymentLinkStatus::Expired
{
let status = match payment_link_status {
api_models::payments::PaymentLinkStatus::Active => {
PaymentLinkStatusWrap::IntentStatus(payment_intent.status)
}
api_models::payments::PaymentLinkStatus::Expired => {
if is_terminal_state {
PaymentLinkStatusWrap::IntentStatus(payment_intent.status)
} else {
PaymentLinkStatusWrap::PaymentLinkStatus(
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(
@ -148,12 +179,14 @@ pub async fn intiate_payment_link_flow(
currency,
payment_id: payment_intent.payment_id,
merchant_name,
merchant_logo: payment_link_config.clone().logo,
merchant_logo: payment_link_config.logo.clone(),
created: payment_link.created_at,
intent_status: payment_intent.status,
payment_link_status,
status,
error_code: payment_attempt.error_code,
error_message: payment_attempt.error_message,
redirect: false,
theme: payment_link_config.theme.clone(),
return_url: return_url.clone(),
};
let js_script = get_js_script(
api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details),
@ -177,11 +210,11 @@ pub async fn intiate_payment_link_flow(
session_expiry,
pub_key,
client_secret,
merchant_logo: payment_link_config.clone().logo,
merchant_logo: payment_link_config.logo.clone(),
max_items_visible_after_collapse: 3,
theme: payment_link_config.clone().theme,
theme: payment_link_config.theme.clone(),
merchant_description: payment_intent.description,
sdk_layout: payment_link_config.clone().sdk_layout,
sdk_layout: payment_link_config.sdk_layout.clone(),
};
let js_script = get_js_script(api_models::payments::PaymentLinkData::PaymentLinkDetails(
@ -425,3 +458,123 @@ fn check_payment_link_invalid_conditions(
) -> bool {
not_allowed_statuses.contains(intent_status)
}
pub async fn get_payment_link_status(
state: AppState,
merchant_account: domain::MerchantAccount,
merchant_id: String,
payment_id: String,
) -> RouterResponse<services::PaymentLinkFormData> {
let db = &*state.store;
let payment_intent = db
.find_payment_intent_by_payment_id_merchant_id(
&payment_id,
&merchant_id,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
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_link_id = payment_intent
.payment_link_id
.get_required_value("payment_link_id")
.change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?;
let merchant_name_from_merchant_account = merchant_account
.merchant_name
.clone()
.map(|merchant_name| merchant_name.into_inner().peek().to_owned())
.unwrap_or_default();
let payment_link = db
.find_payment_link_by_payment_link_id(&payment_link_id)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?;
let payment_link_config = if let Some(pl_config_value) = payment_link.payment_link_config {
extract_payment_link_config(pl_config_value)?
} else {
admin_types::PaymentLinkConfig {
theme: DEFAULT_BACKGROUND_COLOR.to_string(),
logo: DEFAULT_MERCHANT_LOGO.to_string(),
seller_name: merchant_name_from_merchant_account,
sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(),
}
};
let currency =
payment_intent
.currency
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
field_name: "currency",
})?;
let amount = currency
.to_currency_base_unit(payment_attempt.net_amount)
.into_report()
.change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?;
// 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 profile_id = payment_link
.profile_id
.or(payment_intent.profile_id)
.ok_or(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Profile id missing in payment link and payment intent")?;
let business_profile = db
.find_business_profile_by_profile_id(&profile_id)
.await
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
id: profile_id.to_string(),
})?;
let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() {
payment_create_return_url
} else {
business_profile
.return_url
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
field_name: "return_url",
})?
};
let payment_details = api_models::payments::PaymentLinkStatusDetails {
amount,
currency,
payment_id: payment_intent.payment_id,
merchant_name,
merchant_logo: payment_link_config.logo.clone(),
created: payment_link.created_at,
status: PaymentLinkStatusWrap::IntentStatus(payment_intent.status),
error_code: payment_attempt.error_code,
error_message: payment_attempt.error_message,
redirect: true,
theme: payment_link_config.theme.clone(),
return_url,
};
let js_script = get_js_script(
api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details),
)?;
let payment_link_status_data = services::PaymentLinkStatusData {
js_script,
css_script,
};
Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new(
services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_status_data),
)))
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,785 @@
{{ css_color_scheme }}
html,
body {
height: 100%;
overflow: hidden;
}
body {
display: flex;
flex-flow: column;
align-items: center;
justify-content: flex-start;
margin: 0;
color: #333333;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.hide-scrollbar {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
/* For ellipsis on text lines */
.ellipsis-container-3 {
height: 4em;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
white-space: normal;
}
.hidden {
display: none !important;
}
.hyper-checkout {
display: flex;
background-color: #f8f9fb;
color: #333333;
width: 100%;
height: 100%;
overflow: scroll;
}
#hyper-footer {
width: 100vw;
display: flex;
justify-content: center;
padding: 20px 0;
}
.main {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
min-width: 600px;
width: 50vw;
}
#hyper-checkout-details {
font-family: "Montserrat";
}
.hyper-checkout-payment {
min-width: 600px;
box-shadow: 0px 0px 5px #d1d1d1;
border-radius: 8px;
background-color: #fefefe;
}
.hyper-checkout-payment-content-details {
display: flex;
flex-flow: column;
justify-content: space-between;
align-content: space-between;
}
.content-details-wrap {
display: flex;
flex-flow: row;
margin: 20px 20px 30px 20px;
justify-content: space-between;
}
.hyper-checkout-payment-price {
font-weight: 700;
font-size: 40px;
height: 64px;
display: flex;
align-items: center;
}
#hyper-checkout-payment-merchant-details {
margin-top: 5px;
}
.hyper-checkout-payment-merchant-name {
font-weight: 600;
font-size: 19px;
}
.hyper-checkout-payment-ref {
font-size: 12px;
margin-top: 5px;
}
.hyper-checkout-image-header {
display: flex;
justify-content: space-between;
align-items: center;
}
#hyper-checkout-merchant-image,
#hyper-checkout-cart-image {
height: 64px;
width: 64px;
border-radius: 4px;
display: flex;
align-self: flex-start;
align-items: center;
justify-content: center;
}
#hyper-checkout-merchant-image > img {
height: 48px;
width: 48px;
}
#hyper-checkout-cart-image {
display: none;
cursor: pointer;
height: 60px;
width: 60px;
border-radius: 100px;
background-color: #f5f5f5;
}
#hyper-checkout-payment-footer {
margin-top: 20px;
background-color: #f5f5f5;
font-size: 13px;
font-weight: 500;
padding: 12px 20px;
border-radius: 0 0 8px 8px;
}
#hyper-checkout-cart {
display: flex;
flex-flow: column;
min-width: 600px;
margin-top: 40px;
max-height: 60vh;
}
#hyper-checkout-cart-items {
max-height: 291px;
overflow: scroll;
transition: all 0.3s ease;
}
.hyper-checkout-cart-header {
font-size: 15px;
display: flex;
flex-flow: row;
align-items: center;
}
.hyper-checkout-cart-header > span {
margin-left: 5px;
font-weight: 500;
}
.cart-close {
display: none;
cursor: pointer;
}
.hyper-checkout-cart-item {
display: flex;
flex-flow: row;
padding: 20px 0;
font-size: 15px;
}
.hyper-checkout-cart-product-image {
height: 56px;
width: 56px;
border-radius: 4px;
}
.hyper-checkout-card-item-name {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.hyper-checkout-card-item-quantity {
border: 1px solid #e6e6e6;
border-radius: 3px;
width: max-content;
padding: 5px 12px;
background-color: #fafafa;
font-size: 13px;
font-weight: 500;
}
.hyper-checkout-cart-product-details {
display: flex;
flex-flow: column;
margin-left: 15px;
justify-content: space-between;
width: 100%;
}
.hyper-checkout-card-item-price {
justify-self: flex-end;
font-weight: 600;
font-size: 16px;
padding-left: 30px;
text-align: end;
min-width: max-content;
}
.hyper-checkout-cart-item-divider {
height: 1px;
background-color: #e6e6e6;
}
.hyper-checkout-cart-button {
font-size: 12px;
font-weight: 500;
cursor: pointer;
align-self: flex-start;
display: flex;
align-content: flex-end;
gap: 3px;
text-decoration: none;
transition: text-decoration 0.3s;
margin-top: 10px;
}
.hyper-checkout-cart-button:hover {
text-decoration: underline;
}
#hyper-checkout-merchant-description {
font-size: 13px;
color: #808080;
}
.powered-by-hyper {
margin-top: 40px;
align-self: flex-start;
}
.hyper-checkout-sdk {
width: 50vw;
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;
justify-content: center;
}
#payment-form-wrap {
min-width: 300px;
width: 30vw;
padding: 20px;
background-color: white;
border-radius: 3px;
}
#hyper-checkout-sdk-header {
padding: 10px 10px 10px 22px;
display: flex;
align-items: flex-start;
justify-content: flex-start;
border-bottom: 1px solid #f2f2f2;
}
.hyper-checkout-sdk-header-logo {
height: 60px;
width: 60px;
background-color: white;
border-radius: 2px;
}
.hyper-checkout-sdk-header-logo > img {
height: 56px;
width: 56px;
margin: 2px;
}
.hyper-checkout-sdk-header-items {
display: flex;
flex-flow: column;
color: white;
font-size: 20px;
font-weight: 700;
}
.hyper-checkout-sdk-items {
margin-left: 10px;
}
.hyper-checkout-sdk-header-brand-name,
.hyper-checkout-sdk-header-amount {
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
font-family: "Montserrat";
justify-self: flex-start;
}
.hyper-checkout-sdk-header-amount {
font-weight: 800;
font-size: 25px;
}
.page-spinner {
position: absolute;
width: 100vw;
height: 100vh;
z-index: 3;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.sdk-spinner {
width: 100%;
height: 100%;
z-index: 3;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
width: 60px;
height: 60px;
}
.spinner div {
transform-origin: 30px 30px;
animation: spinner 1.2s linear infinite;
}
.spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 28px;
width: 4px;
height: 15px;
border-radius: 20%;
background: var(--primary-color);
}
.spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#hyper-checkout-status-canvas {
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background-color: var(--primary-color);
}
.hyper-checkout-status-wrap {
display: flex;
flex-flow: column;
font-family: "Montserrat";
width: auto;
min-width: 400px;
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;
}
#hyper-checkout-status-redirect-message {
margin-top: 20px;
font-family: "Montserrat";
font-size: 13px;
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes slide-from-right {
from {
right: -582px;
}
to {
right: 0;
}
}
@keyframes slide-to-right {
from {
right: 0;
}
to {
right: -582px;
}
}
#payment-message {
font-size: 12px;
font-weight: 500;
padding: 2%;
color: #ff0000;
font-family: "Montserrat";
}
#payment-form {
max-width: 560px;
width: 100%;
min-height: 500px;
max-height: 90vh;
height: 100%;
overflow: scroll;
margin: 0 auto;
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-y: scroll;
}
.hyper-checkout {
flex-flow: column;
margin: 0;
height: auto;
overflow: visible;
}
#hyper-checkout-payment-merchant-details {
margin-top: 20px;
}
.main {
width: auto;
min-width: 300px;
}
.hyper-checkout-payment {
min-width: 300px;
width: calc(100vw - 50px);
margin: 0;
padding: 25px;
border: 0;
border-radius: 0;
background-color: var(--primary-color);
display: flex;
flex-flow: column;
justify-self: flex-start;
align-self: flex-start;
}
.hyper-checkout-payment-content-details {
max-width: 520px;
width: 100%;
align-self: center;
margin-bottom: 0;
}
.content-details-wrap {
flex-flow: column;
flex-direction: column-reverse;
margin: 0;
}
#hyper-checkout-merchant-image {
background-color: white;
}
#hyper-checkout-cart-image {
display: flex;
}
.hyper-checkout-payment-price {
font-size: 48px;
margin-top: 20px;
}
.hyper-checkout-payment-merchant-name {
font-size: 18px;
}
#hyper-checkout-payment-footer {
border-radius: 50px;
width: max-content;
padding: 10px 20px;
}
#hyper-checkout-cart {
position: absolute;
top: 0;
right: 0;
z-index: 100;
margin: 0;
min-width: 300px;
max-width: 582px;
max-height: 100vh;
width: 100vw;
height: 100vh;
background-color: #f5f5f5;
box-shadow: 0px 10px 10px #aeaeae;
right: 0px;
animation: slide-from-right 0.3s linear;
}
.hyper-checkout-cart-header {
margin: 10px 0 0 10px;
}
.cart-close {
margin: 0 10px 0 auto;
display: inline;
}
#hyper-checkout-cart-items {
margin: 20px 20px 0 20px;
padding: 0;
}
.hyper-checkout-cart-button {
margin: 10px;
text-align: right;
}
.powered-by-hyper {
display: none;
}
#hyper-checkout-sdk {
background-color: transparent;
width: auto;
min-width: 300px;
box-shadow: none;
}
#payment-form-wrap {
min-width: 300px;
width: calc(100vw - 40px);
margin: 0;
padding: 25px 20px;
}
#hyper-checkout-status-canvas {
background-color: #fefefe;
}
.hyper-checkout-status-wrap {
min-width: 100vw;
width: 100vw;
}
#hyper-checkout-status-header {
max-width: calc(100% - 40px);
}
}

View File

@ -0,0 +1,197 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Payments requested by HyperSwitch</title>
<style>
{{rendered_css}}
</style>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800"
/>
</head>
<body class="hide-scrollbar">
<!-- SVG ICONS -->
<svg xmlns="http://www.w3.org/2000/svg" display="none">
<defs>
<symbol id="cart-icon-small">
<image href="https://live.hyperswitch.io/payment-link-assets/icons/cart-small.svg"/>
</symbol>
<symbol id="cart-icon-big">
<image href="https://live.hyperswitch.io/payment-link-assets/icons/cart-big.svg"/>
</symbol>
<symbol id="cart-close">
<image href="https://live.hyperswitch.io/payment-link-assets/icons/close.svg"/>
</symbol>
<symbol id="hyperswitch-brand">
<image href="https://live.hyperswitch.io/payment-link-assets/icons/powered-by-hyperswitch.svg" opacity="0.4"/>
</symbol>
<symbol id="arrow-down">
<image href="https://live.hyperswitch.io/payment-link-assets/icons/arrow-down.svg"/>
</symbol>
<symbol id="arrow-up">
<image href="https://live.hyperswitch.io/payment-link-assets/icons/arrow-up.svg"/>
</symbol>
</defs>
</svg>
<div id="page-spinner" class="page-spinner hidden">
<div class="spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<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>
<div id="hyper-checkout-status-content"></div>
</div>
<div id="hyper-checkout-status-redirect-message"></div>
</div>
<div class="main" id="hyper-checkout-details">
<div id="hyper-checkout-payment" class="hyper-checkout-payment">
<div class="hyper-checkout-payment-content-details">
<div class="content-details-wrap">
<div id="hyper-checkout-payment-context">
<div id="hyper-checkout-payment-merchant-details"></div>
</div>
<div class="hyper-checkout-image-header">
<div id="hyper-checkout-merchant-image"></div>
<div
id="hyper-checkout-cart-image"
onclick="viewCartInMobileView()"
>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="cart-icon"
>
<use
xlink:href="#cart-icon-big"
x="0"
y="0"
width="30"
height="30"
></use>
</svg>
</div>
</div>
</div>
<div id="hyper-checkout-payment-footer"></div>
</div>
</div>
<div id="hyper-checkout-cart" class="">
<div
id="hyper-checkout-cart-header"
class="hyper-checkout-cart-header"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="cart-icon"
>
<use
xlink:href="#cart-icon-small"
x="0"
y="0"
width="16"
height="16"
></use>
</svg>
<span>Your Cart</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 50"
width="25"
height="25"
class="cart-close"
onclick="hideCartInMobileView()"
>
<use
xlink:href="#cart-close"
x="0"
y="0"
width="50"
height="50"
></use>
</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">
<div id="payment-form-wrap">
<div id="sdk-spinner" class="sdk-spinner">
<div class="spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<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>
</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>
{{rendered_js}}
</script>
{{ hyperloader_sdk_link }}
</body>
</html>

View File

@ -0,0 +1,906 @@
// @ts-check
/**
* UTIL FUNCTIONS
*/
function adjustLightness(hexColor, factor) {
// Convert hex to RGB
var r = parseInt(hexColor.slice(1, 3), 16);
var g = parseInt(hexColor.slice(3, 5), 16);
var b = parseInt(hexColor.slice(5, 7), 16);
// Convert RGB to HSL
var hsl = rgbToHsl(r, g, b);
// Adjust lightness
hsl[2] = Math.max(0, Math.min(100, hsl[2] * factor));
// Convert HSL back to RGB
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Convert RGB to hex
var newHexColor = rgbToHex(rgb[0], rgb[1], rgb[2]);
return newHexColor;
}
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h = 1,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h * 360, s * 100, l * 100];
}
function hslToRgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
var r, g, b;
if (s === 0) {
r = g = b = l;
} else {
var hue2rgb = function (p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255];
}
function rgbToHex(r, g, b) {
var toHex = function (c) {
var hex = Math.round(c).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return "#" + toHex(r) + toHex(g) + toHex(b);
}
/**
* Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js
*/
function padz(str, len) {
if (len === void 0) {
len = 2;
}
return (new Array(len).join("0") + str).slice(-len);
}
function hexToRgbArray(hex) {
if (hex.slice(0, 1) === "#") hex = hex.slice(1);
var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i;
if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: "' + hex + '"');
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
return [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
];
}
function toRgbArray(c) {
if (!c) throw new Error("Invalid color value");
if (Array.isArray(c)) return c;
return typeof c === "string" ? hexToRgbArray(c) : [c.r, c.g, c.b];
}
function getLuminance(c) {
var i, x;
var a = [];
for (i = 0; i < c.length; i++) {
x = c[i] / 255;
a[i] = x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
}
function invertToBW(color, bw, asArr) {
var DEFAULT_BW = {
black: "#090302",
white: "#FFFFFC",
threshold: Math.sqrt(1.05 * 0.05) - 0.05,
};
var options = bw === true ? DEFAULT_BW : Object.assign({}, DEFAULT_BW, bw);
return getLuminance(color) > options.threshold
? asArr
? hexToRgbArray(options.black)
: options.black
: asArr
? hexToRgbArray(options.white)
: options.white;
}
function invert(color, bw) {
if (bw === void 0) {
bw = false;
}
color = toRgbArray(color);
if (bw) return invertToBW(color, bw);
return (
"#" +
color
.map(function (c) {
return padz((255 - c).toString(16));
})
.join("")
);
}
/**
* UTIL FUNCTIONS END HERE
*/
{{ payment_details_js_script }}
// @ts-ignore
window.state = {
prevHeight: window.innerHeight,
prevWidth: window.innerWidth,
isMobileView: window.innerWidth <= 1400,
currentScreen: "payment_link",
};
var widgets = null;
var unifiedCheckout = null;
// @ts-ignore
var pub_key = window.__PAYMENT_DETAILS.pub_key;
var hyper = null;
/**
* Trigger - init function invoked once the script tag is loaded
* Use
* - Update document's title
* - Update document's icon
* - Render and populate document with payment details and cart
* - Initialize event listeners for updating UI on screen size changes
* - Initialize SDK
**/
function boot() {
// @ts-ignore
var paymentDetails = window.__PAYMENT_DETAILS;
if (paymentDetails.merchant_name) {
document.title = "Payment requested by " + paymentDetails.merchant_name;
}
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);
}
// Render UI
renderPaymentDetails(paymentDetails);
renderSDKHeader(paymentDetails);
renderCart(paymentDetails);
// Deal w loaders
show("#sdk-spinner");
hide("#page-spinner");
hide("#unified-checkout");
// Add event listeners
initializeEventListeners(paymentDetails);
// Initialize SDK
// @ts-ignore
if (window.Hyper) {
initializeSDK();
}
// State specific functions
// @ts-ignore
if (window.state.isMobileView) {
show("#hyper-footer");
hide("#hyper-checkout-cart");
} else {
show("#hyper-checkout-cart");
}
}
boot();
/**
* Use - add event listeners for changing UI on screen resize
* @param {PaymentDetails} paymentDetails
*/
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 a = lighterColor.match(/[fF]/gi);
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 submitButtonNode = document.getElementById("submit");
var submitButtonLoaderNode = document.getElementById("submit-spinner");
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) {
if (hyperCheckoutNode instanceof HTMLDivElement) {
hyperCheckoutNode.style.color = contrastBWColor;
}
if (hyperCheckoutFooterNode instanceof HTMLDivElement) {
hyperCheckoutFooterNode.style.backgroundColor = contrastingTone;
}
} else if (window.innerWidth > 1400) {
if (hyperCheckoutNode instanceof HTMLDivElement) {
hyperCheckoutNode.style.color = "#333333";
}
if (hyperCheckoutFooterNode instanceof HTMLDivElement) {
hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5";
}
}
window.addEventListener("resize", function (event) {
var currentHeight = window.innerHeight;
var currentWidth = window.innerWidth;
// @ts-ignore
if (currentWidth <= 1400 && window.state.prevWidth > 1400) {
hide("#hyper-checkout-cart");
// @ts-ignore
if (window.state.currentScreen === "payment_link") {
show("#hyper-footer");
}
try {
if (hyperCheckoutNode instanceof HTMLDivElement) {
hyperCheckoutNode.style.color = contrastBWColor;
}
if (hyperCheckoutFooterNode instanceof HTMLDivElement) {
hyperCheckoutFooterNode.style.backgroundColor = lighterColor;
}
} catch (error) {
console.error("Failed to fetch primary-color, using default", error);
}
// @ts-ignore
} else if (currentWidth > 1400 && window.state.prevWidth <= 1400) {
// @ts-ignore
if (window.state.currentScreen === "payment_link") {
hide("#hyper-footer");
}
show("#hyper-checkout-cart");
try {
if (hyperCheckoutNode instanceof HTMLDivElement) {
hyperCheckoutNode.style.color = "#333333";
}
if (hyperCheckoutFooterNode instanceof HTMLDivElement) {
hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5";
}
} catch (error) {
console.error("Failed to revert back to default colors", error);
}
}
// @ts-ignore
window.state.prevHeight = currentHeight;
// @ts-ignore
window.state.prevWidth = currentWidth;
// @ts-ignore
window.state.isMobileView = currentWidth <= 1400;
});
}
/**
* Trigger - post mounting SDK
* Use - set relevant classes to elements in the doc for showing SDK
**/
function showSDK() {
show("#hyper-checkout-sdk");
show("#hyper-checkout-details");
show("#submit");
show("#unified-checkout");
hide("#sdk-spinner");
}
/**
* Trigger - post downloading SDK
* Uses
* - Instantiate SDK
* - Create a payment widget
* - Decide whether or not to show SDK (based on status)
**/
function initializeSDK() {
// @ts-ignore
var paymentDetails = window.__PAYMENT_DETAILS;
var client_secret = paymentDetails.client_secret;
var appearance = {
variables: {
colorPrimary: paymentDetails.theme || "rgb(0, 109, 249)",
fontFamily: "Work Sans, sans-serif",
fontSizeBase: "16px",
colorText: "rgb(51, 65, 85)",
colorTextSecondary: "#334155B3",
colorPrimaryText: "rgb(51, 65, 85)",
colorTextPlaceholder: "#33415550",
borderColor: "#33415550",
colorBackground: "rgb(255, 255, 255)",
},
};
// @ts-ignore
hyper = window.Hyper(pub_key, {
isPreloadEnabled: false,
});
widgets = hyper.widgets({
appearance: appearance,
clientSecret: client_secret,
});
var type =
paymentDetails.sdk_layout === "spaced_accordion" ||
paymentDetails.sdk_layout === "accordion"
? "accordion"
: paymentDetails.sdk_layout;
var unifiedCheckoutOptions = {
disableSaveCards: true,
layout: {
type: type, //accordion , tabs, spaced accordion
spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion",
},
branding: "never",
wallets: {
walletReturnUrl: paymentDetails.return_url,
style: {
theme: "dark",
type: "default",
height: 55,
},
},
};
unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions);
mountUnifiedCheckout("#unified-checkout");
showSDK();
}
/**
* Use - mount payment widget on the passed element
* @param {String} id
**/
function mountUnifiedCheckout(id) {
if (unifiedCheckout !== null) {
unifiedCheckout.mount(id);
}
}
/**
* Trigger - on clicking submit button
* Uses
* - Trigger /payment/confirm through SDK
* - Toggle UI loaders appropriately
* - Handle errors and redirect to status page
* @param {Event} e
*/
function handleSubmit(e) {
// @ts-ignore
var paymentDetails = window.__PAYMENT_DETAILS;
// Update button loader
hide("#submit-button-text");
show("#submit-spinner");
var submitButtonNode = document.getElementById("submit");
if (submitButtonNode instanceof HTMLButtonElement) {
submitButtonNode.disabled = true;
submitButtonNode.classList.add("disabled");
}
hyper
.confirmPayment({
widgets: widgets,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: paymentDetails.return_url,
},
})
.then(function (result) {
var error = result.error;
if (error) {
if (error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occurred.");
}
} else {
redirectToStatus();
}
})
.catch(function (error) {
console.error("Error confirming payment_intent", error);
})
.finally(() => {
hide("#submit-spinner");
show("#submit-button-text");
if (submitButtonNode instanceof HTMLButtonElement) {
submitButtonNode.disabled = false;
submitButtonNode.classList.remove("disabled");
}
});
}
function show(id) {
removeClass(id, "hidden");
}
function hide(id) {
addClass(id, "hidden");
}
function showMessage(msg) {
show("#payment-message");
addText("#payment-message", msg);
}
/**
* Use - redirect to /payment_link/status
*/
function redirectToStatus() {
var arr = window.location.pathname.split("/");
arr.splice(0, 2);
arr.unshift("status");
arr.unshift("payment_link");
window.location.href = window.location.origin + "/" + arr.join("/");
}
function addText(id, msg) {
var element = document.querySelector(id);
element.innerText = msg;
}
function addClass(id, className) {
var element = document.querySelector(id);
element.classList.add(className);
}
function removeClass(id, className) {
var element = document.querySelector(id);
element.classList.remove(className);
}
/**
* Use - format date in "hh:mm AM/PM timezone MM DD, YYYY"
* @param {Date} date
**/
function formatDate(date) {
var months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
var hours = date.getHours();
var minutes = date.getMinutes();
minutes = minutes < 10 ? "0" + minutes : minutes;
var suffix = hours > 11 ? "PM" : "AM";
hours = hours % 12;
hours = hours ? hours : 12;
var day = date.getDate();
var month = months[date.getMonth()];
var year = date.getUTCFullYear();
// @ts-ignore
var locale = navigator.language || navigator.userLanguage;
var timezoneShorthand = date
.toLocaleDateString(locale, {
day: "2-digit",
timeZoneName: "long",
})
.substring(4)
.split(" ")
.reduce(function (tz, c) {
return tz + c.charAt(0).toUpperCase();
}, "");
var formatted =
hours +
":" +
minutes +
" " +
suffix +
" " +
timezoneShorthand +
" " +
month +
" " +
day +
", " +
year;
return formatted;
}
/**
* Trigger - on boot
* Uses
* - Render payment related details (header bit)
* - Amount
* - Merchant's name
* - Expiry
* @param {PaymentDetails} paymentDetails
**/
function renderPaymentDetails(paymentDetails) {
// Create price node
var priceNode = document.createElement("div");
priceNode.className = "hyper-checkout-payment-price";
priceNode.innerText = paymentDetails.currency + " " + paymentDetails.amount;
// Create merchant name's node
var merchantNameNode = document.createElement("div");
merchantNameNode.className = "hyper-checkout-payment-merchant-name";
merchantNameNode.innerText = "Requested by " + paymentDetails.merchant_name;
// Create payment ID node
var paymentIdNode = document.createElement("div");
paymentIdNode.className = "hyper-checkout-payment-ref";
paymentIdNode.innerText = "Ref Id: " + paymentDetails.payment_id;
// Create merchant logo's node
var merchantLogoNode = document.createElement("img");
merchantLogoNode.src = paymentDetails.merchant_logo;
// Create expiry node
var paymentExpiryNode = document.createElement("div");
paymentExpiryNode.className = "hyper-checkout-payment-footer-expiry";
var expiryDate = new Date(paymentDetails.session_expiry);
var formattedDate = formatDate(expiryDate);
paymentExpiryNode.innerText = "Link expires on: " + formattedDate;
// Append information to DOM
var paymentContextNode = document.getElementById(
"hyper-checkout-payment-context"
);
if (paymentContextNode instanceof HTMLDivElement) {
paymentContextNode.prepend(priceNode);
}
var paymentMerchantDetails = document.getElementById(
"hyper-checkout-payment-merchant-details"
);
if (paymentMerchantDetails instanceof HTMLDivElement) {
paymentMerchantDetails.append(merchantNameNode);
paymentMerchantDetails.append(paymentIdNode);
}
var merchantImageNode = document.getElementById(
"hyper-checkout-merchant-image"
);
if (merchantImageNode instanceof HTMLDivElement) {
merchantImageNode.prepend(merchantLogoNode);
}
var footerNode = document.getElementById("hyper-checkout-payment-footer");
if (footerNode instanceof HTMLDivElement) {
footerNode.append(paymentExpiryNode);
}
}
/**
* Trigger - on boot
* Uses
* - Render cart wrapper and items
* - Attaches an onclick event for toggling expand on the items list
* @param {PaymentDetails} paymentDetails
**/
function renderCart(paymentDetails) {
var orderDetails = paymentDetails.order_details;
// Cart items
if (Array.isArray(orderDetails) && orderDetails.length > 0) {
var cartNode = document.getElementById("hyper-checkout-cart");
var cartItemsNode = document.getElementById("hyper-checkout-cart-items");
var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE =
paymentDetails.max_items_visible_after_collapse;
orderDetails.map(function (item, index) {
if (index >= MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) {
return;
}
renderCartItem(
item,
paymentDetails,
index !== 0 && index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE,
cartItemsNode
);
});
// Expand / collapse button
var totalItems = orderDetails.length;
if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) {
var expandButtonNode = document.createElement("div");
expandButtonNode.className = "hyper-checkout-cart-button";
expandButtonNode.onclick = () => {
handleCartView(paymentDetails);
};
var buttonImageNode = document.createElement("svg");
buttonImageNode.id = "hyper-checkout-cart-button-arrow";
var arrowDownImage = document.getElementById("arrow-down");
if (arrowDownImage instanceof Object) {
buttonImageNode.innerHTML = arrowDownImage.innerHTML;
}
var buttonTextNode = document.createElement("span");
buttonTextNode.id = "hyper-checkout-cart-button-text";
var hiddenItemsCount =
orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE;
buttonTextNode.innerText = "Show More (" + hiddenItemsCount + ")";
expandButtonNode.append(buttonTextNode, buttonImageNode);
if (cartNode instanceof HTMLDivElement) {
cartNode.insertBefore(expandButtonNode, cartNode.lastElementChild);
}
}
} else {
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");
}
}
}
/**
* Trigger - on cart render
* Uses
* - Renders a single cart item which includes
* - Product image
* - Product name
* - Quantity
* - Single item amount
* @param {OrderDetailsWithAmount} item
* @param {PaymentDetails} paymentDetails
* @param {boolean} shouldAddDividerNode
* @param {HTMLDivElement} cartItemsNode
**/
function renderCartItem(
item,
paymentDetails,
shouldAddDividerNode,
cartItemsNode
) {
// Wrappers
var itemWrapperNode = document.createElement("div");
itemWrapperNode.className = "hyper-checkout-cart-item";
var nameAndQuantityWrapperNode = document.createElement("div");
nameAndQuantityWrapperNode.className = "hyper-checkout-cart-product-details";
// Image
var productImageNode = document.createElement("img");
productImageNode.className = "hyper-checkout-cart-product-image";
productImageNode.src = item.product_img_link;
// Product title
var productNameNode = document.createElement("div");
productNameNode.className = "hyper-checkout-card-item-name";
productNameNode.innerText = item.product_name;
// Product quantity
var quantityNode = document.createElement("div");
quantityNode.className = "hyper-checkout-card-item-quantity";
quantityNode.innerText = "Qty: " + item.quantity;
// Product price
var priceNode = document.createElement("div");
priceNode.className = "hyper-checkout-card-item-price";
priceNode.innerText = paymentDetails.currency + " " + item.amount;
// Append items
nameAndQuantityWrapperNode.append(productNameNode, quantityNode);
itemWrapperNode.append(
productImageNode,
nameAndQuantityWrapperNode,
priceNode
);
if (shouldAddDividerNode) {
var dividerNode = document.createElement("div");
dividerNode.className = "hyper-checkout-cart-item-divider";
cartItemsNode.append(dividerNode);
}
cartItemsNode.append(itemWrapperNode);
}
/**
* Trigger - on toggling expansion of cart list
* Uses
* - Render or delete items based on current state of the rendered cart list
* @param {PaymentDetails} paymentDetails
**/
function handleCartView(paymentDetails) {
var orderDetails = paymentDetails.order_details;
var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE =
paymentDetails.max_items_visible_after_collapse;
var itemsHTMLCollection = document.getElementsByClassName(
"hyper-checkout-cart-item"
);
var dividerHTMLCollection = document.getElementsByClassName(
"hyper-checkout-cart-item-divider"
);
var cartItems = [].slice.call(itemsHTMLCollection);
var dividerItems = [].slice.call(dividerHTMLCollection);
var isHidden = cartItems.length < orderDetails.length;
var cartItemsNode = document.getElementById("hyper-checkout-cart-items");
var cartButtonTextNode = document.getElementById(
"hyper-checkout-cart-button-text"
);
var cartButtonImageNode = document.getElementById(
"hyper-checkout-cart-button-arrow"
);
if (isHidden) {
if (Array.isArray(orderDetails)) {
orderDetails.map(function (item, index) {
if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) {
return;
}
renderCartItem(
item,
paymentDetails,
index >= MAX_ITEMS_VISIBLE_AFTER_COLLAPSE,
cartItemsNode
);
});
}
if (cartItemsNode instanceof HTMLDivElement) {
cartItemsNode.style.maxHeight = cartItemsNode.scrollHeight + "px";
cartItemsNode.style.height = cartItemsNode.scrollHeight + "px";
}
if (cartButtonTextNode instanceof HTMLButtonElement) {
cartButtonTextNode.innerText = "Show Less";
}
var arrowUpImage = document.getElementById("arrow-up");
if (
cartButtonImageNode instanceof Object &&
arrowUpImage instanceof Object
) {
cartButtonImageNode.innerHTML = arrowUpImage.innerHTML;
}
} else {
if (cartItemsNode instanceof HTMLDivElement) {
cartItemsNode.style.maxHeight = "300px";
cartItemsNode.style.height = "290px";
cartItemsNode.scrollTo({ top: 0, behavior: "smooth" });
setTimeout(function () {
cartItems.map(function (item, index) {
if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) {
return;
}
if (cartItemsNode instanceof HTMLDivElement) {
cartItemsNode.removeChild(item);
}
});
dividerItems.map(function (item, index) {
if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE - 1) {
return;
}
if (cartItemsNode instanceof HTMLDivElement) {
cartItemsNode.removeChild(item);
}
});
}, 300);
}
setTimeout(function () {
var hiddenItemsCount =
orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE;
if (cartButtonTextNode instanceof HTMLButtonElement) {
cartButtonTextNode.innerText = "Show More (" + hiddenItemsCount + ")";
}
var arrowDownImage = document.getElementById("arrow-down");
if (
cartButtonImageNode instanceof Object &&
arrowDownImage instanceof Object
) {
cartButtonImageNode.innerHTML = arrowDownImage.innerHTML;
}
}, 250);
}
}
/**
* Use - hide cart when in mobile view
**/
function hideCartInMobileView() {
window.history.back();
var cartNode = document.getElementById("hyper-checkout-cart");
if (cartNode instanceof HTMLDivElement) {
cartNode.style.animation = "slide-to-right 0.3s linear";
cartNode.style.right = "-582px";
}
setTimeout(function () {
hide("#hyper-checkout-cart");
}, 300);
}
/**
* Use - show cart when in mobile view
**/
function viewCartInMobileView() {
window.history.pushState("view-cart", "");
var cartNode = document.getElementById("hyper-checkout-cart");
if (cartNode instanceof HTMLDivElement) {
cartNode.style.animation = "slide-from-right 0.3s linear";
cartNode.style.right = "0px";
}
show("#hyper-checkout-cart");
}
/**
* Trigger - on boot
* Uses
* - Render SDK header node
* - merchant's name
* - currency + amount
* @param {PaymentDetails} paymentDetails
**/
function renderSDKHeader(paymentDetails) {
// SDK headers' items
var sdkHeaderItemNode = document.createElement("div");
sdkHeaderItemNode.className = "hyper-checkout-sdk-items";
var sdkHeaderMerchantNameNode = document.createElement("div");
sdkHeaderMerchantNameNode.className = "hyper-checkout-sdk-header-brand-name";
sdkHeaderMerchantNameNode.innerText = paymentDetails.merchant_name;
var sdkHeaderAmountNode = document.createElement("div");
sdkHeaderAmountNode.className = "hyper-checkout-sdk-header-amount";
sdkHeaderAmountNode.innerText =
paymentDetails.currency + " " + paymentDetails.amount;
sdkHeaderItemNode.append(sdkHeaderMerchantNameNode);
sdkHeaderItemNode.append(sdkHeaderAmountNode);
// Append to SDK header's node
var sdkHeaderNode = document.getElementById("hyper-checkout-sdk-header");
if (sdkHeaderNode instanceof HTMLDivElement) {
// sdkHeaderNode.append(sdkHeaderLogoNode);
sdkHeaderNode.append(sdkHeaderItemNode);
}
}

View File

@ -0,0 +1,161 @@
{{ 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;
flex-flow: column;
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;
}
#hyper-checkout-status-redirect-message {
margin-top: 20px;
font-family: "Montserrat";
font-size: 13px;
}
.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;
}
}

View File

@ -0,0 +1,26 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Payment Status</title>
<style>
{{rendered_css}}
</style>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800"
/>
<script>
{{ rendered_js }}
</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 id="hyper-checkout-status-redirect-message"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,379 @@
// @ts-check
/**
* UTIL FUNCTIONS
*/
/**
* Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js
*/
function padz(str, len) {
if (len === void 0) {
len = 2;
}
return (new Array(len).join("0") + str).slice(-len);
}
function hexToRgbArray(hex) {
if (hex.slice(0, 1) === "#") hex = hex.slice(1);
var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i;
if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: "' + hex + '"');
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
return [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
];
}
function toRgbArray(c) {
if (!c) throw new Error("Invalid color value");
if (Array.isArray(c)) return c;
return typeof c === "string" ? hexToRgbArray(c) : [c.r, c.g, c.b];
}
function getLuminance(c) {
var i, x;
var a = [];
for (i = 0; i < c.length; i++) {
x = c[i] / 255;
a[i] = x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
}
function invertToBW(color, bw, asArr) {
var DEFAULT_BW = {
black: "#090302",
white: "#FFFFFC",
threshold: Math.sqrt(1.05 * 0.05) - 0.05,
};
var options = bw === true ? DEFAULT_BW : Object.assign({}, DEFAULT_BW, bw);
return getLuminance(color) > options.threshold
? asArr
? hexToRgbArray(options.black)
: options.black
: asArr
? hexToRgbArray(options.white)
: options.white;
}
function invert(color, bw) {
if (bw === void 0) {
bw = false;
}
color = toRgbArray(color);
if (bw) return invertToBW(color, bw);
return (
"#" +
color
.map(function (c) {
return padz((255 - c).toString(16));
})
.join("")
);
}
/**
* UTIL FUNCTIONS END HERE
*/
// @ts-ignore
{{ payment_details_js_script }}
// @ts-ignore
window.state = {
prevHeight: window.innerHeight,
prevWidth: window.innerWidth,
isMobileView: window.innerWidth <= 1400,
};
/**
* Trigger - init function invoked once the script tag is loaded
* Use
* - Update document's title
* - Update document's icon
* - Render and populate document with payment details and cart
* - Initialize event listeners for updating UI on screen size changes
* - Initialize SDK
**/
function boot() {
// @ts-ignore
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);
}
// Render status details
renderStatusDetails(paymentDetails);
// Add event listeners
initializeEventListeners(paymentDetails);
}
/**
* Trigger - on boot
* Uses
* - Render status details
* - Header - (amount, merchant name, merchant logo)
* - Body - status with image
* - Footer - payment details (id | error code and msg, if any)
* @param {PaymentDetails} paymentDetails
**/
function renderStatusDetails(paymentDetails) {
var status = paymentDetails.status;
var statusDetails = {
imageSource: "",
message: "",
status: status,
amountText: "",
items: [],
};
// Payment details
var paymentId = createItem("Ref Id", paymentDetails.payment_id);
// @ts-ignore
statusDetails.items.push(paymentId);
// Status specific information
switch (status) {
case "expired":
statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png";
statusDetails.status = "Payment Link Expired";
statusDetails.message =
"Sorry, this payment link has expired. Please use below reference for further investigation.";
break;
case "succeeded":
statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png";
statusDetails.message = "We have successfully received your payment";
statusDetails.status = "Paid successfully";
statusDetails.amountText = new Date(
paymentDetails.created
).toTimeString();
break;
case "processing":
statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/pending.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://live.hyperswitch.io/payment-link-assets/failed.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://live.hyperswitch.io/payment-link-assets/failed.png";
statusDetails.status = "Payment Cancelled";
break;
case "requires_merchant_action":
statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/pending.png";
statusDetails.status = "Payment under review";
break;
case "requires_capture":
statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png";
statusDetails.message = "We have successfully received your payment";
statusDetails.status = "Payment Success";
break;
case "partially_captured":
statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png";
statusDetails.message = "Partial payment was captured.";
statusDetails.status = "Payment Success";
break;
default:
statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.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);
}
break;
}
// Form header items
var amountNode = document.createElement("div");
amountNode.className = "hyper-checkout-status-amount";
amountNode.innerText = paymentDetails.currency + " " + paymentDetails.amount;
var merchantLogoNode = document.createElement("img");
merchantLogoNode.className = "hyper-checkout-status-merchant-logo";
// @ts-ignore
merchantLogoNode.src = window.__PAYMENT_DETAILS.merchant_logo;
merchantLogoNode.alt = "";
// Form content items
var statusImageNode = document.createElement("img");
statusImageNode.className = "hyper-checkout-status-image";
statusImageNode.src = statusDetails.imageSource;
var statusTextNode = document.createElement("div");
statusTextNode.className = "hyper-checkout-status-text";
statusTextNode.innerText = statusDetails.status;
var statusMessageNode = document.createElement("div");
statusMessageNode.className = "hyper-checkout-status-message";
statusMessageNode.innerText = statusDetails.message;
var statusDetailsNode = document.createElement("div");
statusDetailsNode.className = "hyper-checkout-status-details";
// Append items
if (statusDetailsNode instanceof HTMLDivElement) {
statusDetails.items.map(function (item) {
statusDetailsNode.append(item);
});
}
var statusHeaderNode = document.getElementById(
"hyper-checkout-status-header"
);
if (statusHeaderNode instanceof HTMLDivElement) {
statusHeaderNode.append(amountNode, merchantLogoNode);
}
var statusContentNode = document.getElementById(
"hyper-checkout-status-content"
);
if (statusContentNode instanceof HTMLDivElement) {
statusContentNode.append(statusImageNode, statusTextNode);
if (statusMessageNode instanceof HTMLDivElement) {
statusContentNode.append(statusMessageNode);
}
statusContentNode.append(statusDetailsNode);
}
if (paymentDetails.redirect === true) {
// Form redirect text
var statusRedirectTextNode = document.getElementById(
"hyper-checkout-status-redirect-message"
);
if (
statusRedirectTextNode instanceof HTMLDivElement &&
typeof paymentDetails.return_url === "string"
) {
var timeout = 5,
j = 0;
for (var i = 0; i <= timeout; i++) {
setTimeout(function () {
var secondsLeft = timeout - j++;
var innerText =
secondsLeft === 0
? "Redirecting ..."
: "Redirecting in " + secondsLeft + " seconds ...";
// @ts-ignore
statusRedirectTextNode.innerText = innerText;
if (secondsLeft === 0) {
// Form query params
var queryParams = {
payment_id: paymentDetails.payment_id,
status: paymentDetails.status,
};
var url = new URL(paymentDetails.return_url);
var params = new URLSearchParams(url.search);
// Attach query params to return_url
for (var key in queryParams) {
if (queryParams.hasOwnProperty(key)) {
params.set(key, queryParams[key]);
}
}
url.search = params.toString();
setTimeout(function () {
// Finally redirect
window.location.href = url.toString();
}, 1000);
}
}, i * 1000);
}
}
}
}
/**
* Use - create an item which is a key-value pair of some information related to a payment
* @param {String} heading
* @param {String} value
**/
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.className = "hyper-checkout-item-value";
valueNode.innerText = value;
itemNode.append(headerNode);
itemNode.append(valueNode);
return itemNode;
}
/**
* Use - add event listeners for changing UI on screen resize
* @param {PaymentDetails} paymentDetails
*/
function initializeEventListeners(paymentDetails) {
var primaryColor = paymentDetails.theme;
var contrastBWColor = invert(primaryColor, true);
var statusRedirectTextNode = document.getElementById(
"hyper-checkout-status-redirect-message"
);
if (window.innerWidth <= 1400) {
if (statusRedirectTextNode instanceof HTMLDivElement) {
statusRedirectTextNode.style.color = "#333333";
}
} else if (window.innerWidth > 1400) {
if (statusRedirectTextNode instanceof HTMLDivElement) {
statusRedirectTextNode.style.color = contrastBWColor;
}
}
window.addEventListener("resize", function (event) {
var currentHeight = window.innerHeight;
var currentWidth = window.innerWidth;
// @ts-ignore
if (currentWidth <= 1400 && window.state.prevWidth > 1400) {
try {
if (statusRedirectTextNode instanceof HTMLDivElement) {
statusRedirectTextNode.style.color = "#333333";
}
} catch (error) {
console.error("Failed to fetch primary-color, using default", error);
}
// @ts-ignore
} else if (currentWidth > 1400 && window.state.prevWidth <= 1400) {
try {
if (statusRedirectTextNode instanceof HTMLDivElement) {
statusRedirectTextNode.style.color = contrastBWColor;
}
} catch (error) {
console.error("Failed to revert back to default colors", error);
}
}
// @ts-ignore
window.state.prevHeight = currentHeight;
// @ts-ignore
window.state.prevWidth = currentWidth;
// @ts-ignore
window.state.isMobileView = currentWidth <= 1400;
});
}

View File

@ -1,355 +0,0 @@
<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>

View File

@ -892,6 +892,10 @@ impl PaymentLink {
web::resource("{merchant_id}/{payment_id}")
.route(web::get().to(initiate_payment_link)),
)
.service(
web::resource("status/{merchant_id}/{payment_id}")
.route(web::get().to(payment_link_status)),
)
}
}

View File

@ -147,9 +147,10 @@ impl From<Flow> for ApiIdentifier {
| Flow::BusinessProfileDelete
| Flow::BusinessProfileList => Self::Business,
Flow::PaymentLinkRetrieve | Flow::PaymentLinkInitiate | Flow::PaymentLinkList => {
Self::PaymentLink
}
Flow::PaymentLinkRetrieve
| Flow::PaymentLinkInitiate
| Flow::PaymentLinkList
| Flow::PaymentLinkStatus => Self::PaymentLink,
Flow::Verification => Self::Verification,

View File

@ -123,3 +123,33 @@ pub async fn payments_link_list(
)
.await
}
pub async fn payment_link_status(
state: web::Data<AppState>,
req: actix_web::HttpRequest,
path: web::Path<(String, String)>,
) -> impl Responder {
let flow = Flow::PaymentLinkStatus;
let (merchant_id, payment_id) = path.into_inner();
let payload = api_models::payments::PaymentLinkInitiateRequest {
payment_id,
merchant_id: merchant_id.clone(),
};
Box::pin(api::server_wrap(
flow,
state,
&req,
payload.clone(),
|state, auth, _| {
get_payment_link_status(
state,
auth.merchant_account,
payload.merchant_id.clone(),
payload.payment_id.clone(),
)
},
&crate::services::authentication::MerchantIdAuth(merchant_id),
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -1749,19 +1749,50 @@ pub fn build_redirection_form(
pub fn build_payment_link_html(
payment_link_data: PaymentLinkFormData,
) -> CustomResult<String, errors::ApiErrorResponse> {
let html_template = include_str!("../core/payment_link/payment_link.html").to_string();
let mut tera = Tera::default();
// Add modification to css template with dynamic data
let css_template =
include_str!("../core/payment_link/payment_link_initiate/payment_link.css").to_string();
let _ = tera.add_raw_template("payment_link_css", &css_template);
let mut context = Context::new();
context.insert("css_color_scheme", &payment_link_data.css_script);
let rendered_css = match tera.render("payment_link_css", &context) {
Ok(rendered_css) => rendered_css,
Err(tera_error) => {
crate::logger::warn!("{tera_error}");
Err(errors::ApiErrorResponse::InternalServerError)?
}
};
// Add modification to js template with dynamic data
let js_template =
include_str!("../core/payment_link/payment_link_initiate/payment_link.js").to_string();
let _ = tera.add_raw_template("payment_link_js", &js_template);
context.insert("payment_details_js_script", &payment_link_data.js_script);
let rendered_js = match tera.render("payment_link_js", &context) {
Ok(rendered_js) => rendered_js,
Err(tera_error) => {
crate::logger::warn!("{tera_error}");
Err(errors::ApiErrorResponse::InternalServerError)?
}
};
// Modify Html template with rendered js and rendered css files
let html_template =
include_str!("../core/payment_link/payment_link_initiate/payment_link.html").to_string();
let _ = tera.add_raw_template("payment_link", &html_template);
let mut context = Context::new();
context.insert(
"hyperloader_sdk_link",
&get_hyper_loader_sdk(&payment_link_data.sdk_url),
);
context.insert("css_color_scheme", &payment_link_data.css_script);
context.insert("payment_details_js_script", &payment_link_data.js_script);
context.insert("rendered_css", &rendered_css);
context.insert("rendered_js", &rendered_js);
match tera.render("payment_link", &context) {
Ok(rendered_html) => Ok(rendered_html),
@ -1779,14 +1810,46 @@ fn get_hyper_loader_sdk(sdk_url: &str) -> String {
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);
// Add modification to css template with dynamic data
let css_template =
include_str!("../core/payment_link/payment_link_status/status.css").to_string();
let _ = tera.add_raw_template("payment_link_css", &css_template);
let mut context = Context::new();
context.insert("css_color_scheme", &payment_link_data.css_script);
let rendered_css = match tera.render("payment_link_css", &context) {
Ok(rendered_css) => rendered_css,
Err(tera_error) => {
crate::logger::warn!("{tera_error}");
Err(errors::ApiErrorResponse::InternalServerError)?
}
};
// Add modification to js template with dynamic data
let js_template =
include_str!("../core/payment_link/payment_link_status/status.js").to_string();
let _ = tera.add_raw_template("payment_link_js", &js_template);
context.insert("payment_details_js_script", &payment_link_data.js_script);
let rendered_js = match tera.render("payment_link_js", &context) {
Ok(rendered_js) => rendered_js,
Err(tera_error) => {
crate::logger::warn!("{tera_error}");
Err(errors::ApiErrorResponse::InternalServerError)?
}
};
// Modify Html template with rendered js and rendered css files
let html_template =
include_str!("../core/payment_link/payment_link_status/status.html").to_string();
let _ = tera.add_raw_template("payment_link_status", &html_template);
context.insert("rendered_css", &rendered_css);
context.insert("rendered_js", &rendered_js);
match tera.render("payment_link_status", &context) {
Ok(rendered_html) => Ok(rendered_html),
Err(tera_error) => {