Skip to main content

cranpose_services/
uri_handler.rs

1use cranpose_core::compositionLocalOf;
2use cranpose_core::CompositionLocal;
3use cranpose_core::CompositionLocalProvider;
4use cranpose_macros::composable;
5use std::cell::RefCell;
6use std::rc::Rc;
7
8#[derive(thiserror::Error, Debug)]
9pub enum UriHandlerError {
10    #[error("Failed to open URL: {0}")]
11    OpenFailed(String),
12    #[error("No window object available")]
13    NoWindow,
14    #[error("Popup blocked for URL: {0}")]
15    PopupBlocked(String),
16    #[error("Opening external links is not supported on this platform: {0}")]
17    UnsupportedPlatform(String),
18}
19
20pub trait UriHandler {
21    fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError>;
22}
23
24pub type UriHandlerRef = Rc<dyn UriHandler>;
25
26struct PlatformUriHandler;
27
28impl UriHandler for PlatformUriHandler {
29    fn open_uri(&self, uri: &str) -> Result<(), UriHandlerError> {
30        #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
31        {
32            open::that(uri).map_err(|err| UriHandlerError::OpenFailed(format!("{:?}", err)))?;
33            Ok(())
34        }
35
36        #[cfg(target_arch = "wasm32")]
37        {
38            let window = web_sys::window().ok_or(UriHandlerError::NoWindow)?;
39            let opened = window
40                .open_with_url_and_target(uri, "_blank")
41                .map_err(|err| UriHandlerError::OpenFailed(format!("{:?}", err)))?;
42            if opened.is_none() {
43                Err(UriHandlerError::PopupBlocked(uri.to_string()))
44            } else {
45                Ok(())
46            }
47        }
48
49        #[cfg(target_os = "android")]
50        {
51            webbrowser::open(uri).map_err(|err| UriHandlerError::OpenFailed(err.to_string()))?;
52            Ok(())
53        }
54    }
55}
56
57pub fn default_uri_handler() -> UriHandlerRef {
58    Rc::new(PlatformUriHandler)
59}
60
61pub fn local_uri_handler() -> CompositionLocal<UriHandlerRef> {
62    thread_local! {
63        static LOCAL_URI_HANDLER: RefCell<Option<CompositionLocal<UriHandlerRef>>> = const { RefCell::new(None) };
64    }
65
66    LOCAL_URI_HANDLER.with(|cell| {
67        let mut local = cell.borrow_mut();
68        if local.is_none() {
69            *local = Some(compositionLocalOf(default_uri_handler));
70        }
71        local
72            .as_ref()
73            .expect("Uri handler composition local must be initialized")
74            .clone()
75    })
76}
77
78#[allow(non_snake_case)]
79#[composable]
80pub fn ProvideUriHandler(content: impl FnOnce()) {
81    let uri_handler = cranpose_core::remember(default_uri_handler).with(|state| state.clone());
82    let uri_local = local_uri_handler();
83
84    CompositionLocalProvider(vec![uri_local.provides(uri_handler)], move || {
85        content();
86    });
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::run_test_composition;
93    use cranpose_core::CompositionLocalProvider;
94    use std::cell::RefCell;
95
96    struct TestUriHandler;
97
98    impl UriHandler for TestUriHandler {
99        fn open_uri(&self, _uri: &str) -> Result<(), UriHandlerError> {
100            Ok(())
101        }
102    }
103
104    #[test]
105    fn default_uri_handler_can_be_created() {
106        let handler = default_uri_handler();
107        assert_eq!(Rc::strong_count(&handler), 1);
108    }
109
110    #[test]
111    fn local_uri_handler_can_be_overridden() {
112        let local = local_uri_handler();
113        let default_handler = default_uri_handler();
114        let custom_handler: UriHandlerRef = Rc::new(TestUriHandler);
115        let captured = Rc::new(RefCell::new(None));
116
117        {
118            let captured = Rc::clone(&captured);
119            let custom_handler = custom_handler.clone();
120            let local_for_provider = local.clone();
121            let local_for_read = local.clone();
122            run_test_composition(move || {
123                let captured = Rc::clone(&captured);
124                let custom_handler = custom_handler.clone();
125                let local_for_read = local_for_read.clone();
126                CompositionLocalProvider(
127                    vec![local_for_provider.provides(custom_handler)],
128                    move || {
129                        let current = local_for_read.current();
130                        *captured.borrow_mut() = Some(current);
131                    },
132                );
133            });
134        }
135
136        let current = captured
137            .borrow()
138            .as_ref()
139            .expect("handler captured")
140            .clone();
141        assert!(Rc::ptr_eq(&current, &custom_handler));
142        assert!(!Rc::ptr_eq(&current, &default_handler));
143    }
144
145    #[test]
146    fn provide_uri_handler_sets_current_handler() {
147        let local = local_uri_handler();
148        let captured = Rc::new(RefCell::new(None));
149
150        {
151            let captured = Rc::clone(&captured);
152            let local = local.clone();
153            run_test_composition(move || {
154                let captured = Rc::clone(&captured);
155                let local = local.clone();
156                ProvideUriHandler(move || {
157                    let current = local.current();
158                    *captured.borrow_mut() = Some(current);
159                });
160            });
161        }
162
163        assert!(captured.borrow().is_some());
164    }
165}