mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +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 | apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm | ||||||
|  |  | ||||||
| [payment_link] | [payment_link] | ||||||
| sdk_url = "http://localhost:9090/dist/HyperLoader.js" | sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" | ||||||
|  |  | ||||||
| [payment_method_auth] | [payment_method_auth] | ||||||
| redis_expiry = 900 | 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" | apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" | ||||||
|  |  | ||||||
| [payment_link] | [payment_link] | ||||||
| sdk_url = "http://localhost:9090/dist/HyperLoader.js" | sdk_url = "http://localhost:9050/HyperLoader.js" | ||||||
|  |  | ||||||
| [payment_method_auth] | [payment_method_auth] | ||||||
| redis_expiry = 900 | redis_expiry = 900 | ||||||
|  | |||||||
| @ -3358,6 +3358,13 @@ pub struct PaymentLinkInitiateRequest { | |||||||
|     pub payment_id: String, |     pub payment_id: String, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, serde::Serialize)] | ||||||
|  | #[serde(untagged)] | ||||||
|  | pub enum PaymentLinkData { | ||||||
|  |     PaymentLinkDetails(PaymentLinkDetails), | ||||||
|  |     PaymentLinkStatusDetails(PaymentLinkStatusDetails), | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Debug, serde::Serialize)] | #[derive(Debug, serde::Serialize)] | ||||||
| pub struct PaymentLinkDetails { | pub struct PaymentLinkDetails { | ||||||
|     pub amount: String, |     pub amount: String, | ||||||
| @ -3376,6 +3383,21 @@ pub struct PaymentLinkDetails { | |||||||
|     pub merchant_description: Option<String>, |     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)] | #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] | ||||||
| #[serde(deny_unknown_fields)] | #[serde(deny_unknown_fields)] | ||||||
|  |  | ||||||
| @ -3451,7 +3473,8 @@ pub struct OrderDetailsWithStringAmount { | |||||||
|     pub product_img_link: Option<String>, |     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 { | pub enum PaymentLinkStatus { | ||||||
|     Active, |     Active, | ||||||
|     Expired, |     Expired, | ||||||
|  | |||||||
| @ -133,8 +133,10 @@ where | |||||||
|             .map_into_boxed_body() |             .map_into_boxed_body() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Ok(api::ApplicationResponse::PaymenkLinkForm(payment_link_data)) => { |         Ok(api::ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { | ||||||
|             match api::build_payment_link_html(*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), |                         Ok(rendered_html) => api::http_response_html_data(rendered_html), | ||||||
|                         Err(_) => api::http_response_err( |                         Err(_) => api::http_response_err( | ||||||
|                             r#"{ |                             r#"{ | ||||||
| @ -145,7 +147,20 @@ where | |||||||
|                         ), |                         ), | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |                 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), |         Err(error) => api::log_and_return_error_response(error), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ use time::PrimitiveDateTime; | |||||||
|  |  | ||||||
| use super::errors::{self, RouterResult, StorageErrorExt}; | use super::errors::{self, RouterResult, StorageErrorExt}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     core::payments::helpers, |  | ||||||
|     errors::RouterResponse, |     errors::RouterResponse, | ||||||
|     routes::AppState, |     routes::AppState, | ||||||
|     services, |     services, | ||||||
| @ -68,18 +67,6 @@ pub async fn intiate_payment_link_flow( | |||||||
|         .get_required_value("payment_link_id") |         .get_required_value("payment_link_id") | ||||||
|         .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?; |         .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 |     let merchant_name_from_merchant_account = merchant_account | ||||||
|         .merchant_name |         .merchant_name | ||||||
|         .clone() |         .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 |         payment_create_return_url | ||||||
|     } else { |     } else { | ||||||
|         merchant_account |         merchant_account | ||||||
| @ -114,23 +101,73 @@ pub async fn intiate_payment_link_flow( | |||||||
|     let (pub_key, currency, client_secret) = validate_sdk_requirements( |     let (pub_key, currency, client_secret) = validate_sdk_requirements( | ||||||
|         merchant_account.publishable_key, |         merchant_account.publishable_key, | ||||||
|         payment_intent.currency, |         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(|| { |     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)) |             .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     // converting first letter of merchant name to upperCase |     // converting first letter of merchant name to upperCase | ||||||
|     let merchant_name = capitalize_first_char(&payment_link_config.seller_name); |     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 { |     let payment_details = api_models::payments::PaymentLinkDetails { | ||||||
|         amount: currency |         amount, | ||||||
|             .to_currency_base_unit(payment_intent.amount) |  | ||||||
|             .into_report() |  | ||||||
|             .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?, |  | ||||||
|         currency, |         currency, | ||||||
|         payment_id: payment_intent.payment_id, |         payment_id: payment_intent.payment_id, | ||||||
|         merchant_name, |         merchant_name, | ||||||
| @ -145,15 +182,16 @@ pub async fn intiate_payment_link_flow( | |||||||
|         merchant_description: payment_intent.description, |         merchant_description: payment_intent.description, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let js_script = get_js_script(payment_details)?; |     let js_script = get_js_script(api_models::payments::PaymentLinkData::PaymentLinkDetails( | ||||||
|     let css_script = get_color_scheme_css(payment_link_config.clone()); |         payment_details, | ||||||
|  |     ))?; | ||||||
|     let payment_link_data = services::PaymentLinkFormData { |     let payment_link_data = services::PaymentLinkFormData { | ||||||
|         js_script, |         js_script, | ||||||
|         sdk_url: state.conf.payment_link.sdk_url.clone(), |         sdk_url: state.conf.payment_link.sdk_url.clone(), | ||||||
|         css_script, |         css_script, | ||||||
|     }; |     }; | ||||||
|     Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( |     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. | 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( | fn get_js_script(payment_details: api_models::payments::PaymentLinkData) -> RouterResult<String> { | ||||||
|     payment_details: api_models::payments::PaymentLinkDetails, |  | ||||||
| ) -> RouterResult<String> { |  | ||||||
|     let payment_details_str = serde_json::to_string(&payment_details) |     let payment_details_str = serde_json::to_string(&payment_details) | ||||||
|         .into_report() |         .into_report() | ||||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) |         .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};")) |     Ok(format!("window.__PAYMENT_DETAILS = {payment_details_str};")) | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -218,11 +254,11 @@ pub async fn list_payment_link( | |||||||
| } | } | ||||||
|  |  | ||||||
| pub fn check_payment_link_status( | pub fn check_payment_link_status( | ||||||
|     max_age: PrimitiveDateTime, |     payment_link_expiry: PrimitiveDateTime, | ||||||
| ) -> api_models::payments::PaymentLinkStatus { | ) -> api_models::payments::PaymentLinkStatus { | ||||||
|     let curr_time = common_utils::date_time::now(); |     let curr_time = common_utils::date_time::now(); | ||||||
|  |  | ||||||
|     if curr_time > max_age { |     if curr_time > payment_link_expiry { | ||||||
|         api_models::payments::PaymentLinkStatus::Expired |         api_models::payments::PaymentLinkStatus::Expired | ||||||
|     } else { |     } else { | ||||||
|         api_models::payments::PaymentLinkStatus::Active |         api_models::payments::PaymentLinkStatus::Active | ||||||
| @ -369,3 +405,10 @@ fn capitalize_first_char(s: &str) -> String { | |||||||
|         s.to_owned() |         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; |         width: 100vw; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|         background-color: white; |  | ||||||
|         padding: 20px 0; |         padding: 20px 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @ -133,7 +132,6 @@ | |||||||
|       #hyper-checkout-cart-image { |       #hyper-checkout-cart-image { | ||||||
|         height: 64px; |         height: 64px; | ||||||
|         width: 64px; |         width: 64px; | ||||||
|         border: 1px solid #e6e6e6; |  | ||||||
|         border-radius: 4px; |         border-radius: 4px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-self: flex-start; |         align-self: flex-start; | ||||||
| @ -344,10 +342,6 @@ | |||||||
|         font-size: 25px; |         font-size: 25px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .payNow { |  | ||||||
|         margin-top: 10px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .page-spinner { |       .page-spinner { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         width: 100vw; |         width: 100vw; | ||||||
| @ -605,9 +599,38 @@ | |||||||
|         text-align: center; |         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) { |       @media only screen and (max-width: 1400px) { | ||||||
|         body { |         body { | ||||||
|           overflow: scroll; |           overflow-y: scroll; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .hyper-checkout { |         .hyper-checkout { | ||||||
| @ -720,6 +743,7 @@ | |||||||
|           background-color: transparent; |           background-color: transparent; | ||||||
|           width: auto; |           width: auto; | ||||||
|           min-width: 300px; |           min-width: 300px; | ||||||
|  |           box-shadow: none; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         #payment-form-wrap { |         #payment-form-wrap { | ||||||
| @ -748,7 +772,7 @@ | |||||||
|       href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800" |       href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800" | ||||||
|     /> |     /> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body class="hide-scrollbar"> | ||||||
|     <!-- SVG ICONS --> |     <!-- SVG ICONS --> | ||||||
|     <svg xmlns="http://www.w3.org/2000/svg" display="none"> |     <svg xmlns="http://www.w3.org/2000/svg" display="none"> | ||||||
|       <defs> |       <defs> | ||||||
| @ -920,7 +944,7 @@ | |||||||
|         <div></div> |         <div></div> | ||||||
|       </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="main hidden" id="hyper-checkout-status-canvas"> | ||||||
|         <div class="hyper-checkout-status-wrap"> |         <div class="hyper-checkout-status-wrap"> | ||||||
|           <div id="hyper-checkout-status-header"></div> |           <div id="hyper-checkout-status-header"></div> | ||||||
| @ -1035,8 +1059,12 @@ | |||||||
|               <div></div> |               <div></div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|           <form id="payment-form"> |           <form id="payment-form" onclick="handleSubmit(); return false;"> | ||||||
|             <div id="unified-checkout"></div> |             <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> |             <div id="payment-message" class="hidden"></div> | ||||||
|           </form> |           </form> | ||||||
|         </div> |         </div> | ||||||
| @ -1060,7 +1088,7 @@ | |||||||
|       window.state = { |       window.state = { | ||||||
|         prevHeight: window.innerHeight, |         prevHeight: window.innerHeight, | ||||||
|         prevWidth: window.innerWidth, |         prevWidth: window.innerWidth, | ||||||
|         isMobileView: window.innerWidth <= 1200, |         isMobileView: window.innerWidth <= 1400, | ||||||
|         currentScreen: "payment_link", |         currentScreen: "payment_link", | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
| @ -1088,9 +1116,9 @@ | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Render UI |         // Render UI | ||||||
|         renderPaymentDetails(); |         renderPaymentDetails(paymentDetails); | ||||||
|         renderSDKHeader(); |         renderSDKHeader(paymentDetails); | ||||||
|         renderCart(); |         renderCart(paymentDetails); | ||||||
|  |  | ||||||
|         // Deal w loaders |         // Deal w loaders | ||||||
|         show("#sdk-spinner"); |         show("#sdk-spinner"); | ||||||
| @ -1098,7 +1126,7 @@ | |||||||
|         hide("#unified-checkout"); |         hide("#unified-checkout"); | ||||||
|  |  | ||||||
|         // Add event listeners |         // Add event listeners | ||||||
|         initializeEventListeners(); |         initializeEventListeners(paymentDetails); | ||||||
|  |  | ||||||
|         // Initialize SDK |         // Initialize SDK | ||||||
|         if (window.Hyper) { |         if (window.Hyper) { | ||||||
| @ -1115,30 +1143,46 @@ | |||||||
|       } |       } | ||||||
|       boot(); |       boot(); | ||||||
|  |  | ||||||
|       function initializeEventListeners() { |       function initializeEventListeners(paymentDetails) { | ||||||
|         var primaryColor = window |         var primaryColor = paymentDetails.theme; | ||||||
|           .getComputedStyle(document.documentElement) |  | ||||||
|           .getPropertyValue("--primary-color"); |  | ||||||
|         var lighterColor = adjustLightness(primaryColor, 1.4); |         var lighterColor = adjustLightness(primaryColor, 1.4); | ||||||
|         var darkerColor = adjustLightness(primaryColor, 0.8); |         var darkerColor = adjustLightness(primaryColor, 0.8); | ||||||
|         var contrastBWColor = invert(primaryColor, true); |         var contrastBWColor = invert(primaryColor, true); | ||||||
|  |         var contrastingTone = | ||||||
|  |           Array.isArray(a) && a.length > 4 ? darkerColor : lighterColor; | ||||||
|         var hyperCheckoutNode = document.getElementById( |         var hyperCheckoutNode = document.getElementById( | ||||||
|           "hyper-checkout-payment" |           "hyper-checkout-payment" | ||||||
|         ); |         ); | ||||||
|  |         var hyperCheckoutCartImageNode = document.getElementById( | ||||||
|  |           "hyper-checkout-cart-image" | ||||||
|  |         ); | ||||||
|         var hyperCheckoutFooterNode = document.getElementById( |         var hyperCheckoutFooterNode = document.getElementById( | ||||||
|           "hyper-checkout-payment-footer" |           "hyper-checkout-payment-footer" | ||||||
|         ); |         ); | ||||||
|         var statusRedirectTextNode = document.getElementById( |         var statusRedirectTextNode = document.getElementById( | ||||||
|           "hyper-checkout-status-redirect-message" |           "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"; |           statusRedirectTextNode.style.color = "#333333"; | ||||||
|           hyperCheckoutNode.style.color = contrastBWColor; |           hyperCheckoutNode.style.color = contrastBWColor; | ||||||
|           var a = lighterColor.match(/[fF]/gi); |           var a = lighterColor.match(/[fF]/gi); | ||||||
|           hyperCheckoutFooterNode.style.backgroundColor = |           hyperCheckoutFooterNode.style.backgroundColor = contrastingTone; | ||||||
|             Array.isArray(a) && a.length > 4 ? darkerColor : lighterColor; |         } else if (window.innerWidth > 1400) { | ||||||
|         } else if (window.innerWidth > 1200) { |  | ||||||
|           statusRedirectTextNode.style.color = contrastBWColor; |           statusRedirectTextNode.style.color = contrastBWColor; | ||||||
|           hyperCheckoutNode.style.color = "#333333"; |           hyperCheckoutNode.style.color = "#333333"; | ||||||
|           hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5"; |           hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5"; | ||||||
| @ -1147,7 +1191,7 @@ | |||||||
|         window.addEventListener("resize", function (event) { |         window.addEventListener("resize", function (event) { | ||||||
|           var currentHeight = window.innerHeight; |           var currentHeight = window.innerHeight; | ||||||
|           var currentWidth = window.innerWidth; |           var currentWidth = window.innerWidth; | ||||||
|           if (currentWidth <= 1200 && window.state.prevWidth > 1200) { |           if (currentWidth <= 1400 && window.state.prevWidth > 1400) { | ||||||
|             hide("#hyper-checkout-cart"); |             hide("#hyper-checkout-cart"); | ||||||
|             if (window.state.currentScreen === "payment_link") { |             if (window.state.currentScreen === "payment_link") { | ||||||
|               show("#hyper-footer"); |               show("#hyper-footer"); | ||||||
| @ -1162,7 +1206,7 @@ | |||||||
|                 error |                 error | ||||||
|               ); |               ); | ||||||
|             } |             } | ||||||
|           } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { |           } else if (currentWidth > 1400 && window.state.prevWidth <= 1400) { | ||||||
|             if (window.state.currentScreen === "payment_link") { |             if (window.state.currentScreen === "payment_link") { | ||||||
|               hide("#hyper-footer"); |               hide("#hyper-footer"); | ||||||
|             } |             } | ||||||
| @ -1178,16 +1222,17 @@ | |||||||
|  |  | ||||||
|           window.state.prevHeight = currentHeight; |           window.state.prevHeight = currentHeight; | ||||||
|           window.state.prevWidth = currentWidth; |           window.state.prevWidth = currentWidth; | ||||||
|           window.state.isMobileView = currentWidth <= 1200; |           window.state.isMobileView = currentWidth <= 1400; | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       function showSDK() { |       function showSDK(paymentDetails) { | ||||||
|         checkStatus() |         checkStatus(paymentDetails) | ||||||
|           .then(function (res) { |           .then(function (res) { | ||||||
|             if (res.showSdk) { |             if (res.showSdk) { | ||||||
|               show("#hyper-checkout-sdk"); |               show("#hyper-checkout-sdk"); | ||||||
|               show("#hyper-checkout-details"); |               show("#hyper-checkout-details"); | ||||||
|  |               show("#submit"); | ||||||
|             } else { |             } else { | ||||||
|               hide("#hyper-checkout-details"); |               hide("#hyper-checkout-details"); | ||||||
|               hide("#hyper-checkout-sdk"); |               hide("#hyper-checkout-sdk"); | ||||||
| @ -1195,6 +1240,8 @@ | |||||||
|               hide("#hyper-footer"); |               hide("#hyper-footer"); | ||||||
|               window.state.currentScreen = "status"; |               window.state.currentScreen = "status"; | ||||||
|             } |             } | ||||||
|  |             show("#unified-checkout"); | ||||||
|  |             hide("#sdk-spinner"); | ||||||
|           }) |           }) | ||||||
|           .catch(function (err) { |           .catch(function (err) { | ||||||
|             console.error("Failed to check status", err); |             console.error("Failed to check status", err); | ||||||
| @ -1217,14 +1264,13 @@ | |||||||
|             colorBackground: "rgb(255, 255, 255)", |             colorBackground: "rgb(255, 255, 255)", | ||||||
|           }, |           }, | ||||||
|         }; |         }; | ||||||
|         hyper = window.Hyper(pub_key); |         hyper = window.Hyper(pub_key, { isPreloadEnabled: false }); | ||||||
|         widgets = hyper.widgets({ |         widgets = hyper.widgets({ | ||||||
|           appearance: appearance, |           appearance: appearance, | ||||||
|           clientSecret: client_secret, |           clientSecret: client_secret, | ||||||
|         }); |         }); | ||||||
|         var unifiedCheckoutOptions = { |         var unifiedCheckoutOptions = { | ||||||
|           layout: "tabs", |           layout: "tabs", | ||||||
|           sdkHandleConfirmPayment: true, |  | ||||||
|           branding: "never", |           branding: "never", | ||||||
|           wallets: { |           wallets: { | ||||||
|             walletReturnUrl: paymentDetails.return_url, |             walletReturnUrl: paymentDetails.return_url, | ||||||
| @ -1237,35 +1283,7 @@ | |||||||
|         }; |         }; | ||||||
|         unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); |         unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); | ||||||
|         mountUnifiedCheckout("#unified-checkout"); |         mountUnifiedCheckout("#unified-checkout"); | ||||||
|  |         showSDK(paymentDetails); | ||||||
|         // 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(); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Util functions |       // Util functions | ||||||
| @ -1277,6 +1295,14 @@ | |||||||
|  |  | ||||||
|       function handleSubmit(e) { |       function handleSubmit(e) { | ||||||
|         var paymentDetails = window.__PAYMENT_DETAILS; |         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 |         hyper | ||||||
|           .confirmPayment({ |           .confirmPayment({ | ||||||
|             widgets: widgets, |             widgets: widgets, | ||||||
| @ -1293,9 +1319,6 @@ | |||||||
|               } else { |               } else { | ||||||
|                 showMessage("An unexpected error occurred."); |                 showMessage("An unexpected error occurred."); | ||||||
|               } |               } | ||||||
|  |  | ||||||
|               // Re-initialize SDK |  | ||||||
|               mountUnifiedCheckout("#unified-checkout"); |  | ||||||
|             } else { |             } 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'. |               // 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'. |               // 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) { |           .catch(function (error) { | ||||||
|             console.error("Error confirming payment_intent", 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 |       // Fetches the payment status after payment submission | ||||||
|       function checkStatus() { |       function checkStatus(paymentDetails) { | ||||||
|         return new window.Promise(function (resolve, reject) { |         return new window.Promise(function (resolve, reject) { | ||||||
|           var paymentDetails = window.__PAYMENT_DETAILS; |  | ||||||
|           var res = { |           var res = { | ||||||
|             showSdk: true, |             showSdk: true, | ||||||
|           }; |           }; | ||||||
| @ -1669,9 +1697,7 @@ | |||||||
|         return formatted; |         return formatted; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       function renderPaymentDetails() { |       function renderPaymentDetails(paymentDetails) { | ||||||
|         var paymentDetails = window.__PAYMENT_DETAILS; |  | ||||||
|  |  | ||||||
|         // Create price node |         // Create price node | ||||||
|         var priceNode = document.createElement("div"); |         var priceNode = document.createElement("div"); | ||||||
|         priceNode.className = "hyper-checkout-payment-price"; |         priceNode.className = "hyper-checkout-payment-price"; | ||||||
| @ -1720,8 +1746,7 @@ | |||||||
|         footerNode.append(paymentExpiryNode); |         footerNode.append(paymentExpiryNode); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       function renderCart() { |       function renderCart(paymentDetails) { | ||||||
|         var paymentDetails = window.__PAYMENT_DETAILS; |  | ||||||
|         var orderDetails = paymentDetails.order_details; |         var orderDetails = paymentDetails.order_details; | ||||||
|  |  | ||||||
|         // Cart items |         // Cart items | ||||||
| @ -1749,7 +1774,9 @@ | |||||||
|           if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { |           if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { | ||||||
|             var expandButtonNode = document.createElement("div"); |             var expandButtonNode = document.createElement("div"); | ||||||
|             expandButtonNode.className = "hyper-checkout-cart-button"; |             expandButtonNode.className = "hyper-checkout-cart-button"; | ||||||
|             expandButtonNode.onclick = handleCartView; |             expandButtonNode.onclick = () => { | ||||||
|  |               handleCartView(paymentDetails); | ||||||
|  |             }; | ||||||
|             var buttonImageNode = document.createElement("svg"); |             var buttonImageNode = document.createElement("svg"); | ||||||
|             buttonImageNode.id = "hyper-checkout-cart-button-arrow"; |             buttonImageNode.id = "hyper-checkout-cart-button-arrow"; | ||||||
|             buttonImageNode.innerHTML = |             buttonImageNode.innerHTML = | ||||||
| @ -1822,8 +1849,7 @@ | |||||||
|         cartItemsNode.append(itemWrapperNode); |         cartItemsNode.append(itemWrapperNode); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       function handleCartView() { |       function handleCartView(paymentDetails) { | ||||||
|         var paymentDetails = window.__PAYMENT_DETAILS; |  | ||||||
|         var orderDetails = paymentDetails.order_details; |         var orderDetails = paymentDetails.order_details; | ||||||
|         var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = |         var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = | ||||||
|           paymentDetails.max_items_visible_after_collapse; |           paymentDetails.max_items_visible_after_collapse; | ||||||
| @ -1911,9 +1937,7 @@ | |||||||
|         show("#hyper-checkout-cart"); |         show("#hyper-checkout-cart"); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       function renderSDKHeader() { |       function renderSDKHeader(paymentDetails) { | ||||||
|         var paymentDetails = window.__PAYMENT_DETAILS; |  | ||||||
|  |  | ||||||
|         // SDK headers' items |         // SDK headers' items | ||||||
|         var sdkHeaderItemNode = document.createElement("div"); |         var sdkHeaderItemNode = document.createElement("div"); | ||||||
|         sdkHeaderItemNode.className = "hyper-checkout-sdk-items"; |         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), |     TextPlain(String), | ||||||
|     JsonForRedirection(api::RedirectionResponse), |     JsonForRedirection(api::RedirectionResponse), | ||||||
|     Form(Box<RedirectionFormData>), |     Form(Box<RedirectionFormData>), | ||||||
|     PaymenkLinkForm(Box<PaymentLinkFormData>), |     PaymenkLinkForm(Box<PaymentLinkAction>), | ||||||
|     FileData((Vec<u8>, mime::Mime)), |     FileData((Vec<u8>, mime::Mime)), | ||||||
|     JsonWithHeaders((R, Vec<(String, String)>)), |     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)] | #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] | ||||||
| pub struct PaymentLinkFormData { | pub struct PaymentLinkFormData { | ||||||
|     pub js_script: String, |     pub js_script: String, | ||||||
| @ -745,6 +751,12 @@ pub struct PaymentLinkFormData { | |||||||
|     pub sdk_url: String, |     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)] | #[derive(Debug, Eq, PartialEq)] | ||||||
| pub struct RedirectionFormData { | pub struct RedirectionFormData { | ||||||
|     pub redirect_form: RedirectForm, |     pub redirect_form: RedirectForm, | ||||||
| @ -1051,8 +1063,10 @@ where | |||||||
|             .map_into_boxed_body() |             .map_into_boxed_body() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Ok(ApplicationResponse::PaymenkLinkForm(payment_link_data)) => { |         Ok(ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { | ||||||
|             match build_payment_link_html(*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), |                         Ok(rendered_html) => http_response_html_data(rendered_html), | ||||||
|                         Err(_) => http_response_err( |                         Err(_) => http_response_err( | ||||||
|                             r#"{ |                             r#"{ | ||||||
| @ -1063,6 +1077,20 @@ where | |||||||
|                         ), |                         ), | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |                 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" | ||||||
|  |                                 } | ||||||
|  |                             }"#, | ||||||
|  |                         ), | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         Ok(ApplicationResponse::JsonWithHeaders((response, headers))) => { |         Ok(ApplicationResponse::JsonWithHeaders((response, headers))) => { | ||||||
|             let request_elapsed_time = request.headers().get(X_HS_LATENCY).and_then(|value| { |             let request_elapsed_time = request.headers().get(X_HS_LATENCY).and_then(|value| { | ||||||
| @ -1634,6 +1662,26 @@ fn get_hyper_loader_sdk(sdk_url: &str) -> String { | |||||||
|     format!("<script src=\"{sdk_url}\" onload=\"initializeSDK()\"></script>") |     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)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     #[test] |     #[test] | ||||||
|  | |||||||
| @ -15,7 +15,8 @@ pub(crate) trait PaymentLinkResponseExt: Sized { | |||||||
| impl PaymentLinkResponseExt for RetrievePaymentLinkResponse { | impl PaymentLinkResponseExt for RetrievePaymentLinkResponse { | ||||||
|     async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult<Self> { |     async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult<Self> { | ||||||
|         let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| { |         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)) |                 .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) | ||||||
|         }); |         }); | ||||||
|         let status = payment_link::check_payment_link_status(session_expiry); |         let status = payment_link::check_payment_link_status(session_expiry); | ||||||
|  | |||||||
| @ -8760,8 +8760,8 @@ | |||||||
|       "PaymentLinkStatus": { |       "PaymentLinkStatus": { | ||||||
|         "type": "string", |         "type": "string", | ||||||
|         "enum": [ |         "enum": [ | ||||||
|           "Active", |           "active", | ||||||
|           "Expired" |           "expired" | ||||||
|         ] |         ] | ||||||
|       }, |       }, | ||||||
|       "PaymentListConstraints": { |       "PaymentListConstraints": { | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Sahkal Poddar
					Sahkal Poddar