Skip to main content

vortex_sdk/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// ============================================================================
5// Enums for type-safe API values
6// ============================================================================
7
8/// Target type for invitation responses (who was invited)
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum InvitationTargetType {
12    Email,
13    Phone,
14    Share,
15    Internal,
16}
17
18/// Target type for creating invitations
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum CreateInvitationTargetType {
22    Email,
23    Phone,
24    Internal,
25}
26
27/// Type of invitation
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum InvitationType {
31    SingleUse,
32    MultiUse,
33    Autojoin,
34}
35
36/// Current status of an invitation
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum InvitationStatus {
40    Queued,
41    Sending,
42    Sent,
43    Delivered,
44    Accepted,
45    Shared,
46    Unfurled,
47    AcceptedElsewhere,
48}
49
50/// Delivery type for invitations
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum DeliveryType {
54    Email,
55    Phone,
56    Share,
57    Internal,
58}
59
60// ============================================================================
61// Core types
62// ============================================================================
63
64/// User type for JWT generation
65/// Optional fields: name (max 200 chars), avatar_url (HTTPS URL, max 2000 chars), admin_scopes, allowed_email_domains
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct User {
69    pub id: String,
70    pub email: String,
71    /// User's display name (preferred)
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub name: Option<String>,
74    /// User's avatar URL (preferred)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub avatar_url: Option<String>,
77    /// Deprecated: use name instead
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub user_name: Option<String>,
80    /// Deprecated: use avatar_url instead
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub user_avatar_url: Option<String>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub admin_scopes: Option<Vec<String>>,
85    /// Optional list of allowed email domains for invitation restrictions (e.g., ["acme.com", "acme.org"])
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub allowed_email_domains: Option<Vec<String>>,
88    /// Additional fields included in signing
89    #[serde(flatten)]
90    pub extra: Option<HashMap<String, serde_json::Value>>,
91}
92
93impl User {
94    pub fn new(id: &str, email: &str) -> Self {
95        Self {
96            id: id.to_string(),
97            email: email.to_string(),
98            name: None,
99            avatar_url: None,
100            user_name: None,
101            user_avatar_url: None,
102            admin_scopes: None,
103            allowed_email_domains: None,
104            extra: None,
105        }
106    }
107
108    pub fn with_name(mut self, name: &str) -> Self {
109        self.name = Some(name.to_string());
110        self
111    }
112
113    pub fn with_avatar_url(mut self, avatar_url: &str) -> Self {
114        self.avatar_url = Some(avatar_url.to_string());
115        self
116    }
117
118    /// Deprecated: use with_name instead
119    #[deprecated(note = "Use with_name instead")]
120    pub fn with_user_name(mut self, name: &str) -> Self {
121        self.user_name = Some(name.to_string());
122        self
123    }
124
125    /// Deprecated: use with_avatar_url instead
126    #[deprecated(note = "Use with_avatar_url instead")]
127    pub fn with_user_avatar_url(mut self, avatar_url: &str) -> Self {
128        self.user_avatar_url = Some(avatar_url.to_string());
129        self
130    }
131
132    pub fn with_admin_scopes(mut self, scopes: Vec<String>) -> Self {
133        self.admin_scopes = Some(scopes);
134        self
135    }
136
137    pub fn with_allowed_email_domains(mut self, domains: Vec<String>) -> Self {
138        self.allowed_email_domains = Some(domains);
139        self
140    }
141}
142
143/// Identifier for a user (email, sms, etc.)
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct Identifier {
146    #[serde(rename = "type")]
147    pub identifier_type: String,
148    pub value: String,
149}
150
151impl Identifier {
152    pub fn new(identifier_type: &str, value: &str) -> Self {
153        Self {
154            identifier_type: identifier_type.to_string(),
155            value: value.to_string(),
156        }
157    }
158}
159
160/// Group information for JWT generation (input)
161/// Supports both 'id' (legacy) and 'groupId' (preferred) for backward compatibility
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct Group {
165    #[serde(rename = "type")]
166    pub scope_type: String,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub id: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub scope: Option<String>,
171    #[serde(rename = "groupId")]
172    pub name: String,
173}
174
175impl Group {
176    pub fn new(scope_type: &str, name: &str) -> Self {
177        Self {
178            scope_type: scope_type.to_string(),
179            id: None,
180            scope: None,
181            name: name.to_string(),
182        }
183    }
184
185    pub fn with_id(mut self, id: &str) -> Self {
186        self.id = Some(id.to_string());
187        self
188    }
189
190    pub fn with_scope(mut self, scope: &str) -> Self {
191        self.scope = Some(scope.to_string());
192        self
193    }
194}
195
196/// Invitation group from API responses
197/// This matches the MemberGroups table structure from the API
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct InvitationScope {
201    /// Vortex internal UUID
202    pub id: String,
203    /// Vortex account ID
204    pub account_id: String,
205    /// Customer's group ID (the ID they provided to Vortex)
206    #[serde(rename = "groupId")]
207    pub scope: String,
208    /// Preferred alias for scope (= groupId value). Populated by SDK.
209    #[serde(skip)]
210    pub scope_id: String,
211    /// Group type (e.g., "workspace", "team")
212    #[serde(rename = "type")]
213    pub scope_type: String,
214    /// Group name
215    pub name: String,
216    /// ISO 8601 timestamp when the group was created
217    pub created_at: String,
218}
219
220/// Invitation target from API responses
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct InvitationTarget {
223    #[serde(rename = "type")]
224    pub target_type: InvitationTargetType,
225    pub value: String,
226    /// Display name of the person being invited
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub name: Option<String>,
229    /// Avatar URL for the person being invited (for display in invitation lists)
230    #[serde(skip_serializing_if = "Option::is_none")]
231    #[serde(rename = "avatarUrl")]
232    pub avatar_url: Option<String>,
233}
234
235impl InvitationTarget {
236    pub fn new(target_type: InvitationTargetType, value: &str) -> Self {
237        Self {
238            target_type,
239            value: value.to_string(),
240            name: None,
241            avatar_url: None,
242        }
243    }
244
245    pub fn email(value: &str) -> Self {
246        Self::new(InvitationTargetType::Email, value)
247    }
248
249    pub fn phone(value: &str) -> Self {
250        Self::new(InvitationTargetType::Phone, value)
251    }
252
253    pub fn with_name(mut self, name: &str) -> Self {
254        self.name = Some(name.to_string());
255        self
256    }
257
258    pub fn with_avatar_url(mut self, avatar_url: &str) -> Self {
259        self.avatar_url = Some(avatar_url.to_string());
260        self
261    }
262}
263
264/// User data for accepting invitations (preferred format)
265///
266/// At least one of email or phone must be provided.
267///
268/// # Example
269///
270/// ```
271/// use vortex_sdk::AcceptUser;
272///
273/// // With email only
274/// let user = AcceptUser::new().with_email("user@example.com");
275///
276/// // With email and name
277/// let user = AcceptUser::new()
278///     .with_email("user@example.com")
279///     .with_name("John Doe");
280///
281/// // With all fields
282/// let user = AcceptUser::new()
283///     .with_email("user@example.com")
284///     .with_phone("+1234567890")
285///     .with_name("John Doe")
286///     .with_is_existing(false);
287/// ```
288#[derive(Debug, Clone, Serialize, Deserialize, Default)]
289pub struct AcceptUser {
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub email: Option<String>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub phone: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub name: Option<String>,
296    /// Whether the accepting user is an existing user in your system.
297    /// Set to true if the user was already registered before accepting the invitation.
298    /// Set to false if this is a new user signup.
299    /// Leave as None if unknown.
300    /// Used for analytics to track new vs existing user conversions.
301    #[serde(rename = "isExisting", skip_serializing_if = "Option::is_none")]
302    pub is_existing: Option<bool>,
303}
304
305impl AcceptUser {
306    pub fn new() -> Self {
307        Self::default()
308    }
309
310    pub fn with_email(mut self, email: &str) -> Self {
311        self.email = Some(email.to_string());
312        self
313    }
314
315    pub fn with_phone(mut self, phone: &str) -> Self {
316        self.phone = Some(phone.to_string());
317        self
318    }
319
320    pub fn with_name(mut self, name: &str) -> Self {
321        self.name = Some(name.to_string());
322        self
323    }
324
325    pub fn with_is_existing(mut self, is_existing: bool) -> Self {
326        self.is_existing = Some(is_existing);
327        self
328    }
329}
330
331/// Invitation acceptance information
332#[derive(Debug, Clone, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase")]
334pub struct InvitationAcceptance {
335    pub id: Option<String>,
336    pub account_id: Option<String>,
337    pub accepted_at: Option<String>,
338    pub target: Option<InvitationTarget>,
339}
340
341/// Full invitation details
342#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct Invitation {
345    #[serde(default)]
346    pub id: String,
347    #[serde(default)]
348    pub account_id: String,
349    #[serde(default)]
350    pub click_throughs: u32,
351    /// Invitation form data submitted by the user, including email addresses of invitees and the values of any custom fields.
352    pub form_submission_data: Option<HashMap<String, serde_json::Value>>,
353    /// Deprecated: Use `form_submission_data` instead. Contains the same data.
354    #[deprecated(note = "Use form_submission_data instead")]
355    pub configuration_attributes: Option<HashMap<String, serde_json::Value>>,
356    pub attributes: Option<HashMap<String, serde_json::Value>>,
357    #[serde(default)]
358    pub created_at: String,
359    #[serde(default)]
360    pub deactivated: bool,
361    #[serde(default)]
362    pub delivery_count: u32,
363    #[serde(default)]
364    pub delivery_types: Vec<DeliveryType>,
365    #[serde(default)]
366    pub foreign_creator_id: String,
367    pub invitation_type: InvitationType,
368    pub modified_at: Option<String>,
369    pub status: InvitationStatus,
370    #[serde(default)]
371    pub target: Vec<InvitationTarget>,
372    #[serde(default)]
373    pub views: u32,
374    #[serde(default)]
375    pub widget_configuration_id: String,
376    #[serde(default)]
377    pub groups: Vec<InvitationScope>,
378    /// Preferred alias for groups. Each element also has scope_id. Populated by SDK.
379    #[serde(skip)]
380    pub scopes: Vec<InvitationScope>,
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub accepts: Option<Vec<InvitationAcceptance>>,
383    pub expired: bool,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub expires: Option<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub source: Option<String>,
388    /// Customer-defined subtype for analytics segmentation (e.g., "pymk", "find-friends")
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub subtype: Option<String>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub creator_name: Option<String>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub creator_avatar_url: Option<String>,
395}
396
397/// Response containing multiple invitations
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct InvitationsResponse {
400    pub invitations: Option<Vec<Invitation>>,
401}
402
403/// Accept invitation parameter - supports both new User format and legacy Target format
404#[derive(Debug, Clone)]
405pub enum AcceptInvitationParam {
406    /// New User format (preferred)
407    User(AcceptUser),
408    /// Legacy target format (deprecated)
409    Target(InvitationTarget),
410    /// Legacy multiple targets format (deprecated)
411    Targets(Vec<InvitationTarget>),
412}
413
414impl From<AcceptUser> for AcceptInvitationParam {
415    fn from(user: AcceptUser) -> Self {
416        AcceptInvitationParam::User(user)
417    }
418}
419
420impl From<InvitationTarget> for AcceptInvitationParam {
421    fn from(target: InvitationTarget) -> Self {
422        AcceptInvitationParam::Target(target)
423    }
424}
425
426impl From<Vec<InvitationTarget>> for AcceptInvitationParam {
427    fn from(targets: Vec<InvitationTarget>) -> Self {
428        AcceptInvitationParam::Targets(targets)
429    }
430}
431
432// --- Types for creating invitations via backend API ---
433
434/// Target for creating an invitation
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct CreateInvitationTarget {
437    #[serde(rename = "type")]
438    pub target_type: CreateInvitationTargetType,
439    /// Target value: email address, phone number, or internal user ID
440    pub value: String,
441    /// Display name of the person being invited
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub name: Option<String>,
444    /// Avatar URL for the person being invited (for display in invitation lists)
445    #[serde(skip_serializing_if = "Option::is_none")]
446    #[serde(rename = "avatarUrl")]
447    pub avatar_url: Option<String>,
448}
449
450impl CreateInvitationTarget {
451    pub fn new(target_type: CreateInvitationTargetType, value: &str) -> Self {
452        Self {
453            target_type,
454            value: value.to_string(),
455            name: None,
456            avatar_url: None,
457        }
458    }
459
460    pub fn email(value: &str) -> Self {
461        Self::new(CreateInvitationTargetType::Email, value)
462    }
463
464    pub fn phone(value: &str) -> Self {
465        Self::new(CreateInvitationTargetType::Phone, value)
466    }
467
468    /// Alias for phone (backward compatibility)
469    pub fn sms(value: &str) -> Self {
470        Self::phone(value)
471    }
472
473    pub fn internal(value: &str) -> Self {
474        Self::new(CreateInvitationTargetType::Internal, value)
475    }
476
477    pub fn with_name(mut self, name: &str) -> Self {
478        self.name = Some(name.to_string());
479        self
480    }
481
482    pub fn with_avatar_url(mut self, avatar_url: &str) -> Self {
483        self.avatar_url = Some(avatar_url.to_string());
484        self
485    }
486}
487
488/// Information about the user creating the invitation (the inviter)
489#[derive(Debug, Clone, Serialize, Deserialize)]
490#[serde(rename_all = "camelCase")]
491pub struct Inviter {
492    /// Required: Your internal user ID for the inviter
493    pub user_id: String,
494    /// Optional: Email of the inviter
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub user_email: Option<String>,
497    /// Optional: Display name of the inviter (preferred)
498    #[serde(skip_serializing_if = "Option::is_none")]
499    pub name: Option<String>,
500    /// Optional: Avatar URL of the inviter (preferred)
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub avatar_url: Option<String>,
503    /// Deprecated: use name instead
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub user_name: Option<String>,
506    /// Deprecated: use avatar_url instead
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub user_avatar_url: Option<String>,
509}
510
511impl Inviter {
512    pub fn new(user_id: &str) -> Self {
513        Self {
514            user_id: user_id.to_string(),
515            user_email: None,
516            name: None,
517            avatar_url: None,
518            user_name: None,
519            user_avatar_url: None,
520        }
521    }
522
523    pub fn with_email(mut self, email: &str) -> Self {
524        self.user_email = Some(email.to_string());
525        self
526    }
527
528    pub fn with_name(mut self, name: &str) -> Self {
529        self.name = Some(name.to_string());
530        self
531    }
532
533    pub fn with_avatar_url(mut self, url: &str) -> Self {
534        self.avatar_url = Some(url.to_string());
535        self
536    }
537
538    /// Deprecated: use with_name instead
539    #[deprecated(note = "Use with_name instead")]
540    pub fn with_user_name(mut self, name: &str) -> Self {
541        self.user_name = Some(name.to_string());
542        self
543    }
544
545    /// Deprecated: use with_avatar_url instead
546    #[deprecated(note = "Use with_avatar_url instead")]
547    pub fn with_user_avatar_url(mut self, url: &str) -> Self {
548        self.user_avatar_url = Some(url.to_string());
549        self
550    }
551}
552
553/// Group information for creating invitations
554#[derive(Debug, Clone, Serialize, Deserialize)]
555#[serde(rename_all = "camelCase")]
556pub struct CreateInvitationScope {
557    /// Group type (e.g., "team", "organization")
558    #[serde(rename = "type")]
559    pub scope_type: String,
560    /// Your internal group ID
561    #[serde(rename = "groupId")]
562    pub scope: String,
563    /// Display name of the group
564    pub name: String,
565}
566
567impl CreateInvitationScope {
568    pub fn new(scope_type: &str, scope: &str, name: &str) -> Self {
569        Self {
570            scope_type: scope_type.to_string(),
571            scope: scope.to_string(),
572            name: name.to_string(),
573        }
574    }
575}
576
577/// Valid Open Graph types for unfurl configuration
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579#[serde(rename_all = "snake_case")]
580pub enum UnfurlOgType {
581    Website,
582    Article,
583    Video,
584    Music,
585    Book,
586    Profile,
587    Product,
588}
589
590/// Configuration for link unfurl (Open Graph) metadata
591/// Controls how the invitation link appears when shared on social platforms or messaging apps
592#[derive(Debug, Clone, Serialize, Deserialize, Default)]
593#[serde(rename_all = "camelCase")]
594pub struct UnfurlConfig {
595    /// The title shown in link previews (og:title)
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub title: Option<String>,
598    /// The description shown in link previews (og:description)
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub description: Option<String>,
601    /// The image URL shown in link previews (og:image) - must be HTTPS
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub image: Option<String>,
604    /// The Open Graph type (og:type)
605    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
606    pub og_type: Option<UnfurlOgType>,
607    /// The site name shown in link previews (og:site_name)
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub site_name: Option<String>,
610}
611
612impl UnfurlConfig {
613    pub fn new() -> Self {
614        Self::default()
615    }
616
617    pub fn with_title(mut self, title: &str) -> Self {
618        self.title = Some(title.to_string());
619        self
620    }
621
622    pub fn with_description(mut self, description: &str) -> Self {
623        self.description = Some(description.to_string());
624        self
625    }
626
627    pub fn with_image(mut self, image: &str) -> Self {
628        self.image = Some(image.to_string());
629        self
630    }
631
632    pub fn with_type(mut self, og_type: UnfurlOgType) -> Self {
633        self.og_type = Some(og_type);
634        self
635    }
636
637    pub fn with_site_name(mut self, site_name: &str) -> Self {
638        self.site_name = Some(site_name.to_string());
639        self
640    }
641}
642
643/// Request body for creating an invitation
644#[derive(Debug, Clone, Serialize, Deserialize)]
645#[serde(rename_all = "camelCase")]
646pub struct CreateInvitationRequest {
647    pub widget_configuration_id: String,
648    pub target: CreateInvitationTarget,
649    pub inviter: Inviter,
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub groups: Option<Vec<CreateInvitationScope>>,
652    /// Preferred: flat scope ID for single scope (takes priority over groups/scopes)
653    #[serde(skip)]
654    pub scope_id: Option<String>,
655    /// Scope type when using flat scope_id param
656    #[serde(skip)]
657    pub scope_type_flat: Option<String>,
658    /// Scope name when using flat scope_id param
659    #[serde(skip)]
660    pub scope_name: Option<String>,
661    /// Deprecated: use scope_id/scope_type_flat/scope_name or groups
662    #[serde(skip)]
663    pub scopes: Option<Vec<CreateInvitationScope>>,
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub source: Option<String>,
666    /// Customer-defined subtype for analytics segmentation (e.g., "pymk", "find-friends")
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub subtype: Option<String>,
669    #[serde(skip_serializing_if = "Option::is_none")]
670    pub template_variables: Option<HashMap<String, String>>,
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub metadata: Option<HashMap<String, serde_json::Value>>,
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub unfurl_config: Option<UnfurlConfig>,
675}
676
677impl CreateInvitationRequest {
678    pub fn new(
679        widget_configuration_id: &str,
680        target: CreateInvitationTarget,
681        inviter: Inviter,
682    ) -> Self {
683        Self {
684            widget_configuration_id: widget_configuration_id.to_string(),
685            target,
686            inviter,
687            groups: None,
688            scope_id: None,
689            scope_type_flat: None,
690            scope_name: None,
691            scopes: None,
692            source: None,
693            subtype: None,
694            template_variables: None,
695            metadata: None,
696            unfurl_config: None,
697        }
698    }
699
700    pub fn with_groups(mut self, groups: Vec<CreateInvitationScope>) -> Self {
701        self.groups = Some(groups);
702        self
703    }
704
705    /// Set a single scope using flat params (preferred over with_groups)
706    pub fn with_scope(mut self, scope_id: &str, scope_type: &str, scope_name: &str) -> Self {
707        self.scope_id = Some(scope_id.to_string());
708        self.scope_type_flat = Some(scope_type.to_string());
709        self.scope_name = Some(scope_name.to_string());
710        self
711    }
712
713    pub fn with_source(mut self, source: &str) -> Self {
714        self.source = Some(source.to_string());
715        self
716    }
717
718    pub fn with_subtype(mut self, subtype: &str) -> Self {
719        self.subtype = Some(subtype.to_string());
720        self
721    }
722
723    pub fn with_template_variables(mut self, vars: HashMap<String, String>) -> Self {
724        self.template_variables = Some(vars);
725        self
726    }
727
728    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
729        self.metadata = Some(metadata);
730        self
731    }
732
733    pub fn with_unfurl_config(mut self, unfurl_config: UnfurlConfig) -> Self {
734        self.unfurl_config = Some(unfurl_config);
735        self
736    }
737}
738
739/// Response from creating an invitation
740#[derive(Debug, Clone, Serialize, Deserialize)]
741#[serde(rename_all = "camelCase")]
742pub struct CreateInvitationResponse {
743    /// The ID of the created invitation
744    pub id: String,
745    /// The short link for the invitation
746    pub short_link: String,
747    /// The status of the invitation
748    pub status: String,
749    /// When the invitation was created
750    pub created_at: String,
751}
752
753// --- Types for syncing internal invitation actions ---
754
755/// Request body for syncing an internal invitation action
756#[derive(Debug, Clone, Serialize, Deserialize)]
757#[serde(rename_all = "camelCase")]
758pub struct SyncInternalInvitationRequest {
759    /// The inviter's user ID
760    pub creator_id: String,
761    /// The invitee's user ID
762    pub target_value: String,
763    /// The action taken: "accepted" or "declined"
764    pub action: String,
765    /// The widget component UUID
766    pub component_id: String,
767}
768
769impl SyncInternalInvitationRequest {
770    pub fn new(creator_id: &str, target_value: &str, action: &str, component_id: &str) -> Self {
771        Self {
772            creator_id: creator_id.to_string(),
773            target_value: target_value.to_string(),
774            action: action.to_string(),
775            component_id: component_id.to_string(),
776        }
777    }
778}
779
780/// Response from syncing an internal invitation action
781#[derive(Debug, Clone, Serialize, Deserialize)]
782#[serde(rename_all = "camelCase")]
783pub struct SyncInternalInvitationResponse {
784    /// Number of invitations processed
785    pub processed: u32,
786    /// IDs of the invitations that were processed
787    pub invitation_ids: Vec<String>,
788}
789
790// --- Types for autojoin domain management ---
791
792/// Represents an autojoin domain configuration
793#[derive(Debug, Clone, Serialize, Deserialize)]
794pub struct AutojoinDomain {
795    pub id: String,
796    pub domain: String,
797}
798
799/// Response from autojoin API endpoints
800#[derive(Debug, Clone, Serialize, Deserialize)]
801#[serde(rename_all = "camelCase")]
802pub struct AutojoinDomainsResponse {
803    pub autojoin_domains: Vec<AutojoinDomain>,
804    pub invitation: Option<Invitation>,
805}
806
807/// Request body for configuring autojoin domains
808#[derive(Debug, Clone, Serialize, Deserialize)]
809#[serde(rename_all = "camelCase")]
810pub struct ConfigureAutojoinRequest {
811    pub scope: String,
812    pub scope_type: String,
813    #[serde(skip_serializing_if = "Option::is_none")]
814    pub scope_name: Option<String>,
815    pub domains: Vec<String>,
816    pub component_id: String,
817    #[serde(skip_serializing_if = "Option::is_none")]
818    pub metadata: Option<HashMap<String, serde_json::Value>>,
819}
820
821impl ConfigureAutojoinRequest {
822    pub fn new(scope: &str, scope_type: &str, domains: Vec<String>, component_id: &str) -> Self {
823        Self {
824            scope: scope.to_string(),
825            scope_type: scope_type.to_string(),
826            scope_name: None,
827            domains,
828            component_id: component_id.to_string(),
829            metadata: None,
830        }
831    }
832
833    pub fn with_scope_name(mut self, scope_name: &str) -> Self {
834        self.scope_name = Some(scope_name.to_string());
835        self
836    }
837
838    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
839        self.metadata = Some(metadata);
840        self
841    }
842}
843
844// Deprecated type aliases for backward compatibility
845#[deprecated(since = "0.1.0", note = "Use InvitationScope instead")]
846pub type InvitationGroup = InvitationScope;
847
848#[deprecated(since = "0.1.0", note = "Use CreateInvitationScope instead")]
849pub type CreateInvitationGroup = CreateInvitationScope;
850
851// ─── generate_token types ────────────────────────────────────
852
853/// Helper function to check if a string is empty (for serde skip_serializing_if)
854fn is_empty_string(s: &String) -> bool {
855    s.is_empty()
856}
857
858/// User data for generate_token
859#[derive(Debug, Clone, Serialize, Deserialize, Default)]
860#[serde(rename_all = "camelCase")]
861pub struct TokenUser {
862    #[serde(skip_serializing_if = "is_empty_string")]
863    pub id: String,
864    #[serde(skip_serializing_if = "Option::is_none")]
865    pub name: Option<String>,
866    #[serde(skip_serializing_if = "Option::is_none")]
867    pub email: Option<String>,
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub avatar_url: Option<String>,
870    #[serde(skip_serializing_if = "Option::is_none")]
871    pub admin_scopes: Option<Vec<String>>,
872    #[serde(skip_serializing_if = "Option::is_none")]
873    pub allowed_email_domains: Option<Vec<String>>,
874    #[serde(flatten)]
875    pub extra: Option<HashMap<String, serde_json::Value>>,
876}
877
878impl TokenUser {
879    pub fn new(id: &str) -> Self {
880        Self {
881            id: id.to_string(),
882            ..Default::default()
883        }
884    }
885
886    pub fn with_name(mut self, name: &str) -> Self {
887        self.name = Some(name.to_string());
888        self
889    }
890
891    pub fn with_email(mut self, email: &str) -> Self {
892        self.email = Some(email.to_string());
893        self
894    }
895}
896
897/// Payload for generate_token
898#[derive(Debug, Clone, Serialize, Deserialize, Default)]
899#[serde(rename_all = "camelCase")]
900pub struct GenerateTokenPayload {
901    #[serde(skip_serializing_if = "Option::is_none")]
902    pub user: Option<TokenUser>,
903    #[serde(skip_serializing_if = "Option::is_none")]
904    pub component: Option<String>,
905    #[serde(skip_serializing_if = "Option::is_none")]
906    pub scope: Option<String>,
907    #[serde(skip_serializing_if = "Option::is_none")]
908    pub vars: Option<HashMap<String, serde_json::Value>>,
909    #[serde(flatten)]
910    pub extra: Option<HashMap<String, serde_json::Value>>,
911}
912
913impl GenerateTokenPayload {
914    pub fn new() -> Self {
915        Self::default()
916    }
917
918    pub fn with_user(mut self, user: TokenUser) -> Self {
919        self.user = Some(user);
920        self
921    }
922
923    pub fn with_component(mut self, component: &str) -> Self {
924        self.component = Some(component.to_string());
925        self
926    }
927
928    pub fn with_scope(mut self, scope: &str) -> Self {
929        self.scope = Some(scope.to_string());
930        self
931    }
932}
933
934/// Expiration time for generate_token
935#[derive(Debug, Clone)]
936pub enum ExpiresIn {
937    /// Duration string like "5m", "1h", "24h", "7d"
938    Duration(String),
939    /// Seconds as integer
940    Seconds(u64),
941}
942
943impl From<&str> for ExpiresIn {
944    fn from(s: &str) -> Self {
945        ExpiresIn::Duration(s.to_string())
946    }
947}
948
949impl From<String> for ExpiresIn {
950    fn from(s: String) -> Self {
951        ExpiresIn::Duration(s)
952    }
953}
954
955impl From<u64> for ExpiresIn {
956    fn from(s: u64) -> Self {
957        ExpiresIn::Seconds(s)
958    }
959}
960
961impl From<i64> for ExpiresIn {
962    fn from(s: i64) -> Self {
963        ExpiresIn::Seconds(s as u64)
964    }
965}
966
967impl From<u32> for ExpiresIn {
968    fn from(s: u32) -> Self {
969        ExpiresIn::Seconds(s as u64)
970    }
971}
972
973impl From<i32> for ExpiresIn {
974    fn from(s: i32) -> Self {
975        ExpiresIn::Seconds(s as u64)
976    }
977}
978
979/// Options for generate_token
980#[derive(Debug, Clone, Default)]
981pub struct GenerateTokenOptions {
982    pub expires_in: Option<ExpiresIn>,
983}
984
985impl GenerateTokenOptions {
986    pub fn new() -> Self {
987        Self::default()
988    }
989
990    pub fn with_expires_in<T: Into<ExpiresIn>>(mut self, expires_in: T) -> Self {
991        self.expires_in = Some(expires_in.into());
992        self
993    }
994}