mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
677 lines
27 KiB
Rust
677 lines
27 KiB
Rust
mod client;
|
|
pub(crate) mod request;
|
|
|
|
use std::{
|
|
collections::HashMap,
|
|
fmt::Debug,
|
|
future::Future,
|
|
str,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use actix_web::{body, http::header, HttpRequest, HttpResponse, Responder};
|
|
use common_utils::errors::ReportSwitchExt;
|
|
use error_stack::{report, IntoReport, Report, ResultExt};
|
|
use masking::ExposeOptionInterface;
|
|
use router_env::{instrument, tracing, Tag};
|
|
use serde::Serialize;
|
|
|
|
use self::request::{ContentType, HeaderExt, RequestBuilderExt};
|
|
pub use self::request::{Method, Request, RequestBuilder};
|
|
use crate::{
|
|
configs::settings::Connectors,
|
|
consts,
|
|
core::{
|
|
errors::{self, CustomResult, RouterResult},
|
|
payments,
|
|
},
|
|
db::StorageInterface,
|
|
logger,
|
|
routes::{app::AppStateInfo, AppState},
|
|
services::authentication as auth,
|
|
types::{self, api, storage, ErrorResponse},
|
|
};
|
|
|
|
pub type BoxedConnectorIntegration<'a, T, Req, Resp> =
|
|
Box<&'a (dyn ConnectorIntegration<T, Req, Resp> + Send + Sync)>;
|
|
|
|
pub trait ConnectorIntegrationAny<T, Req, Resp>: Send + Sync + 'static {
|
|
fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>;
|
|
}
|
|
|
|
impl<S, T, Req, Resp> ConnectorIntegrationAny<T, Req, Resp> for S
|
|
where
|
|
S: ConnectorIntegration<T, Req, Resp> + Send + Sync,
|
|
{
|
|
fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> {
|
|
Box::new(self)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Resp> + Sync {
|
|
fn get_headers(
|
|
&self,
|
|
_req: &types::RouterData<T, Req, Resp>,
|
|
_connectors: &Connectors,
|
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
fn get_content_type(&self) -> &'static str {
|
|
mime::APPLICATION_JSON.essence_str()
|
|
}
|
|
|
|
/// primarily used when creating signature based on request method of payment flow
|
|
fn get_http_method(&self) -> Method {
|
|
Method::Post
|
|
}
|
|
|
|
fn get_url(
|
|
&self,
|
|
_req: &types::RouterData<T, Req, Resp>,
|
|
_connectors: &Connectors,
|
|
) -> CustomResult<String, errors::ConnectorError> {
|
|
Ok(String::new())
|
|
}
|
|
|
|
fn get_request_body(
|
|
&self,
|
|
_req: &types::RouterData<T, Req, Resp>,
|
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
|
Ok(None)
|
|
}
|
|
|
|
/// This module can be called before executing a payment flow where a pre-task is needed
|
|
/// Eg: Some connectors requires one-time session token before making a payment, we can add the session token creation logic in this block
|
|
async fn execute_pretasks(
|
|
&self,
|
|
_router_data: &mut types::RouterData<T, Req, Resp>,
|
|
_app_state: &AppState,
|
|
) -> CustomResult<(), errors::ConnectorError> {
|
|
Ok(())
|
|
}
|
|
|
|
/// This module can be called after executing a payment flow where a post-task needed
|
|
/// Eg: Some connectors require payment sync to happen immediately after the authorize call to complete the transaction, we can add that logic in this block
|
|
async fn execute_posttasks(
|
|
&self,
|
|
_router_data: &mut types::RouterData<T, Req, Resp>,
|
|
_app_state: &AppState,
|
|
) -> CustomResult<(), errors::ConnectorError> {
|
|
Ok(())
|
|
}
|
|
|
|
fn build_request(
|
|
&self,
|
|
_req: &types::RouterData<T, Req, Resp>,
|
|
_connectors: &Connectors,
|
|
) -> CustomResult<Option<Request>, errors::ConnectorError> {
|
|
Ok(None)
|
|
}
|
|
|
|
fn handle_response(
|
|
&self,
|
|
data: &types::RouterData<T, Req, Resp>,
|
|
_res: types::Response,
|
|
) -> CustomResult<types::RouterData<T, Req, Resp>, errors::ConnectorError>
|
|
where
|
|
T: Clone,
|
|
Req: Clone,
|
|
Resp: Clone,
|
|
{
|
|
Ok(data.clone())
|
|
}
|
|
|
|
fn get_error_response(
|
|
&self,
|
|
_res: types::Response,
|
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
|
Ok(ErrorResponse::get_not_implemented())
|
|
}
|
|
|
|
fn get_certificate(
|
|
&self,
|
|
_req: &types::RouterData<T, Req, Resp>,
|
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
|
Ok(None)
|
|
}
|
|
|
|
fn get_certificate_key(
|
|
&self,
|
|
_req: &types::RouterData<T, Req, Resp>,
|
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub async fn execute_connector_processing_step<
|
|
'b,
|
|
'a,
|
|
T: 'static,
|
|
Req: Debug + Clone + 'static,
|
|
Resp: Debug + Clone + 'static,
|
|
>(
|
|
state: &'b AppState,
|
|
connector_integration: BoxedConnectorIntegration<'a, T, Req, Resp>,
|
|
req: &'b types::RouterData<T, Req, Resp>,
|
|
call_connector_action: payments::CallConnectorAction,
|
|
) -> CustomResult<types::RouterData<T, Req, Resp>, errors::ConnectorError>
|
|
where
|
|
T: Clone + Debug,
|
|
// BoxedConnectorIntegration<T, Req, Resp>: 'b,
|
|
{
|
|
// If needed add an error stack as follows
|
|
// connector_integration.build_request(req).attach_printable("Failed to build request");
|
|
let mut router_data = req.clone();
|
|
match call_connector_action {
|
|
payments::CallConnectorAction::HandleResponse(res) => {
|
|
let response = types::Response {
|
|
response: res.into(),
|
|
status_code: 200,
|
|
};
|
|
|
|
connector_integration.handle_response(req, response)
|
|
}
|
|
payments::CallConnectorAction::Avoid => Ok(router_data),
|
|
payments::CallConnectorAction::StatusUpdate(status) => {
|
|
router_data.status = status;
|
|
Ok(router_data)
|
|
}
|
|
payments::CallConnectorAction::Trigger => {
|
|
match connector_integration.build_request(req, &state.conf.connectors)? {
|
|
Some(request) => {
|
|
logger::debug!(connector_request=?request);
|
|
let response = call_connector_api(state, request).await;
|
|
logger::debug!(connector_response=?response);
|
|
match response {
|
|
Ok(body) => {
|
|
let response = match body {
|
|
Ok(body) => connector_integration.handle_response(req, body)?,
|
|
Err(body) => {
|
|
let error = connector_integration.get_error_response(body)?;
|
|
router_data.response = Err(error);
|
|
|
|
router_data
|
|
}
|
|
};
|
|
Ok(response)
|
|
}
|
|
Err(error) => Err(error
|
|
.change_context(errors::ConnectorError::ProcessingStepFailed(None))),
|
|
}
|
|
}
|
|
None => Ok(router_data),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub async fn call_connector_api(
|
|
state: &AppState,
|
|
request: Request,
|
|
) -> CustomResult<Result<types::Response, types::Response>, errors::ApiClientError> {
|
|
let current_time = Instant::now();
|
|
|
|
let response = send_request(state, request).await;
|
|
|
|
let elapsed_time = current_time.elapsed();
|
|
logger::info!(request_time=?elapsed_time);
|
|
|
|
handle_response(response).await
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
async fn send_request(
|
|
state: &AppState,
|
|
request: Request,
|
|
) -> CustomResult<reqwest::Response, errors::ApiClientError> {
|
|
logger::debug!(method=?request.method, headers=?request.headers, payload=?request.payload, ?request);
|
|
let url = &request.url;
|
|
let should_bypass_proxy = client::proxy_bypass_urls(&state.conf.locker).contains(url);
|
|
let client = client::create_client(
|
|
&state.conf.proxy,
|
|
should_bypass_proxy,
|
|
request.certificate,
|
|
request.certificate_key,
|
|
)?;
|
|
let headers = request.headers.construct_header_map()?;
|
|
match request.method {
|
|
Method::Get => client.get(url),
|
|
Method::Post => {
|
|
let client = client.post(url);
|
|
match request.content_type {
|
|
Some(ContentType::Json) => client.json(&request.payload),
|
|
|
|
// Currently this is not used remove this if not required
|
|
// If using this then handle the serde_part
|
|
Some(ContentType::FormUrlEncoded) => {
|
|
let url_encoded_payload = serde_urlencoded::to_string(&request.payload)
|
|
.into_report()
|
|
.change_context(errors::ApiClientError::UrlEncodingFailed)
|
|
.attach_printable_lazy(|| {
|
|
format!(
|
|
"Unable to do url encoding on request: {:?}",
|
|
&request.payload
|
|
)
|
|
})?;
|
|
|
|
logger::debug!(?url_encoded_payload);
|
|
client.body(url_encoded_payload)
|
|
}
|
|
// If payload needs processing the body cannot have default
|
|
None => client.body(request.payload.expose_option().unwrap_or_default()),
|
|
}
|
|
}
|
|
|
|
Method::Put => {
|
|
client
|
|
.put(url)
|
|
.body(request.payload.expose_option().unwrap_or_default()) // If payload needs processing the body cannot have default
|
|
}
|
|
Method::Delete => client.delete(url),
|
|
}
|
|
.add_headers(headers)
|
|
.timeout(Duration::from_secs(crate::consts::REQUEST_TIME_OUT))
|
|
.send()
|
|
.await
|
|
.map_err(|error| match error {
|
|
error if error.is_timeout() => errors::ApiClientError::RequestTimeoutReceived,
|
|
_ => errors::ApiClientError::RequestNotSent(error.to_string()),
|
|
})
|
|
.into_report()
|
|
.attach_printable("Unable to send request to connector")
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
async fn handle_response(
|
|
response: CustomResult<reqwest::Response, errors::ApiClientError>,
|
|
) -> CustomResult<Result<types::Response, types::Response>, errors::ApiClientError> {
|
|
response
|
|
.map(|response| async {
|
|
logger::info!(?response);
|
|
let status_code = response.status().as_u16();
|
|
match status_code {
|
|
200..=202 | 302 => {
|
|
logger::debug!(response=?response);
|
|
// If needed add log line
|
|
// logger:: error!( error_parsing_response=?err);
|
|
let response = response
|
|
.bytes()
|
|
.await
|
|
.into_report()
|
|
.change_context(errors::ApiClientError::ResponseDecodingFailed)
|
|
.attach_printable("Error while waiting for response")?;
|
|
Ok(Ok(types::Response {
|
|
response,
|
|
status_code,
|
|
}))
|
|
}
|
|
|
|
status_code @ 500..=599 => {
|
|
let bytes = response.bytes().await.map_err(|error| {
|
|
report!(error)
|
|
.change_context(errors::ApiClientError::ResponseDecodingFailed)
|
|
.attach_printable("Client error response received")
|
|
})?;
|
|
// let error = match status_code {
|
|
// 500 => errors::ApiClientError::InternalServerErrorReceived,
|
|
// 502 => errors::ApiClientError::BadGatewayReceived,
|
|
// 503 => errors::ApiClientError::ServiceUnavailableReceived,
|
|
// 504 => errors::ApiClientError::GatewayTimeoutReceived,
|
|
// _ => errors::ApiClientError::UnexpectedServerResponse,
|
|
// };
|
|
Ok(Err(types::Response {
|
|
response: bytes,
|
|
status_code,
|
|
}))
|
|
}
|
|
|
|
status_code @ 400..=499 => {
|
|
let bytes = response.bytes().await.map_err(|error| {
|
|
report!(error)
|
|
.change_context(errors::ApiClientError::ResponseDecodingFailed)
|
|
.attach_printable("Client error response received")
|
|
})?;
|
|
/* let error = match status_code {
|
|
400 => errors::ApiClientError::BadRequestReceived(bytes),
|
|
401 => errors::ApiClientError::UnauthorizedReceived(bytes),
|
|
403 => errors::ApiClientError::ForbiddenReceived,
|
|
404 => errors::ApiClientError::NotFoundReceived(bytes),
|
|
405 => errors::ApiClientError::MethodNotAllowedReceived,
|
|
408 => errors::ApiClientError::RequestTimeoutReceived,
|
|
422 => errors::ApiClientError::UnprocessableEntityReceived(bytes),
|
|
429 => errors::ApiClientError::TooManyRequestsReceived,
|
|
_ => errors::ApiClientError::UnexpectedServerResponse,
|
|
};
|
|
Err(report!(error).attach_printable("Client error response received"))
|
|
*/
|
|
Ok(Err(types::Response {
|
|
response: bytes,
|
|
status_code,
|
|
}))
|
|
}
|
|
|
|
_ => Err(report!(errors::ApiClientError::UnexpectedServerResponse)
|
|
.attach_printable("Unexpected response from server")),
|
|
}
|
|
})?
|
|
.await
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
pub enum ApplicationResponse<R> {
|
|
Json(R),
|
|
StatusOk,
|
|
TextPlain(String),
|
|
JsonForRedirection(api::RedirectionResponse),
|
|
Form(RedirectForm),
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
pub struct ApplicationRedirectResponse {
|
|
pub url: String,
|
|
}
|
|
|
|
impl From<&storage::PaymentAttempt> for ApplicationRedirectResponse {
|
|
fn from(payment_attempt: &storage::PaymentAttempt) -> Self {
|
|
Self {
|
|
url: format!(
|
|
"/payments/start/{}/{}/{}",
|
|
&payment_attempt.payment_id,
|
|
&payment_attempt.merchant_id,
|
|
&payment_attempt.attempt_id
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct RedirectForm {
|
|
pub endpoint: String,
|
|
pub method: Method,
|
|
pub form_fields: HashMap<String, String>,
|
|
}
|
|
|
|
impl From<(url::Url, Method)> for RedirectForm {
|
|
fn from((mut redirect_url, method): (url::Url, Method)) -> Self {
|
|
let form_fields = std::collections::HashMap::from_iter(
|
|
redirect_url
|
|
.query_pairs()
|
|
.map(|(key, value)| (key.to_string(), value.to_string())),
|
|
);
|
|
|
|
// Do not include query params in the endpoint
|
|
redirect_url.set_query(None);
|
|
|
|
Self {
|
|
endpoint: redirect_url.to_string(),
|
|
method,
|
|
form_fields,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum AuthFlow {
|
|
Client,
|
|
Merchant,
|
|
}
|
|
|
|
#[instrument(skip(request, payload, state, func, api_auth))]
|
|
pub async fn server_wrap_util<'a, 'b, A, U, T, Q, F, Fut, E, OErr>(
|
|
state: &'b A,
|
|
request: &'a HttpRequest,
|
|
payload: T,
|
|
func: F,
|
|
api_auth: &dyn auth::AuthenticateAndFetch<U, A>,
|
|
) -> CustomResult<ApplicationResponse<Q>, OErr>
|
|
where
|
|
F: Fn(&'b A, U, T) -> Fut,
|
|
Fut: Future<Output = CustomResult<ApplicationResponse<Q>, E>>,
|
|
Q: Serialize + Debug + 'a,
|
|
T: Debug,
|
|
A: AppStateInfo,
|
|
CustomResult<ApplicationResponse<Q>, E>: ReportSwitchExt<ApplicationResponse<Q>, OErr>,
|
|
CustomResult<U, errors::ApiErrorResponse>: ReportSwitchExt<U, OErr>,
|
|
{
|
|
let auth_out = api_auth
|
|
.authenticate_and_fetch(request.headers(), state)
|
|
.await
|
|
.switch()?;
|
|
func(state, auth_out, payload).await.switch()
|
|
}
|
|
|
|
#[instrument(
|
|
skip(request, payload, state, func, api_auth),
|
|
fields(request_method, request_url_path)
|
|
)]
|
|
pub async fn server_wrap<'a, 'b, A, T, U, Q, F, Fut, E>(
|
|
state: &'b A,
|
|
request: &'a HttpRequest,
|
|
payload: T,
|
|
func: F,
|
|
api_auth: &dyn auth::AuthenticateAndFetch<U, A>,
|
|
) -> HttpResponse
|
|
where
|
|
F: Fn(&'b A, U, T) -> Fut,
|
|
Fut: Future<Output = CustomResult<ApplicationResponse<Q>, E>>,
|
|
Q: Serialize + Debug + 'a,
|
|
T: Debug,
|
|
A: AppStateInfo,
|
|
CustomResult<ApplicationResponse<Q>, E>:
|
|
ReportSwitchExt<ApplicationResponse<Q>, api_models::errors::types::ApiErrorResponse>,
|
|
{
|
|
let request_method = request.method().as_str();
|
|
let url_path = request.path();
|
|
tracing::Span::current().record("request_method", request_method);
|
|
tracing::Span::current().record("request_url_path", url_path);
|
|
|
|
let start_instant = Instant::now();
|
|
logger::info!(tag = ?Tag::BeginRequest);
|
|
|
|
let res = match server_wrap_util(state, request, payload, func, api_auth).await {
|
|
Ok(ApplicationResponse::Json(response)) => match serde_json::to_string(&response) {
|
|
Ok(res) => http_response_json(res),
|
|
Err(_) => http_response_err(
|
|
r#"{
|
|
"error": {
|
|
"message": "Error serializing response from connector"
|
|
}
|
|
}"#,
|
|
),
|
|
},
|
|
Ok(ApplicationResponse::StatusOk) => http_response_ok(),
|
|
Ok(ApplicationResponse::TextPlain(text)) => http_response_plaintext(text),
|
|
Ok(ApplicationResponse::JsonForRedirection(response)) => {
|
|
match serde_json::to_string(&response) {
|
|
Ok(res) => http_redirect_response(res, response),
|
|
Err(_) => http_response_err(
|
|
r#"{
|
|
"error": {
|
|
"message": "Error serializing response from connector"
|
|
}
|
|
}"#,
|
|
),
|
|
}
|
|
}
|
|
Ok(ApplicationResponse::Form(response)) => build_redirection_form(&response)
|
|
.respond_to(request)
|
|
.map_into_boxed_body(),
|
|
|
|
Err(error) => log_and_return_error_response(error),
|
|
};
|
|
|
|
let response_code = res.status().as_u16();
|
|
let end_instant = Instant::now();
|
|
let request_duration = end_instant.saturating_duration_since(start_instant);
|
|
logger::info!(
|
|
tag = ?Tag::EndRequest,
|
|
status_code = response_code,
|
|
time_taken_ms = request_duration.as_millis(),
|
|
);
|
|
|
|
res
|
|
}
|
|
|
|
pub fn log_and_return_error_response<T>(error: Report<T>) -> HttpResponse
|
|
where
|
|
T: actix_web::ResponseError + error_stack::Context + Clone,
|
|
{
|
|
logger::error!(?error);
|
|
HttpResponse::from_error(error.current_context().clone())
|
|
}
|
|
|
|
pub async fn authenticate_by_api_key(
|
|
store: &dyn StorageInterface,
|
|
api_key: &str,
|
|
) -> RouterResult<storage::MerchantAccount> {
|
|
store
|
|
.find_merchant_account_by_api_key(api_key)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::Unauthorized)
|
|
.attach_printable("Merchant not authenticated")
|
|
}
|
|
|
|
pub fn http_response_json<T: body::MessageBody + 'static>(response: T) -> HttpResponse {
|
|
HttpResponse::Ok()
|
|
.content_type("application/json")
|
|
.append_header((header::VIA, "Juspay_router"))
|
|
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
|
|
.body(response)
|
|
}
|
|
|
|
pub fn http_response_plaintext<T: body::MessageBody + 'static>(res: T) -> HttpResponse {
|
|
HttpResponse::Ok()
|
|
.content_type("text/plain")
|
|
.append_header((header::VIA, "Juspay_router"))
|
|
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
|
|
.body(res)
|
|
}
|
|
|
|
pub fn http_response_ok() -> HttpResponse {
|
|
HttpResponse::Ok()
|
|
.append_header((header::VIA, "Juspay_router"))
|
|
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
|
|
.finish()
|
|
}
|
|
|
|
pub fn http_redirect_response<T: body::MessageBody + 'static>(
|
|
response: T,
|
|
redirection_response: api::RedirectionResponse,
|
|
) -> HttpResponse {
|
|
HttpResponse::Ok()
|
|
.content_type("application/json")
|
|
.append_header((header::VIA, "Juspay_router"))
|
|
.append_header((
|
|
"Location",
|
|
redirection_response.return_url_with_query_params,
|
|
))
|
|
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
|
|
.status(http::StatusCode::FOUND)
|
|
.body(response)
|
|
}
|
|
|
|
pub fn http_response_err<T: body::MessageBody + 'static>(response: T) -> HttpResponse {
|
|
HttpResponse::BadRequest()
|
|
.content_type("application/json")
|
|
.append_header((header::VIA, "Juspay_router"))
|
|
.append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE))
|
|
.body(response)
|
|
}
|
|
|
|
pub trait ConnectorRedirectResponse {
|
|
fn get_flow_type(
|
|
&self,
|
|
_query_params: &str,
|
|
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
|
|
Ok(payments::CallConnectorAction::Avoid)
|
|
}
|
|
}
|
|
|
|
pub trait Authenticate {
|
|
fn get_client_secret(&self) -> Option<&String>;
|
|
}
|
|
|
|
impl Authenticate for api_models::payments::PaymentsRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl Authenticate for api_models::payment_methods::PaymentMethodListRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup {
|
|
use maud::PreEscaped;
|
|
|
|
maud::html! {
|
|
(maud::DOCTYPE)
|
|
html {
|
|
meta name="viewport" content="width=device-width, initial-scale=1";
|
|
head {
|
|
style {
|
|
r##"
|
|
|
|
"##
|
|
}
|
|
(PreEscaped(r##"
|
|
<style>
|
|
#loader1 {
|
|
width: 500px,
|
|
}
|
|
@media max-width: 600px {
|
|
#loader1 {
|
|
width: 200px
|
|
}
|
|
}
|
|
</style>
|
|
"##))
|
|
}
|
|
|
|
body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" {
|
|
|
|
div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-left: auto; margin-right: auto;" { "" }
|
|
|
|
(PreEscaped(r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.7.4/lottie.min.js"></script>"#))
|
|
|
|
(PreEscaped(r#"
|
|
<script>
|
|
var anime = bodymovin.loadAnimation({
|
|
container: document.getElementById('loader1'),
|
|
renderer: 'svg',
|
|
loop: true,
|
|
autoplay: true,
|
|
name: 'hyperswitch loader',
|
|
animationData: {"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":31.0000012626559,"w":400,"h":250,"nm":"loader_shape","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[278.25,202.671,0],"ix":2},"a":{"a":0,"k":[23.72,23.72,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[12.935,0],[0,-12.936],[-12.935,0],[0,12.935]],"o":[[-12.952,0],[0,12.935],[12.935,0],[0,-12.936]],"v":[[0,-23.471],[-23.47,0.001],[0,23.471],[23.47,0.001]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19.99,"s":[100]},{"t":29.9800012211104,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[23.72,23.721],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"square 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[196.25,201.271,0],"ix":2},"a":{"a":0,"k":[22.028,22.03,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.914,0],[0,0],[0,-1.914],[0,0],[-1.914,0],[0,0],[0,1.914],[0,0]],"o":[[0,0],[-1.914,0],[0,0],[0,1.914],[0,0],[1.914,0],[0,0],[0,-1.914]],"v":[[18.313,-21.779],[-18.312,-21.779],[-21.779,-18.313],[-21.779,18.314],[-18.312,21.779],[18.313,21.779],[21.779,18.314],[21.779,-18.313]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14.99,"s":[100]},{"t":24.9800010174563,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22.028,22.029],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":47.0000019143492,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Triangle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[116.25,200.703,0],"ix":2},"a":{"a":0,"k":[27.11,21.243,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.558,-0.879],[0,0],[-1.133,0],[0,0],[0.609,0.947],[0,0]],"o":[[-0.558,-0.879],[0,0],[-0.609,0.947],[0,0],[1.133,0],[0,0],[0,0]],"v":[[1.209,-20.114],[-1.192,-20.114],[-26.251,18.795],[-25.051,20.993],[25.051,20.993],[26.251,18.795],[1.192,-20.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.427451010311,0.976470648074,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9.99,"s":[100]},{"t":19.9800008138021,"s":[10]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.11,21.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48.0000019550801,"st":0,"bm":0}],"markers":[]}
|
|
})
|
|
</script>
|
|
"#))
|
|
|
|
|
|
h3 style="text-align: center;" { "Please wait while we process your payment..." }
|
|
form action=(PreEscaped(&form.endpoint)) method=(form.method.to_string()) #payment_form {
|
|
@for (field, value) in &form.form_fields {
|
|
input type="hidden" name=(field) value=(value);
|
|
}
|
|
}
|
|
|
|
(PreEscaped(r#"<script type="text/javascript"> var frm = document.getElementById("payment_form"); window.setTimeout(function () { frm.submit(); }, 300); </script>"#))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn test_mime_essence() {
|
|
assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json");
|
|
}
|
|
}
|