saleor-apps-rs/sdk/src/bridge/mod.rs

208 lines
6.7 KiB
Rust
Raw Normal View History

2024-06-17 20:18:13 +00:00
pub mod event;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use strum_macros::{EnumString, IntoStaticStr};
use tracing::{debug, error, warn};
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use crate::{locales::LocaleCode, manifest::AppPermission};
use self::event::{Event, EventChannels};
use web_sys::console;
pub struct AppBridge {
pub state: AppBridgeState,
pub referer_origin: Option<String>,
pub event_channels: EventChannels,
/**
* Should automatically emit Actions.NotifyReady.
* If app loading time is longer, this can be disabled and sent manually.
*/
auto_notify_ready: Option<bool>,
}
#[derive(Default, Debug)]
pub struct AppBridgeState {
pub token: Option<String>,
pub id: String,
pub ready: bool,
pub domain: String,
pub path: String,
pub theme: ThemeType,
pub locale: LocaleCode,
pub saleor_api_url: String,
/**pub
* Versions of Saleor that app is mounted. Passed from the Dashboard.pub
* Works form Saleor 3.15pub
*/
pub saleor_version: Option<String>,
pub dashboard_version: Option<String>,
pub user: Option<AppBridgeUser>,
pub app_permissions: Option<Vec<AppPermission>>,
}
impl AppBridgeState {
pub fn from_window() -> Result<Self, JsValue> {
let mut state = AppBridgeState::default();
let window = web_sys::window().ok_or(JsValue::from_str("Missing window"))?;
let href = window.location().href()?;
let url = web_sys::Url::new(&href)?;
let saleor_api_url = url
.search_params()
.get(AppIframeParams::SaleorApiUrl.into());
let id = url.search_params().get(AppIframeParams::Id.into());
let theme = url.search_params().get(AppIframeParams::Theme.into());
let domain = url.search_params().get(AppIframeParams::Domain.into());
let locale = url.search_params().get(AppIframeParams::Locale.into());
if let Some(id) = id {
state.id = id
}
if let Some(saleor_api_url) = saleor_api_url {
state.saleor_api_url = saleor_api_url
}
if let Some(theme) = theme {
if let Ok(theme_type) = ThemeType::from_str(&theme) {
state.theme = theme_type
}
}
if let Some(domain) = domain {
state.domain = domain
}
if let Some(locale) = locale {
if let Ok(loc) = LocaleCode::from_str(&locale) {
state.locale = loc
}
}
debug!("state from window: {:?}", &state);
console::log_1(&format!("state from window: {:?}", &state).into());
Ok(state)
}
}
#[derive(Default, Debug)]
pub struct AppBridgeUser {
/**
* Original permissions of the user that is using the app.
* *Not* the same permissions as the app itself.
*
* Can be used by app to check if user is authorized to perform
* domain specific actions
*/
pub permissions: Vec<AppPermission>,
pub email: String,
}
#[derive(Debug, Serialize, Deserialize, EnumString, IntoStaticStr)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum AppIframeParams {
Id,
Theme,
Domain,
SaleorApiUrl,
Locale,
}
#[derive(Debug, Serialize, Deserialize, EnumString)]
#[serde(rename_all = "lowercase")]
pub enum ThemeType {
Light,
Dark,
}
impl Default for ThemeType {
fn default() -> Self {
ThemeType::Light
}
}
impl AppBridge {
pub fn new(auto_notify_ready: Option<bool>) -> Result<Self, AppBridgeError> {
debug!("creating app bridge");
console::log_1(&"creating app bridge".into());
if web_sys::Window::is_type_of(&JsValue::from_str("undefined")) {
error!("Window is undefined");
console::log_1(&"Window is undefined".into());
return Err(AppBridgeError::WindowIsUndefined);
}
let referrer = web_sys::window().and_then(|w| {
w.document().and_then(|d| {
web_sys::Url::new(&d.referrer())
.ok()
.and_then(|u| Some(u.origin()))
})
});
if referrer.is_none() {
warn!("Referrer origin is none");
console::log_1(&"Referrer origin is none".into());
}
let mut bridge = Self {
auto_notify_ready,
state: match AppBridgeState::from_window() {
Ok(s) => s,
Err(e) => return Err(AppBridgeError::JsValue(e)),
},
referer_origin: referrer,
event_channels: EventChannels::new(),
};
if bridge.auto_notify_ready.unwrap_or(false) {
bridge.notify_ready()?;
}
Ok(bridge)
}
pub fn listen_to_events(&mut self) -> Result<&mut Self, AppBridgeError> {
let window = web_sys::window().ok_or(AppBridgeError::WindowIsUndefined)?;
let cb = Closure::wrap(Box::new(|e: JsValue| {
let event_data: Result<SaleorIframeEvent, _> = serde_wasm_bindgen::from_value(e);
web_sys::console::log_1(&format!("{:?}", &event_data).into());
debug!("{:?}", &event_data);
}) as Box<dyn FnMut(_)>);
window
.add_event_listener_with_callback("message", &cb.as_ref().unchecked_ref())
.map_err(|e| AppBridgeError::JsValue(e))?;
Ok(self)
}
pub fn dispatch_event(&mut self, event: Event) -> Result<Event, AppBridgeError> {
let window = web_sys::window().ok_or(AppBridgeError::WindowIsUndefined)?;
let parent = match window.parent() {
Ok(p) => p.ok_or(AppBridgeError::WindowParentIsUndefined)?,
Err(e) => return Err(AppBridgeError::JsValue(e)),
};
let message = JsValue::from_str(&serde_json::to_string(&event)?);
parent
.post_message(&message, "*")
.map_err(|e| AppBridgeError::JsValue(e))?;
todo!()
}
pub fn notify_ready(&mut self) -> Result<&mut Self, AppBridgeError> {
todo!()
}
}
#[derive(thiserror::Error, Debug)]
pub enum AppBridgeError {
#[error("failed serializing event from window")]
SerdeError(#[from] serde_wasm_bindgen::Error),
#[error("Something went wrong with serde_json::to_string(&event)")]
FailedPayloadToJsonStringification(#[from] serde_json::Error),
#[error("Windows parent is missing, meaning the app is probably not embedded in Iframe")]
WindowParentIsUndefined,
#[error("Window is typeof undefined. Probably means AppBridge::new() is being called outside of a browser")]
WindowIsUndefined,
#[error("JS error")]
JsValue(JsValue),
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SaleorIframeEvent {
origin: String,
data: Event,
}