1use serde_json::{Map, Value};
29
30pub(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#[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 pub fn new() -> Self {
97 Self {
98 value: Value::Object(Map::new()),
99 }
100 }
101
102 pub fn from_value(value: Value) -> Self {
104 Self { value }
105 }
106
107 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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
146pub enum Routing {
147 Hash,
149 Path,
151}
152
153impl Routing {
154 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub enum UserProfileMode {
175 Modal,
177 Navigation,
179}
180
181impl UserProfileMode {
182 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 pub fn new() -> Self {
209 Self {
210 inner: JsonOptions::new(),
211 }
212 }
213
214 pub fn from_value(value: Value) -> Self {
216 Self {
217 inner: JsonOptions::from_value(value),
218 }
219 }
220
221 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 #[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 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
294macro_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 string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
409 string sign_up_url / maybe_sign_up_url => keys::SIGN_UP_URL;
411 string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
413 string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
415 string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
417 string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
419 string after_sign_out_url / maybe_after_sign_out_url => keys::AFTER_SIGN_OUT_URL;
421 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 string after_switch_session_url / maybe_after_switch_session_url => keys::AFTER_SWITCH_SESSION_URL;
425 string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
427 string user_profile_url / maybe_user_profile_url => keys::USER_PROFILE_URL;
429 string organization_profile_url / maybe_organization_profile_url => keys::ORGANIZATION_PROFILE_URL;
431 string create_organization_url / maybe_create_organization_url => keys::CREATE_ORGANIZATION_URL;
433 string proxy_url / maybe_proxy_url => keys::PROXY_URL;
435 string domain / maybe_domain => keys::DOMAIN;
437 bool is_satellite / maybe_is_satellite => keys::IS_SATELLITE;
439 bool satellite_auto_sync / maybe_satellite_auto_sync => keys::SATELLITE_AUTO_SYNC;
441 bool prefetch_ui / maybe_prefetch_ui => keys::PREFETCH_UI;
443 list allowed_redirect_origins / maybe_allowed_redirect_origins => keys::ALLOWED_REDIRECT_ORIGINS;
445 list allowed_redirect_protocols / maybe_allowed_redirect_protocols => keys::ALLOWED_REDIRECT_PROTOCOLS;
447 value appearance / maybe_appearance => keys::APPEARANCE;
449 value localization / maybe_localization => keys::LOCALIZATION;
451 value task_urls / maybe_task_urls => keys::TASK_URLS;
454});
455
456option_setters!(SignInOptions {
457 enum(Routing) routing / maybe_routing => keys::ROUTING;
459 string path / maybe_path => keys::PATH;
461 string sign_up_url / maybe_sign_up_url => keys::SIGN_UP_URL;
463 string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
465 string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
467 string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
469 string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
471 string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
473 value initial_values / maybe_initial_values => keys::INITIAL_VALUES;
475 bool transferable / maybe_transferable => keys::TRANSFERABLE;
477 value appearance / maybe_appearance => keys::APPEARANCE;
479});
480
481option_setters!(SignUpOptions {
482 enum(Routing) routing / maybe_routing => keys::ROUTING;
484 string path / maybe_path => keys::PATH;
486 string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
488 string waitlist_url / maybe_waitlist_url => keys::WAITLIST_URL;
490 string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
492 string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
494 string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
496 string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
498 value initial_values / maybe_initial_values => keys::INITIAL_VALUES;
500 value appearance / maybe_appearance => keys::APPEARANCE;
502});
503
504option_setters!(UserButtonOptions {
505 string after_sign_out_url / maybe_after_sign_out_url => keys::AFTER_SIGN_OUT_URL;
507 string after_switch_session_url / maybe_after_switch_session_url => keys::AFTER_SWITCH_SESSION_URL;
509 string sign_in_url / maybe_sign_in_url => keys::SIGN_IN_URL;
511 bool show_name / maybe_show_name => keys::SHOW_NAME;
513 bool default_open / maybe_default_open => keys::DEFAULT_OPEN;
515 enum(UserProfileMode) user_profile_mode / maybe_user_profile_mode => keys::USER_PROFILE_MODE;
517 string user_profile_url / maybe_user_profile_url => keys::USER_PROFILE_URL;
519 value user_profile_props / maybe_user_profile_props => keys::USER_PROFILE_PROPS;
521 value appearance / maybe_appearance => keys::APPEARANCE;
523});
524
525option_setters!(UserProfileOptions {
526 enum(Routing) routing / maybe_routing => keys::ROUTING;
528 string path / maybe_path => keys::PATH;
530 value appearance / maybe_appearance => keys::APPEARANCE;
532});
533
534option_setters!(CreateOrganizationOptions {
535 enum(Routing) routing / maybe_routing => keys::ROUTING;
537 string path / maybe_path => keys::PATH;
539 string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
541 value appearance / maybe_appearance => keys::APPEARANCE;
543});
544
545option_setters!(OrganizationProfileOptions {
546 enum(Routing) routing / maybe_routing => keys::ROUTING;
548 string path / maybe_path => keys::PATH;
550 value appearance / maybe_appearance => keys::APPEARANCE;
552});
553
554option_setters!(OrganizationSwitcherOptions {
555 string create_organization_url / maybe_create_organization_url => keys::CREATE_ORGANIZATION_URL;
557 string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
559 string organization_profile_url / maybe_organization_profile_url => keys::ORGANIZATION_PROFILE_URL;
561 value appearance / maybe_appearance => keys::APPEARANCE;
563});
564
565option_setters!(OrganizationListOptions {
566 string after_create_organization_url / maybe_after_create_organization_url => keys::AFTER_CREATE_ORGANIZATION_URL;
568 string after_select_organization_url / maybe_after_select_organization_url => keys::AFTER_SELECT_ORGANIZATION_URL;
570 value appearance / maybe_appearance => keys::APPEARANCE;
572});
573
574option_setters!(WaitlistOptions {
575 string after_join_waitlist_url / maybe_after_join_waitlist_url => keys::AFTER_JOIN_WAITLIST_URL;
577 value appearance / maybe_appearance => keys::APPEARANCE;
579});
580
581option_setters!(TaskSetupMFAOptions {
582 string redirect_url_complete / maybe_redirect_url_complete => keys::REDIRECT_URL_COMPLETE;
584 value appearance / maybe_appearance => keys::APPEARANCE;
586});
587
588option_setters!(RedirectOptions {
589 string force_redirect_url / maybe_force_redirect_url => keys::FORCE_REDIRECT_URL;
591 string fallback_redirect_url / maybe_fallback_redirect_url => keys::FALLBACK_REDIRECT_URL;
593 string sign_up_force_redirect_url / maybe_sign_up_force_redirect_url => keys::SIGN_UP_FORCE_REDIRECT_URL;
595 string sign_up_fallback_redirect_url / maybe_sign_up_fallback_redirect_url => keys::SIGN_UP_FALLBACK_REDIRECT_URL;
597 string sign_in_force_redirect_url / maybe_sign_in_force_redirect_url => keys::SIGN_IN_FORCE_REDIRECT_URL;
599 string sign_in_fallback_redirect_url / maybe_sign_in_fallback_redirect_url => keys::SIGN_IN_FALLBACK_REDIRECT_URL;
601});
602
603option_setters!(SignOutOptions {
604 string redirect_url / maybe_redirect_url => keys::REDIRECT_URL;
606 string session_id / maybe_session_id => keys::SESSION_ID;
608});
609
610option_setters!(GetTokenOptions {
611 string template / maybe_template => keys::TEMPLATE;
613 string organization_id / maybe_organization_id => keys::ORGANIZATION_ID;
616 u64 leeway_in_seconds / maybe_leeway_in_seconds => keys::LEEWAY_IN_SECONDS;
618 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 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 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}