From 3bf66a32c14d56d37b65a30576e7c743aa3c1208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djk=C3=A1=C5=A5o?= Date: Mon, 17 Jun 2024 22:18:13 +0200 Subject: [PATCH] Bridge init --- sdk/Cargo.toml | 20 +++- sdk/rust-toolchain.toml | 5 + sdk/src/bridge/event.rs | 113 ++++++++++++++++++++++ sdk/src/bridge/mod.rs | 207 ++++++++++++++++++++++++++++++++++++++++ sdk/src/lib.rs | 2 + sdk/src/locales.rs | 6 ++ 6 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 sdk/rust-toolchain.toml create mode 100644 sdk/src/bridge/event.rs create mode 100644 sdk/src/bridge/mod.rs diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 2ff6b7b..e7e615d 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -14,6 +14,7 @@ license = "MIT OR Apache-2.0" [dependencies] anyhow = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } serde_json = { workspace = true } envy = { workspace = true } dotenvy = { workspace = true } @@ -44,8 +45,24 @@ tracing-subscriber = { workspace = true, optional = true } ## Needed for webhooks +## Needed for bridge +wasm-bindgen = { workspace = true, optional = true } +serde-wasm-bindgen = { version = "0.6.5", optional = true } +bus = { version = "2.4.1", optional = true } + +[dependencies.web-sys] +workspace = true +features = [ + "Window", + "Document", + "Url", + "UrlSearchParams", + "EventListener", + "EventTarget", + "console", +] + [features] -# default = [] default = ["middleware", "redis_apl", "webhook_utils", "tracing"] middleware = [ "dep:axum", @@ -57,3 +74,4 @@ middleware = [ redis_apl = ["dep:redis"] webhook_utils = ["dep:http"] tracing = ["dep:tracing", "dep:tracing-subscriber"] +bridge = ["dep:wasm-bindgen", "dep:bus", "dep:serde-wasm-bindgen"] diff --git a/sdk/rust-toolchain.toml b/sdk/rust-toolchain.toml new file mode 100644 index 0000000..7ae25e7 --- /dev/null +++ b/sdk/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# channel = "nightly-2024-04-25" +## Toggle to this one for sdk releases +channel = "stable" +targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] diff --git a/sdk/src/bridge/event.rs b/sdk/src/bridge/event.rs new file mode 100644 index 0000000..a600843 --- /dev/null +++ b/sdk/src/bridge/event.rs @@ -0,0 +1,113 @@ +use crate::locales::LocaleCode; + +use super::ThemeType; +use bus::{Bus, BusReader}; +use serde::{Deserialize, Serialize}; +use strum_macros::EnumIter; + +pub struct EventChannels { + pub handshake: Bus, + pub response: Bus, + pub redirect: Bus, + pub theme: Bus, + pub locale_changed: Bus, + pub token_refreshed: Bus, +} + +impl EventChannels { + pub fn new() -> Self { + Self { + handshake: Bus::new(10), + response: Bus::new(10), + redirect: Bus::new(10), + theme: Bus::new(10), + locale_changed: Bus::new(10), + token_refreshed: Bus::new(10), + } + } + + pub fn subscribe_handshake(&mut self) -> BusReader { + self.handshake.add_rx() + } + + pub fn subscribe_response(&mut self) -> BusReader { + self.response.add_rx() + } + + pub fn subscribe_redirect(&mut self) -> BusReader { + self.redirect.add_rx() + } + + pub fn subscribe_theme(&mut self) -> BusReader { + self.theme.add_rx() + } + + pub fn subscribe_locale_changed(&mut self) -> BusReader { + self.locale_changed.add_rx() + } + + pub fn subscribe_token_refreshed(&mut self) -> BusReader { + self.token_refreshed.add_rx() + } +} + +#[derive(EnumIter, Debug)] +pub enum EventType { + Handshake, + Response, + Redirect, + Theme, + LocaleChanged, + TokenRefreshed, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(tag = "type", content = "payload")] +#[serde(rename_all = "camelCase")] +pub enum Event { + Handshake(PayloadHanshake), + Response(PayloadResponse), + Redirect(PayloadRedirect), + Theme(PayloadTheme), + LocaleChanged(PayloadLocaleChanged), + TokenRefreshed(PayloadTokenRefreshed), +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayloadHanshake { + pub token: String, + pub version: String, + pub saleor_version: Option, + pub dashboard_version: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayloadResponse { + pub action_id: String, + pub ok: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayloadRedirect { + pub path: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayloadTheme { + pub theme: ThemeType, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayloadLocaleChanged { + pub locale: LocaleCode, +} +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PayloadTokenRefreshed { + pub token: String, +} diff --git a/sdk/src/bridge/mod.rs b/sdk/src/bridge/mod.rs new file mode 100644 index 0000000..2dbdf3d --- /dev/null +++ b/sdk/src/bridge/mod.rs @@ -0,0 +1,207 @@ +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, + 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, +} + +#[derive(Default, Debug)] +pub struct AppBridgeState { + pub token: Option, + 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, + pub dashboard_version: Option, + pub user: Option, + pub app_permissions: Option>, +} + +impl AppBridgeState { + pub fn from_window() -> Result { + 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, + 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) -> Result { + 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 = serde_wasm_bindgen::from_value(e); + web_sys::console::log_1(&format!("{:?}", &event_data).into()); + debug!("{:?}", &event_data); + }) as Box); + 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 { + 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, +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 4bea2cc..9684ebd 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -1,4 +1,6 @@ pub mod apl; +#[cfg(feature = "bridge")] +pub mod bridge; pub mod config; pub mod headers; pub mod locales; diff --git a/sdk/src/locales.rs b/sdk/src/locales.rs index 2674fd2..d125e7c 100644 --- a/sdk/src/locales.rs +++ b/sdk/src/locales.rs @@ -46,3 +46,9 @@ pub enum LocaleCode { ZhHans, ZhHant, } + +impl Default for LocaleCode { + fn default() -> Self { + Self::En + } +}