Bridge init
This commit is contained in:
parent
305a58e8b5
commit
3bf66a32c1
6 changed files with 352 additions and 1 deletions
|
@ -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
5
sdk/rust-toolchain.toml
Normal 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
113
sdk/src/bridge/event.rs
Normal 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
207
sdk/src/bridge/mod.rs
Normal 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,
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
pub mod apl;
|
||||
#[cfg(feature = "bridge")]
|
||||
pub mod bridge;
|
||||
pub mod config;
|
||||
pub mod headers;
|
||||
pub mod locales;
|
||||
|
|
|
@ -46,3 +46,9 @@ pub enum LocaleCode {
|
|||
ZhHans,
|
||||
ZhHant,
|
||||
}
|
||||
|
||||
impl Default for LocaleCode {
|
||||
fn default() -> Self {
|
||||
Self::En
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue