Bridge init

This commit is contained in:
Djkáťo 2024-06-17 22:18:13 +02:00
parent 305a58e8b5
commit 3bf66a32c1
6 changed files with 352 additions and 1 deletions

View file

@ -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"]

5
sdk/rust-toolchain.toml Normal file
View file

@ -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"]

113
sdk/src/bridge/event.rs Normal file
View file

@ -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<PayloadHanshake>,
pub response: Bus<PayloadResponse>,
pub redirect: Bus<PayloadRedirect>,
pub theme: Bus<PayloadTheme>,
pub locale_changed: Bus<PayloadLocaleChanged>,
pub token_refreshed: Bus<PayloadTokenRefreshed>,
}
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<PayloadHanshake> {
self.handshake.add_rx()
}
pub fn subscribe_response(&mut self) -> BusReader<PayloadResponse> {
self.response.add_rx()
}
pub fn subscribe_redirect(&mut self) -> BusReader<PayloadRedirect> {
self.redirect.add_rx()
}
pub fn subscribe_theme(&mut self) -> BusReader<PayloadTheme> {
self.theme.add_rx()
}
pub fn subscribe_locale_changed(&mut self) -> BusReader<PayloadLocaleChanged> {
self.locale_changed.add_rx()
}
pub fn subscribe_token_refreshed(&mut self) -> BusReader<PayloadTokenRefreshed> {
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<String>,
pub dashboard_version: Option<String>,
}
#[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,
}

207
sdk/src/bridge/mod.rs Normal file
View file

@ -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<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,
}

View file

@ -1,4 +1,6 @@
pub mod apl;
#[cfg(feature = "bridge")]
pub mod bridge;
pub mod config;
pub mod headers;
pub mod locales;

View file

@ -46,3 +46,9 @@ pub enum LocaleCode {
ZhHans,
ZhHant,
}
impl Default for LocaleCode {
fn default() -> Self {
Self::En
}
}