use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
pub(crate) const VAPID_PUBLIC_KEY: &str =
"BHtamLu5RHqMWbV3JyyEmQKL-lweTVq3ePiFOHGu_EBzvrz4w0SzpWpBTI02UgWOkFR9sbAqPrvj8LOtF5R5jow";
fn js_err(context: &str, e: JsValue) -> String {
format!("{context}: {}", e.as_string().unwrap_or_else(|| format!("{e:?}")))
}
fn window() -> Result<web_sys::Window, String> {
web_sys::window().ok_or_else(|| "no window".to_string())
}
pub(crate) async fn ensure_permission() -> Result<bool, String> {
use web_sys::{Notification, NotificationPermission};
match Notification::permission() {
NotificationPermission::Granted => return Ok(true),
NotificationPermission::Denied => return Ok(false),
_ => {}
}
let promise = Notification::request_permission().map_err(|e| js_err("requestPermission", e))?;
let result = JsFuture::from(promise)
.await
.map_err(|e| js_err("requestPermission", e))?;
Ok(result.as_string().as_deref() == Some("granted"))
}
async fn sw_registration() -> Option<web_sys::ServiceWorkerRegistration> {
let sw = web_sys::window()?.navigator().service_worker();
let v = JsFuture::from(sw.get_registration()).await.ok()?;
v.dyn_into::<web_sys::ServiceWorkerRegistration>().ok()
}
pub(crate) async fn show(title: &str, body: &str) -> Result<(), String> {
let opts = web_sys::NotificationOptions::new();
opts.set_body(body);
opts.set_icon("/icons/icon-192.png");
opts.set_tag(&format!("lh-{title}-{body}"));
if let Some(reg) = sw_registration().await {
let promise = reg
.show_notification_with_options(title, &opts)
.map_err(|e| js_err("showNotification", e))?;
JsFuture::from(promise)
.await
.map_err(|e| js_err("showNotification", e))?;
return Ok(());
}
web_sys::Notification::new_with_options(title, &opts)
.map(|_| ())
.map_err(|e| js_err("Notification", e))
}
pub(crate) fn vibrate(ms: u32) -> bool {
match web_sys::window() {
Some(win) => win.navigator().vibrate_with_duration(ms),
None => false,
}
}
pub(crate) async fn subscribe_push() -> Result<String, String> {
let container = window()?.navigator().service_worker();
let _ = JsFuture::from(container.register("/sw.js")).await;
let ready = container.ready().map_err(|e| js_err("serviceWorker.ready", e))?;
let reg: web_sys::ServiceWorkerRegistration = JsFuture::from(ready)
.await
.map_err(|e| js_err("serviceWorker.ready", e))?
.dyn_into()
.map_err(|_| "serviceWorker.ready: not a registration".to_string())?;
let manager = reg.push_manager().map_err(|e| js_err("pushManager", e))?;
let opts = web_sys::PushSubscriptionOptionsInit::new();
opts.set_user_visible_only(true);
let key_bytes = {
use base64::Engine as _;
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(VAPID_PUBLIC_KEY)
.map_err(|e| format!("bad VAPID_PUBLIC_KEY constant: {e}"))?
};
let key_js: JsValue = js_sys::Uint8Array::from(key_bytes.as_slice()).into();
opts.set_application_server_key(&key_js);
let sub: web_sys::PushSubscription = JsFuture::from(
manager
.subscribe_with_options(&opts)
.map_err(|e| js_err("push subscribe", e))?,
)
.await
.map_err(|e| js_err("push subscribe", e))?
.dyn_into()
.map_err(|_| "push subscribe: not a PushSubscription".to_string())?;
js_sys::JSON::stringify(&sub)
.map_err(|e| js_err("subscription stringify", e))?
.as_string()
.ok_or_else(|| "subscription stringify: empty".to_string())
}
pub(crate) async fn enable_and_publish() -> Result<String, String> {
if !ensure_permission().await? {
return Err("notification permission denied — allow notifications for this site in the browser settings".to_string());
}
let sub_json = subscribe_push().await?;
let (name, owner) = crate::app::tenant::current_tenant_owner().await?;
let token_id = match crate::registry::main_of(&owner).await {
Ok(id) if id != 0 => id,
_ => match crate::registry::id_of_name(&name).await {
Ok(id) if id != 0 => id,
Ok(_) => return Err("this subdomain isn't registered on-chain yet".to_string()),
Err(e) => return Err(format!("id_of_name: {e}")),
},
};
let registry_addr = crate::encoding::parse_address(crate::registry::REGISTRY_ADDRESS)?;
let call = crate::tempo_tx::TempoCall {
to: registry_addr,
value_wei: 0,
input: crate::registry::encode_set_push_sub(token_id, sub_json.as_bytes()),
};
let gas = crate::app::gas::set_metadata_gas(sub_json.len());
crate::app::events::run_sponsored_tempo_call(&owner, vec![call], gas, "publish push subscription")
.await
}
pub(crate) async fn refresh_subscription_if_stale() {
if !matches!(
web_sys::Notification::permission(),
web_sys::NotificationPermission::Granted
) {
return;
}
let Ok(current) = subscribe_push().await else {
return;
};
let Ok((name, owner)) = crate::app::tenant::current_tenant_owner().await else {
return;
};
let token_id = match crate::registry::main_of(&owner).await {
Ok(id) if id != 0 => id,
_ => match crate::registry::id_of_name(&name).await {
Ok(id) if id != 0 => id,
_ => return,
},
};
let published = crate::registry::push_sub_of(token_id)
.await
.ok()
.flatten()
.unwrap_or_default();
if published == current {
return;
}
let publish = async {
let registry_addr = crate::encoding::parse_address(crate::registry::REGISTRY_ADDRESS)?;
let call = crate::tempo_tx::TempoCall {
to: registry_addr,
value_wei: 0,
input: crate::registry::encode_set_push_sub(token_id, current.as_bytes()),
};
let gas = crate::app::gas::set_metadata_gas(current.len());
crate::app::events::run_sponsored_tempo_call(
&owner,
vec![call],
gas,
"refresh push subscription",
)
.await
};
match publish.await {
Err(e) => web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(&format!(
"push subscription refresh failed: {e}"
))),
Ok(_) => web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(
"stale push subscription refreshed on-chain",
)),
}
}