leptos_use/
use_cookie.rs

1#![allow(clippy::too_many_arguments)]
2
3use crate::core::now;
4use crate::utils::get_header;
5use codee::{CodecError, Decoder, Encoder};
6use cookie::time::{Duration, OffsetDateTime};
7pub use cookie::SameSite;
8use cookie::{Cookie, CookieJar};
9use default_struct_builder::DefaultBuilder;
10use leptos::{
11    logging::{debug_warn, error},
12    prelude::*,
13};
14use std::sync::Arc;
15
16/// SSR-friendly and reactive cookie access.
17///
18/// You can use this function multiple times for the same cookie and their signals will synchronize
19/// (even across windows/tabs). But there is no way to listen to changes to `document.cookie` directly so in case
20/// something outside of this function changes the cookie, the signal will **not** be updated.
21///
22/// When the options `max_age` or `expire` is given then the returned signal will
23/// automatically turn to `None` after that time.
24///
25/// ## Demo
26///
27/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_cookie)
28///
29/// ## Usage
30///
31/// The example below creates a cookie called `counter`. If the cookie doesn't exist, it is initially set to a random value.
32/// Whenever we update the `counter` variable, the cookie will be updated accordingly.
33///
34/// ```
35/// # use leptos::prelude::*;
36/// # use leptos_use::use_cookie;
37/// # use codee::string::FromToStringCodec;
38/// # use rand::random;
39///
40/// #
41/// # #[component]
42/// # fn Demo() -> impl IntoView {
43/// let (counter, set_counter) = use_cookie::<u32, FromToStringCodec>("counter");
44///
45/// let reset = move || set_counter.set(Some(random()));
46///
47/// if counter.get().is_none() {
48///     reset();
49/// }
50///
51/// let increase = move || {
52///     set_counter.set(counter.get().map(|c| c + 1));
53/// };
54///
55/// view! {
56///     <p>Counter: {move || counter.get().map(|c| c.to_string()).unwrap_or("—".to_string())}</p>
57///     <button on:click=move |_| reset()>Reset</button>
58///     <button on:click=move |_| increase()>+</button>
59/// }
60/// # }
61/// ```
62///
63/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
64/// binary codec wrapped in `Base64`.
65///
66/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
67/// > available and what feature flags they require.
68///
69/// ## Cookie attributes
70///
71/// As part of the options when you use `use_cookie_with_options` you can specify cookie attributes.
72///
73/// ```
74/// # use cookie::SameSite;
75/// # use leptos::prelude::*;
76/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
77/// # use codee::string::FromToStringCodec;
78/// #
79/// # #[component]
80/// # fn Demo() -> impl IntoView {
81/// let (cookie, set_cookie) = use_cookie_with_options::<bool, FromToStringCodec>(
82///     "user_info",
83///     UseCookieOptions::default()
84///         .max_age(3600_000) // one hour
85///         .same_site(SameSite::Lax)
86/// );
87/// #
88/// # view! {}
89/// # }
90/// ```
91///
92/// ## Server-Side Rendering
93///
94/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
95///
96/// This works equally well on the server or the client.
97/// On the server this function reads the cookie from the HTTP request header and writes it back into
98/// the HTTP response header according to options (if provided).
99/// The returned `WriteSignal` may not affect the cookie headers on the server! It will try and write
100/// the headers but if this happens after the headers have already been streamed to the client then
101/// this will have no effect.
102///
103/// > If you're using `axum` you have to enable the `"axum"` feature in your Cargo.toml.
104/// > In case it's `actix-web` enable the feature `"actix"`..
105///
106/// ### Bring your own header
107///
108/// In case you're neither using Axum nor Actix or the default implementation is not to your liking,
109/// you can provide your own way of reading and writing the cookie header value.
110///
111/// ```
112/// # use cookie::Cookie;
113/// # use leptos::prelude::*;
114/// # use serde::{Deserialize, Serialize};
115/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
116/// # use codee::string::JsonSerdeCodec;
117/// #
118/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
119/// # pub struct Auth {
120/// #     pub username: String,
121/// #     pub token: String,
122/// # }
123/// #
124/// # #[component]
125/// # fn Demo() -> impl IntoView {
126/// use_cookie_with_options::<Auth, JsonSerdeCodec>(
127///     "auth",
128///     UseCookieOptions::default()
129///         .ssr_cookies_header_getter(|| {
130///             #[cfg(feature = "ssr")]
131///             {
132///                 Some("Somehow get the value of the cookie header as a string".to_owned())
133///             }
134///             #[cfg(not(feature = "ssr"))]
135///             None
136///         })
137///         .ssr_set_cookie(|cookie: &Cookie| {
138///             #[cfg(feature = "ssr")]
139///             {
140///                 // somehow insert the Set-Cookie header for this cookie
141///             }
142///         }),
143/// );
144/// # view! {}
145/// # }
146/// ```
147pub fn use_cookie<T, C>(cookie_name: &str) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
148where
149    C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
150    T: Clone + Send + Sync + 'static,
151    T: std::fmt::Debug,
152    <C as Encoder<T>>::Error: std::fmt::Debug,
153{
154    use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
155}
156
157/// Version of [`use_cookie`] that takes [`UseCookieOptions`].
158pub fn use_cookie_with_options<T, C>(
159    cookie_name: &str,
160    options: UseCookieOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
161) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
162where
163    C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
164    T: Clone + Send + Sync + 'static,
165    T: std::fmt::Debug,
166    <C as Encoder<T>>::Error: std::fmt::Debug,
167{
168    let UseCookieOptions {
169        max_age,
170        expires,
171        http_only,
172        secure,
173        domain,
174        path,
175        same_site,
176        ssr_cookies_header_getter,
177        ssr_set_cookie,
178        default_value,
179        readonly,
180        on_error,
181    } = options;
182
183    let delay = if let Some(max_age) = max_age {
184        Some(max_age)
185    } else {
186        expires.map(|expires| expires * 1000 - now() as i64)
187    };
188
189    let has_expired = if let Some(delay) = delay {
190        delay <= 0
191    } else {
192        false
193    };
194
195    let jar = StoredValue::new(CookieJar::new());
196
197    let (cookie, set_cookie) = if !has_expired {
198        let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
199
200        let new_cookie = jar.try_update_value(|jar| {
201            *jar = load_and_parse_cookie_jar(ssr_cookies_header_getter)?;
202            jar.get(cookie_name)
203                .and_then(|c| {
204                    C::decode(c.value())
205                        .map_err(|err| on_error(CodecError::Decode(err)))
206                        .ok()
207                })
208                .or(default_value)
209        });
210
211        let out = signal(new_cookie.flatten());
212        handle_expiration(delay, out.1);
213        out
214    } else {
215        debug_warn!(
216            "not setting cookie '{}' because it has already expired",
217            cookie_name
218        );
219
220        signal(None::<T>)
221    };
222
223    #[cfg(not(feature = "ssr"))]
224    {
225        use crate::{
226            use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
227        };
228        use codee::string::{FromToStringCodec, OptionCodec};
229
230        let UseBroadcastChannelReturn { message, post, .. } =
231            use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&format!(
232                "leptos-use:cookies:{cookie_name}"
233            ));
234
235        let on_cookie_change = {
236            let cookie_name = cookie_name.to_owned();
237            let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
238            let on_error = Arc::clone(&on_error);
239            let domain = domain.clone();
240            let path = path.clone();
241
242            move || {
243                if readonly {
244                    return;
245                }
246
247                let value = cookie.try_with_untracked(|cookie| {
248                    cookie.as_ref().and_then(|cookie| {
249                        C::encode(cookie)
250                            .map_err(|err| on_error(CodecError::Encode(err)))
251                            .ok()
252                    })
253                });
254
255                if let Some(value) = value {
256                    if value
257                        == jar.with_value(|jar| jar.get(&cookie_name).map(|c| c.value().to_owned()))
258                    {
259                        return;
260                    }
261
262                    jar.update_value(|jar| {
263                        write_client_cookie(
264                            &cookie_name,
265                            &value,
266                            jar,
267                            max_age,
268                            expires,
269                            &domain,
270                            &path,
271                            same_site,
272                            secure,
273                            http_only,
274                            Arc::clone(&ssr_cookies_header_getter),
275                        );
276                    });
277
278                    post(&value);
279                }
280            }
281        };
282
283        let WatchPausableReturn {
284            pause,
285            resume,
286            stop,
287            ..
288        } = watch_pausable(move || cookie.track(), {
289            let on_cookie_change = on_cookie_change.clone();
290
291            move |_, _, _| {
292                on_cookie_change();
293            }
294        });
295
296        // listen to cookie changes from the broadcast channel
297        Effect::new({
298            let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
299            let cookie_name = cookie_name.to_owned();
300
301            move |_| {
302                if let Some(message) = message.get() {
303                    pause();
304
305                    if let Some(message) = message {
306                        match C::decode(&message) {
307                            Ok(value) => {
308                                let ssr_cookies_header_getter =
309                                    Arc::clone(&ssr_cookies_header_getter);
310
311                                jar.update_value(|jar| {
312                                    update_client_cookie_jar(
313                                        &cookie_name,
314                                        &Some(message),
315                                        jar,
316                                        max_age,
317                                        expires,
318                                        &domain,
319                                        &path,
320                                        same_site,
321                                        secure,
322                                        http_only,
323                                        ssr_cookies_header_getter,
324                                    );
325                                });
326
327                                set_cookie.set(Some(value));
328                            }
329                            Err(err) => {
330                                on_error(CodecError::Decode(err));
331                            }
332                        }
333                    } else {
334                        let cookie_name = cookie_name.clone();
335                        let ssr_cookies_header_getter = Arc::clone(&ssr_cookies_header_getter);
336
337                        jar.update_value(|jar| {
338                            update_client_cookie_jar(
339                                &cookie_name,
340                                &None,
341                                jar,
342                                max_age,
343                                expires,
344                                &domain,
345                                &path,
346                                same_site,
347                                secure,
348                                http_only,
349                                ssr_cookies_header_getter,
350                            );
351                            jar.force_remove(cookie_name);
352                        });
353
354                        set_cookie.set(None);
355                    }
356
357                    resume();
358                }
359            }
360        });
361
362        on_cleanup(move || {
363            stop();
364            on_cookie_change();
365        });
366
367        let _ = ssr_set_cookie;
368    }
369
370    #[cfg(feature = "ssr")]
371    {
372        if !readonly {
373            ImmediateEffect::new_isomorphic({
374                let cookie_name = cookie_name.to_owned();
375                let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
376
377                let lock = Arc::new(std::sync::Mutex::new(()));
378
379                move || {
380                    if let Ok(_lock_guard) = lock.clone().lock() {
381                        let domain = domain.clone();
382                        let path = path.clone();
383
384                        if let Some(value) = cookie.try_with(|cookie| {
385                            cookie.as_ref().map(|cookie| {
386                                C::encode(cookie)
387                                    .map_err(|err| on_error(CodecError::Encode(err)))
388                                    .ok()
389                            })
390                        }) {
391                            jar.update_value({
392                                let domain = domain.clone();
393                                let path = path.clone();
394                                let ssr_set_cookie = Arc::clone(&ssr_set_cookie);
395
396                                |jar| {
397                                    write_server_cookie(
398                                        &cookie_name,
399                                        value.flatten(),
400                                        jar,
401                                        max_age,
402                                        expires,
403                                        domain,
404                                        path,
405                                        same_site,
406                                        secure,
407                                        http_only,
408                                        ssr_set_cookie,
409                                    )
410                                }
411                            });
412                        }
413                    }
414                }
415            });
416        }
417    }
418
419    (cookie.into(), set_cookie)
420}
421
422/// Options for [`use_cookie_with_options`].
423#[derive(DefaultBuilder)]
424pub struct UseCookieOptions<T, E, D> {
425    /// [`Max-Age` of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.2) in milliseconds. The returned signal will turn to `None` after the max age is reached.
426    /// Default: `None`
427    ///
428    /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
429    /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
430    /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
431    ///
432    /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
433    #[builder(into)]
434    max_age: Option<i64>,
435
436    /// [Expiration date-time of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.1) as UNIX timestamp in seconds.
437    /// The signal will turn to `None` after the expiration date-time is reached.
438    /// Default: `None`
439    ///
440    /// > The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states
441    /// > that if both `expires` and `max_age` is set, then `max_age` takes precedence,
442    /// > but not all clients may obey this, so if both are set, they should point to the same date and time!
443    ///
444    /// > If neither of `expires` and `max_age` is set, the cookie will be session-only and removed when the user closes their browser.
445    #[builder(into)]
446    expires: Option<i64>,
447
448    /// Specifies the [`HttpOnly` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
449    /// When `true`, the `HttpOnly` attribute is set; otherwise it is not.
450    /// By default, the `HttpOnly` attribute is not set.
451    ///
452    /// > Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
453    http_only: bool,
454
455    /// Specifies the value for the [`Secure` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
456    /// When `true`, the `Secure` attribute is set; otherwise it is not.
457    /// By default, the `Secure` attribute is not set.
458    ///
459    /// > Be careful when setting this to `true`, as compliant clients will not send the cookie back to the
460    /// > server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
461    secure: bool,
462
463    /// Specifies the value for the [`Domain` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
464    /// By default, no domain is set, and most clients will consider applying the cookie only to the current domain.
465    #[builder(into)]
466    domain: Option<String>,
467
468    /// Specifies the value for the [`Path` cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
469    /// By default, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
470    #[builder(into)]
471    path: Option<String>,
472
473    /// Specifies the value for the [`SameSite` cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
474    ///
475    /// - `'Some(SameSite::Lax)'` will set the `SameSite` attribute to `Lax` for lax same-site enforcement.
476    /// - `'Some(SameSite::None)'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
477    /// - `'Some(SameSite::Strict)'` will set the `SameSite` attribute to `Strict` for strict same-site enforcement.
478    /// - `None` will not set the `SameSite` attribute (default).
479    ///
480    /// More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).
481    #[builder(into)]
482    same_site: Option<SameSite>,
483
484    /// The default cookie value in case the cookie is not set.
485    /// Defaults to `None`.
486    default_value: Option<T>,
487
488    /// If `true` the returned `WriteSignal` will not affect the actual cookie.
489    /// Default: `false`
490    readonly: bool,
491
492    /// Getter function to return the string value of the cookie header.
493    /// When you use one of the features `"axum"` or `"actix"` there's a valid default implementation provided.
494    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
495
496    /// Function to add a set cookie header to the response on the server.
497    /// When you use one of the features `"axum"` or `"actix"` there's a valid default implementation provided.
498    ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
499
500    /// Callback for encoding/decoding errors. Defaults to logging the error to the console.
501    on_error: Arc<dyn Fn(CodecError<E, D>) + Send + Sync>,
502}
503
504impl<T, E, D> Default for UseCookieOptions<T, E, D> {
505    #[allow(dead_code)]
506    fn default() -> Self {
507        Self {
508            max_age: None,
509            expires: None,
510            http_only: false,
511            default_value: None,
512            readonly: false,
513            secure: false,
514            domain: None,
515            path: None,
516            same_site: None,
517            ssr_cookies_header_getter: Arc::new(move || {
518                get_header!(COOKIE, use_cookie, ssr_cookies_header_getter)
519            }),
520            ssr_set_cookie: Arc::new(|cookie: &Cookie| {
521                #[cfg(feature = "ssr")]
522                {
523                    server_set_cookie(cookie);
524                }
525
526                let _ = cookie;
527            }),
528            on_error: Arc::new(|_| {
529                error!("cookie (de-/)serialization error");
530            }),
531        }
532    }
533}
534
535fn read_cookies_string(
536    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
537) -> Option<String> {
538    let cookies;
539
540    #[cfg(feature = "ssr")]
541    {
542        cookies = ssr_cookies_header_getter();
543    }
544
545    #[cfg(not(feature = "ssr"))]
546    {
547        use wasm_bindgen::JsCast;
548
549        let _ = ssr_cookies_header_getter;
550
551        let js_value: wasm_bindgen::JsValue = document().into();
552        let document: web_sys::HtmlDocument = js_value.unchecked_into();
553        cookies = Some(document.cookie().unwrap_or_default());
554    }
555
556    cookies
557}
558
559fn handle_expiration<T>(delay: Option<i64>, set_cookie: WriteSignal<Option<T>>)
560where
561    T: Send + Sync + 'static,
562{
563    if let Some(delay) = delay {
564        #[cfg(not(feature = "ssr"))]
565        {
566            use leptos::leptos_dom::helpers::TimeoutHandle;
567            use std::sync::{atomic::AtomicI32, Mutex};
568
569            // The maximum value allowed on a timeout delay.
570            // Reference: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
571            const MAX_TIMEOUT_DELAY: i64 = 2_147_483_647;
572
573            let timeout = Arc::new(Mutex::new(None::<TimeoutHandle>));
574            let elapsed = Arc::new(AtomicI32::new(0));
575
576            on_cleanup({
577                let timeout = Arc::clone(&timeout);
578                move || {
579                    if let Some(timeout) = timeout.lock().unwrap().take() {
580                        timeout.clear();
581                    }
582                }
583            });
584
585            let create_expiration_timeout =
586                Arc::new(Mutex::new(None::<Box<dyn Fn() + Send + Sync>>));
587
588            *create_expiration_timeout.lock().unwrap() = Some(Box::new({
589                let timeout = Arc::clone(&timeout);
590                let elapsed = Arc::clone(&elapsed);
591                let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
592
593                move || {
594                    if let Some(timeout) = timeout.lock().unwrap().take() {
595                        timeout.clear();
596                    }
597
598                    let time_remaining =
599                        delay - elapsed.load(std::sync::atomic::Ordering::Relaxed) as i64;
600                    let timeout_length = time_remaining.min(MAX_TIMEOUT_DELAY);
601
602                    let elapsed = Arc::clone(&elapsed);
603                    let create_expiration_timeout = Arc::clone(&create_expiration_timeout);
604
605                    *timeout.lock().unwrap() = set_timeout_with_handle(
606                        move || {
607                            let elapsed = elapsed.fetch_add(
608                                timeout_length as i32,
609                                std::sync::atomic::Ordering::Relaxed,
610                            ) as i64
611                                + timeout_length;
612
613                            if elapsed < delay {
614                                if let Some(create_expiration_timeout) =
615                                    create_expiration_timeout.lock().unwrap().as_ref()
616                                {
617                                    create_expiration_timeout();
618                                }
619                                return;
620                            }
621
622                            set_cookie.set(None);
623                        },
624                        std::time::Duration::from_millis(timeout_length as u64),
625                    )
626                    .ok();
627                }
628            }));
629
630            if let Some(create_expiration_timeout) =
631                create_expiration_timeout.lock().unwrap().as_ref()
632            {
633                create_expiration_timeout();
634            };
635        }
636
637        #[cfg(feature = "ssr")]
638        {
639            let _ = set_cookie;
640            let _ = delay;
641        }
642    }
643}
644
645#[cfg(not(feature = "ssr"))]
646fn write_client_cookie(
647    name: &str,
648    value: &Option<String>,
649    jar: &mut CookieJar,
650    max_age: Option<i64>,
651    expires: Option<i64>,
652    domain: &Option<String>,
653    path: &Option<String>,
654    same_site: Option<SameSite>,
655    secure: bool,
656    http_only: bool,
657    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
658) {
659    use wasm_bindgen::JsCast;
660
661    update_client_cookie_jar(
662        name,
663        value,
664        jar,
665        max_age,
666        expires,
667        domain,
668        path,
669        same_site,
670        secure,
671        http_only,
672        ssr_cookies_header_getter,
673    );
674
675    let document = document();
676    let document: &web_sys::HtmlDocument = document.unchecked_ref();
677
678    document.set_cookie(&cookie_jar_to_string(jar, name)).ok();
679}
680
681#[cfg(not(feature = "ssr"))]
682fn update_client_cookie_jar(
683    name: &str,
684    value: &Option<String>,
685    jar: &mut CookieJar,
686    max_age: Option<i64>,
687    expires: Option<i64>,
688    domain: &Option<String>,
689    path: &Option<String>,
690    same_site: Option<SameSite>,
691    secure: bool,
692    http_only: bool,
693    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
694) {
695    if let Some(new_jar) = load_and_parse_cookie_jar(ssr_cookies_header_getter) {
696        *jar = new_jar;
697        if let Some(value) = value {
698            let cookie = build_cookie_from_options(
699                name, max_age, expires, http_only, secure, path, same_site, domain, value,
700            );
701
702            jar.add_original(cookie);
703        } else {
704            let max_age = Some(0);
705            let expires = Some(0);
706            let value = "";
707            let cookie = build_cookie_from_options(
708                name, max_age, expires, http_only, secure, path, same_site, domain, value,
709            );
710
711            jar.add(cookie);
712        }
713    }
714}
715
716#[cfg(not(feature = "ssr"))]
717fn cookie_jar_to_string(jar: &CookieJar, name: &str) -> String {
718    match jar.get(name) {
719        Some(c) => c.encoded().to_string(),
720        None => "".to_string(),
721    }
722}
723
724fn build_cookie_from_options(
725    name: &str,
726    max_age: Option<i64>,
727    expires: Option<i64>,
728    http_only: bool,
729    secure: bool,
730    path: &Option<String>,
731    same_site: Option<SameSite>,
732    domain: &Option<String>,
733    value: &str,
734) -> Cookie<'static> {
735    let mut cookie = Cookie::build((name, value));
736    if let Some(max_age) = max_age {
737        cookie = cookie.max_age(Duration::milliseconds(max_age));
738    }
739    if let Some(expires) = expires {
740        match OffsetDateTime::from_unix_timestamp(expires) {
741            Ok(expires) => {
742                cookie = cookie.expires(expires);
743            }
744            Err(err) => {
745                debug_warn!("failed to set cookie expiration: {:?}", err);
746            }
747        }
748    }
749    if http_only {
750        cookie = cookie.http_only(true);
751    }
752    if secure {
753        cookie = cookie.secure(true);
754    }
755    if let Some(domain) = domain {
756        cookie = cookie.domain(domain);
757    }
758    if let Some(path) = path {
759        cookie = cookie.path(path);
760    }
761    if let Some(same_site) = same_site {
762        cookie = cookie.same_site(same_site);
763    }
764
765    let cookie: Cookie = cookie.into();
766    cookie.into_owned()
767}
768
769#[cfg(feature = "ssr")]
770fn write_server_cookie(
771    name: &str,
772    value: Option<String>,
773    jar: &mut CookieJar,
774    max_age: Option<i64>,
775    expires: Option<i64>,
776    domain: Option<String>,
777    path: Option<String>,
778    same_site: Option<SameSite>,
779    secure: bool,
780    http_only: bool,
781    ssr_set_cookie: Arc<dyn Fn(&Cookie) + Send + Sync>,
782) {
783    if let Some(value) = value {
784        let cookie: Cookie = build_cookie_from_options(
785            name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
786        );
787
788        jar.add(cookie.into_owned());
789    } else {
790        jar.remove(name.to_owned());
791    }
792
793    for cookie in jar.delta() {
794        ssr_set_cookie(cookie);
795    }
796}
797
798fn load_and_parse_cookie_jar(
799    ssr_cookies_header_getter: Arc<dyn Fn() -> Option<String> + Send + Sync>,
800) -> Option<CookieJar> {
801    read_cookies_string(ssr_cookies_header_getter).map(|cookies| {
802        let mut jar = CookieJar::new();
803        for cookie in Cookie::split_parse_encoded(cookies).flatten() {
804            jar.add_original(cookie);
805        }
806
807        jar
808    })
809}
810
811#[cfg(feature = "ssr")]
812fn server_set_cookie(cookie: &Cookie) {
813    #[cfg(feature = "actix")]
814    use leptos_actix::ResponseOptions;
815    #[cfg(feature = "axum")]
816    use leptos_axum::ResponseOptions;
817
818    #[cfg(feature = "actix")]
819    const SET_COOKIE: http0_2::HeaderName = http0_2::header::SET_COOKIE;
820    #[cfg(feature = "axum")]
821    const SET_COOKIE: http1::HeaderName = http1::header::SET_COOKIE;
822
823    #[cfg(feature = "actix")]
824    type HeaderValue = http0_2::HeaderValue;
825    #[cfg(feature = "axum")]
826    type HeaderValue = http1::HeaderValue;
827
828    #[cfg(all(not(feature = "axum"), not(feature = "actix")))]
829    {
830        use leptos::logging::warn;
831        let _ = cookie;
832        warn!("If you're using use_cookie without the feature `axum` or `actix` enabled, you should provide the option `ssr_set_cookie`");
833    }
834
835    #[cfg(any(feature = "axum", feature = "actix"))]
836    {
837        if let Some(response_options) = use_context::<ResponseOptions>() {
838            if let Ok(header_value) = HeaderValue::from_str(&cookie.encoded().to_string()) {
839                response_options.append_header(SET_COOKIE, header_value);
840            }
841        }
842    }
843}