Skip to main content

dioxus_clerk/
options.rs

1//! Clerk options mapping: typed option builders for Clerk JS calls.
2//!
3//! The typed builders are the single surface translating Rust-side option
4//! names into clerk-js JSON option keys: Clerk widget component props
5//! delegate to them through the `maybe_*` setter variants, so each clerk-js
6//! key is stated exactly once. Use [`JsonOptions::option`] or
7//! `serde_json::Value` directly for Clerk options this crate has not named
8//! yet.
9//!
10//! Each Clerk widget component's `options` prop is typed to *its own* builder,
11//! so handing it the wrong builder is a compile error rather than silently
12//! forwarding option keys the widget ignores:
13//!
14//! ```compile_fail
15//! use dioxus_clerk::{ClerkOptions, SignInOptions};
16//! // `ClerkOptions` cannot stand in for `SignInOptions`.
17//! let _: SignInOptions = ClerkOptions::new().into();
18//! ```
19//!
20//! A raw [`serde_json::Value`] is still accepted for any builder, so the
21//! escape hatch for un-named Clerk options keeps working:
22//!
23//! ```
24//! use dioxus_clerk::SignInOptions;
25//! let _: SignInOptions = dioxus_clerk::serde_json::json!({ "signUpUrl": "/su" }).into();
26//! ```
27
28use serde_json::{Map, Value};
29
30/// Clerk option keys shared by the typed builders, so each key is spelled
31/// once. clerk-js silently ignores misspelled keys; a shared constant turns
32/// drift into a compile error.
33pub(crate) mod keys {
34    pub(crate) const AFTER_CREATE_ORGANIZATION_URL: &str = "afterCreateOrganizationUrl";
35    pub(crate) const AFTER_JOIN_WAITLIST_URL: &str = "afterJoinWaitlistUrl";
36    pub(crate) const AFTER_MULTI_SESSION_SINGLE_SIGN_OUT_URL: &str =
37        "afterMultiSessionSingleSignOutUrl";
38    pub(crate) const AFTER_SELECT_ORGANIZATION_URL: &str = "afterSelectOrganizationUrl";
39    pub(crate) const AFTER_SIGN_OUT_URL: &str = "afterSignOutUrl";
40    pub(crate) const AFTER_SWITCH_SESSION_URL: &str = "afterSwitchSessionUrl";
41    pub(crate) const ALLOWED_REDIRECT_ORIGINS: &str = "allowedRedirectOrigins";
42    pub(crate) const ALLOWED_REDIRECT_PROTOCOLS: &str = "allowedRedirectProtocols";
43    pub(crate) const APPEARANCE: &str = "appearance";
44    pub(crate) const CREATE_ORGANIZATION_URL: &str = "createOrganizationUrl";
45    pub(crate) const DEFAULT_OPEN: &str = "defaultOpen";
46    pub(crate) const DOMAIN: &str = "domain";
47    pub(crate) const FALLBACK_REDIRECT_URL: &str = "fallbackRedirectUrl";
48    pub(crate) const FORCE_REDIRECT_URL: &str = "forceRedirectUrl";
49    pub(crate) const INITIAL_VALUES: &str = "initialValues";
50    pub(crate) const IS_SATELLITE: &str = "isSatellite";
51    pub(crate) const LEEWAY_IN_SECONDS: &str = "leewayInSeconds";
52    pub(crate) const LOCALIZATION: &str = "localization";
53    pub(crate) const ORGANIZATION_ID: &str = "organizationId";
54    pub(crate) const ORGANIZATION_PROFILE_URL: &str = "organizationProfileUrl";
55    pub(crate) const PATH: &str = "path";
56    pub(crate) const PREFETCH_UI: &str = "prefetchUI";
57    pub(crate) const PROXY_URL: &str = "proxyUrl";
58    pub(crate) const REDIRECT_URL: &str = "redirectUrl";
59    pub(crate) const REDIRECT_URL_COMPLETE: &str = "redirectUrlComplete";
60    pub(crate) const ROUTING: &str = "routing";
61    pub(crate) const SATELLITE_AUTO_SYNC: &str = "satelliteAutoSync";
62    pub(crate) const SESSION_ID: &str = "sessionId";
63    pub(crate) const SHOW_NAME: &str = "showName";
64    pub(crate) const SIGN_IN_FALLBACK_REDIRECT_URL: &str = "signInFallbackRedirectUrl";
65    pub(crate) const SIGN_IN_FORCE_REDIRECT_URL: &str = "signInForceRedirectUrl";
66    pub(crate) const SIGN_IN_URL: &str = "signInUrl";
67    pub(crate) const SIGN_UP_FALLBACK_REDIRECT_URL: &str = "signUpFallbackRedirectUrl";
68    pub(crate) const SIGN_UP_FORCE_REDIRECT_URL: &str = "signUpForceRedirectUrl";
69    pub(crate) const SIGN_UP_URL: &str = "signUpUrl";
70    pub(crate) const SKIP_CACHE: &str = "skipCache";
71    pub(crate) const TASK_URLS: &str = "taskUrls";
72    pub(crate) const TEMPLATE: &str = "template";
73    pub(crate) const TRANSFERABLE: &str = "transferable";
74    pub(crate) const USER_PROFILE_MODE: &str = "userProfileMode";
75    pub(crate) const USER_PROFILE_PROPS: &str = "userProfileProps";
76    pub(crate) const USER_PROFILE_URL: &str = "userProfileUrl";
77    pub(crate) const WAITLIST_URL: &str = "waitlistUrl";
78}
79
80/// Generic JSON option builder used as the escape hatch for unsupported Clerk
81/// options.
82#[derive(Debug, Clone, PartialEq, Eq)]
83#[must_use = "builders are consuming; assign or chain the returned value"]
84pub struct JsonOptions {
85    value: Value,
86}
87
88impl Default for JsonOptions {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl JsonOptions {
95    /// Start with an empty JSON object.
96    pub fn new() -> Self {
97        Self {
98            value: Value::Object(Map::new()),
99        }
100    }
101
102    /// Wrap an already-built JSON value.
103    pub fn from_value(value: Value) -> Self {
104        Self { value }
105    }
106
107    /// Set a raw clerk-js option by its JavaScript key.
108    ///
109    /// If the wrapped value is not a JSON object (for example a
110    /// [`JsonOptions::from_value`] array or string), it is replaced with an
111    /// empty object before the key is set.
112    pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
113        if !self.value.is_object() {
114            self.value = Value::Object(Map::new());
115        }
116        if let Value::Object(map) = &mut self.value {
117            map.insert(key.into(), value.into());
118        }
119        self
120    }
121
122    /// Convert into the raw JSON value passed to clerk-js.
123    #[must_use = "into_value returns the built options; it does not send them anywhere"]
124    pub fn into_value(self) -> Value {
125        self.value
126    }
127}
128
129impl From<JsonOptions> for Value {
130    fn from(options: JsonOptions) -> Self {
131        options.into_value()
132    }
133}
134
135impl From<&JsonOptions> for Value {
136    fn from(options: &JsonOptions) -> Self {
137        options.value.clone()
138    }
139}
140
141/// Routing mode for Clerk's embedded UI components.
142///
143/// This mirrors clerk-js's fixed `hash`/`path` routing set; it is not
144/// `#[non_exhaustive]` so downstream `match`es need no wildcard arm.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
146pub enum Routing {
147    /// Clerk manages embedded routing through the URL hash.
148    Hash,
149    /// Clerk manages embedded routing through the current path.
150    Path,
151}
152
153impl Routing {
154    /// The raw clerk-js routing string.
155    pub fn as_str(self) -> &'static str {
156        match self {
157            Self::Hash => "hash",
158            Self::Path => "path",
159        }
160    }
161}
162
163impl From<Routing> for Value {
164    fn from(routing: Routing) -> Self {
165        Value::String(routing.as_str().into())
166    }
167}
168
169/// How `UserButton` opens the user profile UI.
170///
171/// This mirrors clerk-js's fixed `modal`/`navigation` set; it is not
172/// `#[non_exhaustive]` so downstream `match`es need no wildcard arm.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub enum UserProfileMode {
175    /// Open the profile as a modal.
176    Modal,
177    /// Navigate to `user_profile_url`.
178    Navigation,
179}
180
181impl UserProfileMode {
182    /// The raw clerk-js user profile mode string.
183    pub fn as_str(self) -> &'static str {
184        match self {
185            Self::Modal => "modal",
186            Self::Navigation => "navigation",
187        }
188    }
189}
190
191impl From<UserProfileMode> for Value {
192    fn from(mode: UserProfileMode) -> Self {
193        Value::String(mode.as_str().into())
194    }
195}
196
197macro_rules! option_wrapper {
198    ($name:ident, $doc:literal) => {
199        #[doc = $doc]
200        #[derive(Debug, Clone, Default, PartialEq, Eq)]
201        #[must_use = "builders are consuming; assign or chain the returned value"]
202        pub struct $name {
203            inner: JsonOptions,
204        }
205
206        impl $name {
207            /// Start with an empty Clerk options object.
208            pub fn new() -> Self {
209                Self {
210                    inner: JsonOptions::new(),
211                }
212            }
213
214            /// Wrap an already-built JSON value.
215            pub fn from_value(value: Value) -> Self {
216                Self {
217                    inner: JsonOptions::from_value(value),
218                }
219            }
220
221            /// Set a raw clerk-js option by its JavaScript key.
222            ///
223            /// If the wrapped value is not a JSON object, it is replaced with
224            /// an empty object before the key is set.
225            pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
226                self.inner = self.inner.option(key, value);
227                self
228            }
229
230            /// Convert into the raw JSON value passed to clerk-js.
231            #[must_use = "into_value returns the built options; it does not send them anywhere"]
232            pub fn into_value(self) -> Value {
233                self.inner.into_value()
234            }
235        }
236
237        impl From<$name> for Value {
238            fn from(options: $name) -> Self {
239                options.into_value()
240            }
241        }
242
243        impl From<&$name> for Value {
244            fn from(options: &$name) -> Self {
245                options.inner.value.clone()
246            }
247        }
248
249        // Raw-`Value` escape hatch: lets a widget's `options` prop accept a
250        // `serde_json::Value` (via `#[props(into)]`) while still rejecting a
251        // different builder type at compile time. Wrong builder in → no
252        // `Into` impl → compile error.
253        impl From<Value> for $name {
254            fn from(value: Value) -> Self {
255                Self::from_value(value)
256            }
257        }
258    };
259}
260
261option_wrapper!(
262    ClerkOptions,
263    "Options forwarded to `Clerk.load(...)` by `ClerkProvider`."
264);
265option_wrapper!(SignInOptions, "Options for sign-in UI and modal flows.");
266option_wrapper!(SignUpOptions, "Options for sign-up UI and modal flows.");
267option_wrapper!(UserButtonOptions, "Options for the Clerk user button.");
268option_wrapper!(UserProfileOptions, "Options for the Clerk user profile UI.");
269option_wrapper!(
270    CreateOrganizationOptions,
271    "Options for the Clerk create-organization UI."
272);
273option_wrapper!(
274    OrganizationProfileOptions,
275    "Options for the Clerk organization profile UI."
276);
277option_wrapper!(
278    OrganizationSwitcherOptions,
279    "Options for the Clerk organization switcher UI."
280);
281option_wrapper!(
282    OrganizationListOptions,
283    "Options for the Clerk organization list UI."
284);
285option_wrapper!(WaitlistOptions, "Options for the Clerk waitlist UI.");
286option_wrapper!(
287    TaskSetupMFAOptions,
288    "Options for the Clerk task setup-MFA UI."
289);
290option_wrapper!(RedirectOptions, "Options for Clerk redirect helpers.");
291option_wrapper!(SignOutOptions, "Options for `Clerk.signOut(...)`.");
292option_wrapper!(GetTokenOptions, "Options for `session.getToken(...)`.");
293
294/// Generate the typed setter table for one option wrapper. Each entry expands
295/// to the public setter plus a `pub(crate) maybe_*` variant used by the Clerk
296/// widget component prop mappings, so a prop that is `None` leaves the
297/// options untouched. Kinds: `string`, `bool`, `u64`, `value` (raw JSON),
298/// `list` (string list), and `enum(Type)` for typed enums convertible to
299/// [`Value`].
300macro_rules! option_setters {
301    ($owner:ident { $($rest:tt)* }) => {
302        impl $owner {
303            option_setters!(@items $($rest)*);
304        }
305    };
306    (@items) => {};
307    (@items $(#[$doc:meta])* string $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
308        $(#[$doc])*
309        pub fn $name(self, value: impl Into<String>) -> Self {
310            self.option($key, value.into())
311        }
312
313        #[allow(dead_code)]
314        pub(crate) fn $maybe(self, value: Option<String>) -> Self {
315            match value {
316                Some(value) => self.$name(value),
317                None => self,
318            }
319        }
320
321        option_setters!(@items $($rest)*);
322    };
323    (@items $(#[$doc:meta])* bool $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
324        $(#[$doc])*
325        pub fn $name(self, value: bool) -> Self {
326            self.option($key, value)
327        }
328
329        #[allow(dead_code)]
330        pub(crate) fn $maybe(self, value: Option<bool>) -> Self {
331            match value {
332                Some(value) => self.$name(value),
333                None => self,
334            }
335        }
336
337        option_setters!(@items $($rest)*);
338    };
339    (@items $(#[$doc:meta])* u64 $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
340        $(#[$doc])*
341        pub fn $name(self, value: u64) -> Self {
342            self.option($key, value)
343        }
344
345        #[allow(dead_code)]
346        pub(crate) fn $maybe(self, value: Option<u64>) -> Self {
347            match value {
348                Some(value) => self.$name(value),
349                None => self,
350            }
351        }
352
353        option_setters!(@items $($rest)*);
354    };
355    (@items $(#[$doc:meta])* value $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
356        $(#[$doc])*
357        pub fn $name(self, value: Value) -> Self {
358            self.option($key, value)
359        }
360
361        #[allow(dead_code)]
362        pub(crate) fn $maybe(self, value: Option<Value>) -> Self {
363            match value {
364                Some(value) => self.$name(value),
365                None => self,
366            }
367        }
368
369        option_setters!(@items $($rest)*);
370    };
371    (@items $(#[$doc:meta])* list $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
372        $(#[$doc])*
373        pub fn $name(self, values: impl IntoIterator<Item = impl Into<String>>) -> Self {
374            let values: Vec<String> = values.into_iter().map(Into::into).collect();
375            self.option($key, serde_json::json!(values))
376        }
377
378        #[allow(dead_code)]
379        pub(crate) fn $maybe(self, values: Option<Vec<String>>) -> Self {
380            match values {
381                Some(values) => self.$name(values),
382                None => self,
383            }
384        }
385
386        option_setters!(@items $($rest)*);
387    };
388    (@items $(#[$doc:meta])* enum($ty:ty) $name:ident / $maybe:ident => $key:path; $($rest:tt)*) => {
389        $(#[$doc])*
390        pub fn $name(self, value: $ty) -> Self {
391            self.option($key, value)
392        }
393
394        #[allow(dead_code)]
395        pub(crate) fn $maybe(self, value: Option<$ty>) -> Self {
396            match value {
397                Some(value) => self.$name(value),
398                None => self,
399            }
400        }
401
402        option_setters!(@items $($rest)*);
403    };
404}
405
406option_setters!(ClerkOptions {
407    /// Set the application sign-in URL.
408    string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
409    /// Set the application sign-up URL.
410    string sign_up_url / maybe_sign_up_url => keys::SIGN_UP_URL;
411    /// Set the URL to redirect to after sign-in when no `redirect_url` is in play.
412    string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
413    /// Set the URL to always redirect to after sign-in.
414    string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
415    /// Set the URL to redirect to after sign-up when no `redirect_url` is in play.
416    string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
417    /// Set the URL to always redirect to after sign-up.
418    string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
419    /// Set the URL Clerk should use after sign-out.
420    string after_sign_out_url / maybe_after_sign_out_url => keys::AFTER_SIGN_OUT_URL;
421    /// Set the URL Clerk should use after signing out one account in multi-session apps.
422    string after_multi_session_single_sign_out_url / maybe_after_multi_session_single_sign_out_url => keys::AFTER_MULTI_SESSION_SINGLE_SIGN_OUT_URL;
423    /// Set the URL Clerk should use after switching sessions in multi-session apps.
424    string after_switch_session_url / maybe_after_switch_session_url => keys::AFTER_SWITCH_SESSION_URL;
425    /// Set the application waitlist URL.
426    string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
427    /// Set the application user profile URL.
428    string user_profile_url / maybe_user_profile_url => keys::USER_PROFILE_URL;
429    /// Set the application organization profile URL.
430    string organization_profile_url / maybe_organization_profile_url => keys::ORGANIZATION_PROFILE_URL;
431    /// Set the application create-organization URL.
432    string create_organization_url / maybe_create_organization_url => keys::CREATE_ORGANIZATION_URL;
433    /// Set Clerk's reverse-proxy URL.
434    string proxy_url / maybe_proxy_url => keys::PROXY_URL;
435    /// Set Clerk's satellite domain.
436    string domain / maybe_domain => keys::DOMAIN;
437    /// Set whether Clerk should treat this app as a satellite application.
438    bool is_satellite / maybe_is_satellite => keys::IS_SATELLITE;
439    /// Set whether satellite apps should automatically sync on initial page load.
440    bool satellite_auto_sync / maybe_satellite_auto_sync => keys::SATELLITE_AUTO_SYNC;
441    /// Set whether Clerk should prefetch its UI package when supported.
442    bool prefetch_ui / maybe_prefetch_ui => keys::PREFETCH_UI;
443    /// Set Clerk's `allowedRedirectOrigins` list.
444    list allowed_redirect_origins / maybe_allowed_redirect_origins => keys::ALLOWED_REDIRECT_ORIGINS;
445    /// Set Clerk's `allowedRedirectProtocols` list.
446    list allowed_redirect_protocols / maybe_allowed_redirect_protocols => keys::ALLOWED_REDIRECT_PROTOCOLS;
447    /// Forward Clerk's raw `appearance` object.
448    value appearance / maybe_appearance => keys::APPEARANCE;
449    /// Forward Clerk's raw `localization` object.
450    value localization / maybe_localization => keys::LOCALIZATION;
451    /// Map clerk-js session-task keys to the app URLs Clerk navigates to when a
452    /// session has a pending task (e.g. `{ "setup-mfa": "/onboarding/mfa" }`).
453    value task_urls / maybe_task_urls => keys::TASK_URLS;
454});
455
456option_setters!(SignInOptions {
457    /// Set embedded routing mode, for example `"path"` or `"hash"`.
458    enum(Routing) routing / maybe_routing => keys::ROUTING;
459    /// Set the path used by embedded routing.
460    string path / maybe_path => keys::PATH;
461    /// Set the sign-up URL linked from the sign-in flow.
462    string sign_up_url / maybe_sign_up_url => keys::SIGN_UP_URL;
463    /// Set the waitlist URL linked from the sign-in flow.
464    string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
465    /// Always redirect here after sign-in.
466    string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
467    /// Fallback redirect URL after sign-in.
468    string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
469    /// Always redirect here after sign-up from sign-in.
470    string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
471    /// Fallback redirect URL after sign-up from sign-in.
472    string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
473    /// Forward Clerk's raw `initialValues` object.
474    value initial_values / maybe_initial_values => keys::INITIAL_VALUES;
475    /// Set whether sign-in attempts can transfer to sign-up when Clerk supports it.
476    bool transferable / maybe_transferable => keys::TRANSFERABLE;
477    /// Forward Clerk's raw `appearance` object.
478    value appearance / maybe_appearance => keys::APPEARANCE;
479});
480
481option_setters!(SignUpOptions {
482    /// Set embedded routing mode, for example `"path"` or `"hash"`.
483    enum(Routing) routing / maybe_routing => keys::ROUTING;
484    /// Set the path used by embedded routing.
485    string path / maybe_path => keys::PATH;
486    /// Set the sign-in URL linked from the sign-up flow.
487    string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
488    /// Set the waitlist URL linked from the sign-up flow.
489    string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
490    /// Always redirect here after sign-up.
491    string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
492    /// Fallback redirect URL after sign-up.
493    string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
494    /// Always redirect here after sign-in from sign-up.
495    string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
496    /// Fallback redirect URL after sign-in from sign-up.
497    string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
498    /// Forward Clerk's raw `initialValues` object.
499    value initial_values / maybe_initial_values => keys::INITIAL_VALUES;
500    /// Forward Clerk's raw `appearance` object.
501    value appearance / maybe_appearance => keys::APPEARANCE;
502});
503
504option_setters!(UserButtonOptions {
505    /// Set the URL Clerk should use after sign-out.
506    string after_sign_out_url / maybe_after_sign_out_url => keys::AFTER_SIGN_OUT_URL;
507    /// Set the URL Clerk should use after switching sessions in multi-session apps.
508    string after_switch_session_url / maybe_after_switch_session_url => keys::AFTER_SWITCH_SESSION_URL;
509    /// Set the URL Clerk should use when adding another account.
510    string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
511    /// Show the user's name next to the avatar when Clerk supports it.
512    bool show_name / maybe_show_name => keys::SHOW_NAME;
513    /// Open the user button menu by default on first render.
514    bool default_open / maybe_default_open => keys::DEFAULT_OPEN;
515    /// Set the user profile mode, for example `"modal"` or `"navigation"`.
516    enum(UserProfileMode) user_profile_mode / maybe_user_profile_mode => keys::USER_PROFILE_MODE;
517    /// Set the user profile URL for navigation mode.
518    string user_profile_url / maybe_user_profile_url => keys::USER_PROFILE_URL;
519    /// Forward options to the underlying `UserProfile` component.
520    value user_profile_props / maybe_user_profile_props => keys::USER_PROFILE_PROPS;
521    /// Forward Clerk's raw `appearance` object.
522    value appearance / maybe_appearance => keys::APPEARANCE;
523});
524
525option_setters!(UserProfileOptions {
526    /// Set embedded routing mode, for example `"path"` or `"hash"`.
527    enum(Routing) routing / maybe_routing => keys::ROUTING;
528    /// Set the path used by embedded routing.
529    string path / maybe_path => keys::PATH;
530    /// Forward Clerk's raw `appearance` object.
531    value appearance / maybe_appearance => keys::APPEARANCE;
532});
533
534option_setters!(CreateOrganizationOptions {
535    /// Set embedded routing mode.
536    enum(Routing) routing / maybe_routing => keys::ROUTING;
537    /// Set the path used by embedded routing.
538    string path / maybe_path => keys::PATH;
539    /// Set where Clerk redirects after creating an organization.
540    string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
541    /// Forward Clerk's raw `appearance` object.
542    value appearance / maybe_appearance => keys::APPEARANCE;
543});
544
545option_setters!(OrganizationProfileOptions {
546    /// Set embedded routing mode.
547    enum(Routing) routing / maybe_routing => keys::ROUTING;
548    /// Set the path used by embedded routing.
549    string path / maybe_path => keys::PATH;
550    /// Forward Clerk's raw `appearance` object.
551    value appearance / maybe_appearance => keys::APPEARANCE;
552});
553
554option_setters!(OrganizationSwitcherOptions {
555    /// Set where Clerk navigates to create an organization.
556    string create_organization_url / maybe_create_organization_url => keys::CREATE_ORGANIZATION_URL;
557    /// Set where Clerk redirects after creating an organization.
558    string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
559    /// Set where Clerk navigates for organization profile management.
560    string organization_profile_url / maybe_organization_profile_url => keys::ORGANIZATION_PROFILE_URL;
561    /// Forward Clerk's raw `appearance` object.
562    value appearance / maybe_appearance => keys::APPEARANCE;
563});
564
565option_setters!(OrganizationListOptions {
566    /// Set where Clerk redirects after creating an organization.
567    string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
568    /// Set where Clerk redirects after selecting an organization.
569    string after_select_organization_url / maybe_after_select_organization_url => keys::AFTER_SELECT_ORGANIZATION_URL;
570    /// Forward Clerk's raw `appearance` object.
571    value appearance / maybe_appearance => keys::APPEARANCE;
572});
573
574option_setters!(WaitlistOptions {
575    /// Set where Clerk redirects after joining the waitlist.
576    string after_join_waitlist_url / maybe_after_join_waitlist_url => keys::AFTER_JOIN_WAITLIST_URL;
577    /// Forward Clerk's raw `appearance` object.
578    value appearance / maybe_appearance => keys::APPEARANCE;
579});
580
581option_setters!(TaskSetupMFAOptions {
582    /// Set the URL Clerk navigates to after all pending session tasks resolve.
583    string redirect_url_complete / maybe_redirect_url_complete => keys::REDIRECT_URL_COMPLETE;
584    /// Forward Clerk's raw `appearance` object.
585    value appearance / maybe_appearance => keys::APPEARANCE;
586});
587
588option_setters!(RedirectOptions {
589    /// Always redirect here after the target auth flow.
590    string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
591    /// Fallback redirect URL after the target auth flow.
592    string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
593    /// Always redirect here after sign-up from sign-in.
594    string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
595    /// Fallback redirect URL after sign-up from sign-in.
596    string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
597    /// Always redirect here after sign-in from sign-up.
598    string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
599    /// Fallback redirect URL after sign-in from sign-up.
600    string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
601});
602
603option_setters!(SignOutOptions {
604    /// Full URL or path to navigate to after sign-out.
605    string redirect_url / maybe_redirect_url => keys::REDIRECT_URL;
606    /// Sign out a specific session id in multi-session applications.
607    string session_id / maybe_session_id => keys::SESSION_ID;
608});
609
610option_setters!(GetTokenOptions {
611    /// Use a named Clerk JWT template.
612    string template / maybe_template => keys::TEMPLATE;
613    /// Request a token scoped to a specific organization without changing the
614    /// active organization in clerk-js.
615    string organization_id / maybe_organization_id => keys::ORGANIZATION_ID;
616    /// Allow Clerk to reuse a cached token for this many extra seconds when supported.
617    u64 leeway_in_seconds / maybe_leeway_in_seconds => keys::LEEWAY_IN_SECONDS;
618    /// Ask Clerk to bypass its token cache when supported.
619    bool skip_cache / maybe_skip_cache => keys::SKIP_CACHE;
620});
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use serde_json::json;
626
627    #[test]
628    fn clerk_options_emit_clerk_keys() {
629        let value = ClerkOptions::new()
630            .sign_in_url("/si")
631            .sign_up_url("/su")
632            .sign_in_fallback_redirect_url("/sifb")
633            .sign_in_force_redirect_url("/sif")
634            .sign_up_fallback_redirect_url("/sufb")
635            .sign_up_force_redirect_url("/suf")
636            .after_sign_out_url("/aso")
637            .after_multi_session_single_sign_out_url("/amsso")
638            .after_switch_session_url("/ass")
639            .waitlist_url("/wl")
640            .user_profile_url("/up")
641            .organization_profile_url("/op")
642            .create_organization_url("/co")
643            .proxy_url("/proxy")
644            .domain("clerk.example.com")
645            .is_satellite(true)
646            .satellite_auto_sync(false)
647            .prefetch_ui(true)
648            .allowed_redirect_origins(["https://a.example"])
649            .allowed_redirect_protocols(["https"])
650            .appearance(json!({"variables": {}}))
651            .localization(json!({"locale": "en-US"}))
652            .into_value();
653
654        assert_eq!(
655            value,
656            json!({
657                "signInUrl": "/si",
658                "signUpUrl": "/su",
659                "signInFallbackRedirectUrl": "/sifb",
660                "signInForceRedirectUrl": "/sif",
661                "signUpFallbackRedirectUrl": "/sufb",
662                "signUpForceRedirectUrl": "/suf",
663                "afterSignOutUrl": "/aso",
664                "afterMultiSessionSingleSignOutUrl": "/amsso",
665                "afterSwitchSessionUrl": "/ass",
666                "waitlistUrl": "/wl",
667                "userProfileUrl": "/up",
668                "organizationProfileUrl": "/op",
669                "createOrganizationUrl": "/co",
670                "proxyUrl": "/proxy",
671                "domain": "clerk.example.com",
672                "isSatellite": true,
673                "satelliteAutoSync": false,
674                "prefetchUI": true,
675                "allowedRedirectOrigins": ["https://a.example"],
676                "allowedRedirectProtocols": ["https"],
677                "appearance": {"variables": {}},
678                "localization": {"locale": "en-US"},
679            })
680        );
681    }
682
683    #[test]
684    fn clerk_options_emit_session_task_urls() {
685        // clerk-js v6 `taskUrls` maps session-task keys to the app URLs Clerk
686        // navigates to when a session has a pending after-auth task.
687        let value = ClerkOptions::new()
688            .task_urls(json!({ "setup-mfa": "/onboarding/mfa" }))
689            .into_value();
690
691        assert_eq!(
692            value,
693            json!({ "taskUrls": { "setup-mfa": "/onboarding/mfa" } })
694        );
695    }
696
697    #[test]
698    fn sign_in_options_emit_clerk_keys() {
699        let value = SignInOptions::new()
700            .routing(Routing::Path)
701            .path("/sign-in")
702            .sign_up_url("/su")
703            .waitlist_url("/wl")
704            .force_redirect_url("/f")
705            .fallback_redirect_url("/fb")
706            .sign_up_force_redirect_url("/suf")
707            .sign_up_fallback_redirect_url("/sufb")
708            .initial_values(json!({"emailAddress": "a@example.com"}))
709            .transferable(true)
710            .appearance(json!({"variables": {}}))
711            .into_value();
712
713        assert_eq!(
714            value,
715            json!({
716                "routing": "path",
717                "path": "/sign-in",
718                "signUpUrl": "/su",
719                "waitlistUrl": "/wl",
720                "forceRedirectUrl": "/f",
721                "fallbackRedirectUrl": "/fb",
722                "signUpForceRedirectUrl": "/suf",
723                "signUpFallbackRedirectUrl": "/sufb",
724                "initialValues": {"emailAddress": "a@example.com"},
725                "transferable": true,
726                "appearance": {"variables": {}},
727            })
728        );
729    }
730
731    #[test]
732    fn sign_up_options_emit_clerk_keys() {
733        let value = SignUpOptions::new()
734            .routing(Routing::Hash)
735            .path("/sign-up")
736            .sign_in_url("/si")
737            .waitlist_url("/wl")
738            .force_redirect_url("/f")
739            .fallback_redirect_url("/fb")
740            .sign_in_force_redirect_url("/sif")
741            .sign_in_fallback_redirect_url("/sifb")
742            .initial_values(json!({"username": "a"}))
743            .appearance(json!({}))
744            .into_value();
745
746        assert_eq!(
747            value,
748            json!({
749                "routing": "hash",
750                "path": "/sign-up",
751                "signInUrl": "/si",
752                "waitlistUrl": "/wl",
753                "forceRedirectUrl": "/f",
754                "fallbackRedirectUrl": "/fb",
755                "signInForceRedirectUrl": "/sif",
756                "signInFallbackRedirectUrl": "/sifb",
757                "initialValues": {"username": "a"},
758                "appearance": {},
759            })
760        );
761    }
762
763    #[test]
764    fn user_button_options_emit_clerk_keys() {
765        let value = UserButtonOptions::new()
766            .after_sign_out_url("/aso")
767            .after_switch_session_url("/ass")
768            .sign_in_url("/si")
769            .show_name(true)
770            .default_open(false)
771            .user_profile_mode(UserProfileMode::Navigation)
772            .user_profile_url("/up")
773            .user_profile_props(json!({"appearance": {}}))
774            .appearance(json!({}))
775            .into_value();
776
777        assert_eq!(
778            value,
779            json!({
780                "afterSignOutUrl": "/aso",
781                "afterSwitchSessionUrl": "/ass",
782                "signInUrl": "/si",
783                "showName": true,
784                "defaultOpen": false,
785                "userProfileMode": "navigation",
786                "userProfileUrl": "/up",
787                "userProfileProps": {"appearance": {}},
788                "appearance": {},
789            })
790        );
791    }
792
793    #[test]
794    fn widget_options_emit_clerk_keys() {
795        assert_eq!(
796            UserProfileOptions::new()
797                .routing(Routing::Path)
798                .path("/profile")
799                .appearance(json!({}))
800                .into_value(),
801            json!({"routing": "path", "path": "/profile", "appearance": {}})
802        );
803        assert_eq!(
804            CreateOrganizationOptions::new()
805                .routing(Routing::Hash)
806                .path("/create-org")
807                .after_create_organization_url("/org")
808                .appearance(json!({}))
809                .into_value(),
810            json!({
811                "routing": "hash",
812                "path": "/create-org",
813                "afterCreateOrganizationUrl": "/org",
814                "appearance": {},
815            })
816        );
817        assert_eq!(
818            OrganizationProfileOptions::new()
819                .routing(Routing::Path)
820                .path("/org-profile")
821                .appearance(json!({}))
822                .into_value(),
823            json!({"routing": "path", "path": "/org-profile", "appearance": {}})
824        );
825        assert_eq!(
826            OrganizationSwitcherOptions::new()
827                .create_organization_url("/co")
828                .after_create_organization_url("/aco")
829                .organization_profile_url("/op")
830                .appearance(json!({}))
831                .into_value(),
832            json!({
833                "createOrganizationUrl": "/co",
834                "afterCreateOrganizationUrl": "/aco",
835                "organizationProfileUrl": "/op",
836                "appearance": {},
837            })
838        );
839        assert_eq!(
840            OrganizationListOptions::new()
841                .after_create_organization_url("/aco")
842                .after_select_organization_url("/aso")
843                .appearance(json!({}))
844                .into_value(),
845            json!({
846                "afterCreateOrganizationUrl": "/aco",
847                "afterSelectOrganizationUrl": "/aso",
848                "appearance": {},
849            })
850        );
851        assert_eq!(
852            WaitlistOptions::new()
853                .after_join_waitlist_url("/ajw")
854                .appearance(json!({}))
855                .into_value(),
856            json!({"afterJoinWaitlistUrl": "/ajw", "appearance": {}})
857        );
858    }
859
860    #[test]
861    fn task_setup_mfa_options_emit_clerk_keys() {
862        assert_eq!(
863            TaskSetupMFAOptions::new()
864                .redirect_url_complete("/onboarding/done")
865                .appearance(json!({}))
866                .into_value(),
867            json!({ "redirectUrlComplete": "/onboarding/done", "appearance": {} })
868        );
869    }
870
871    #[test]
872    fn action_options_emit_clerk_keys() {
873        assert_eq!(
874            RedirectOptions::new()
875                .force_redirect_url("/f")
876                .fallback_redirect_url("/fb")
877                .sign_up_force_redirect_url("/suf")
878                .sign_up_fallback_redirect_url("/sufb")
879                .sign_in_force_redirect_url("/sif")
880                .sign_in_fallback_redirect_url("/sifb")
881                .into_value(),
882            json!({
883                "forceRedirectUrl": "/f",
884                "fallbackRedirectUrl": "/fb",
885                "signUpForceRedirectUrl": "/suf",
886                "signUpFallbackRedirectUrl": "/sufb",
887                "signInForceRedirectUrl": "/sif",
888                "signInFallbackRedirectUrl": "/sifb",
889            })
890        );
891        assert_eq!(
892            SignOutOptions::new()
893                .redirect_url("/after")
894                .session_id("sess_1")
895                .into_value(),
896            json!({"redirectUrl": "/after", "sessionId": "sess_1"})
897        );
898        assert_eq!(
899            GetTokenOptions::new()
900                .template("supabase")
901                .organization_id("org_1")
902                .leeway_in_seconds(10)
903                .skip_cache(true)
904                .into_value(),
905            json!({
906                "template": "supabase",
907                "organizationId": "org_1",
908                "leewayInSeconds": 10,
909                "skipCache": true,
910            })
911        );
912    }
913
914    #[test]
915    fn maybe_setters_with_none_leave_options_untouched() {
916        let value = SignInOptions::new()
917            .maybe_routing(None)
918            .maybe_path(None)
919            .maybe_sign_up_url(None)
920            .maybe_initial_values(None)
921            .maybe_transferable(None)
922            .into_value();
923
924        assert_eq!(value, json!({}));
925
926        let untouched = SignInOptions::from_value(json!({"signUpUrl": "/kept"}))
927            .maybe_sign_up_url(None)
928            .into_value();
929        assert_eq!(untouched, json!({"signUpUrl": "/kept"}));
930    }
931
932    #[test]
933    fn builders_accept_a_raw_value_via_from() {
934        // The widget `options` props rely on `From<Value>` (through
935        // `#[props(into)]`) to keep accepting a raw JSON escape hatch. It must
936        // match `from_value` exactly.
937        let value = json!({ "signUpUrl": "/kept", "path": "/p" });
938        assert_eq!(
939            SignInOptions::from(value.clone()).into_value(),
940            SignInOptions::from_value(value).into_value(),
941        );
942    }
943
944    #[test]
945    fn explicit_props_win_over_raw_options() {
946        let value = SignInOptions::from_value(json!({"signUpUrl": "/old", "path": "/kept"}))
947            .sign_up_url("/new")
948            .into_value();
949
950        assert_eq!(value, json!({"signUpUrl": "/new", "path": "/kept"}));
951    }
952
953    #[test]
954    fn raw_null_options_stay_null_when_no_setter_runs() {
955        assert_eq!(
956            SignInOptions::from_value(serde_json::Value::Null)
957                .maybe_sign_up_url(None)
958                .into_value(),
959            serde_json::Value::Null
960        );
961        assert_eq!(
962            SignInOptions::from_value(serde_json::Value::Null)
963                .sign_up_url("/su")
964                .into_value(),
965            json!({"signUpUrl": "/su"})
966        );
967    }
968}