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: user_name (max 200 chars), user_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    #[serde(skip_serializing_if = "Option::is_none")]
72    pub user_name: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub user_avatar_url: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub admin_scopes: Option<Vec<String>>,
77    /// Optional list of allowed email domains for invitation restrictions (e.g., ["acme.com", "acme.org"])
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub allowed_email_domains: Option<Vec<String>>,
80}
81
82impl User {
83    pub fn new(id: &str, email: &str) -> Self {
84        Self {
85            id: id.to_string(),
86            email: email.to_string(),
87            user_name: None,
88            user_avatar_url: None,
89            admin_scopes: None,
90            allowed_email_domains: None,
91        }
92    }
93
94    pub fn with_user_name(mut self, name: &str) -> Self {
95        self.user_name = Some(name.to_string());
96        self
97    }
98
99    pub fn with_user_avatar_url(mut self, avatar_url: &str) -> Self {
100        self.user_avatar_url = Some(avatar_url.to_string());
101        self
102    }
103
104    pub fn with_admin_scopes(mut self, scopes: Vec<String>) -> Self {
105        self.admin_scopes = Some(scopes);
106        self
107    }
108
109    pub fn with_allowed_email_domains(mut self, domains: Vec<String>) -> Self {
110        self.allowed_email_domains = Some(domains);
111        self
112    }
113}
114
115/// Identifier for a user (email, sms, etc.)
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Identifier {
118    #[serde(rename = "type")]
119    pub identifier_type: String,
120    pub value: String,
121}
122
123impl Identifier {
124    pub fn new(identifier_type: &str, value: &str) -> Self {
125        Self {
126            identifier_type: identifier_type.to_string(),
127            value: value.to_string(),
128        }
129    }
130}
131
132/// Group information for JWT generation (input)
133/// Supports both 'id' (legacy) and 'groupId' (preferred) for backward compatibility
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct Group {
137    #[serde(rename = "type")]
138    pub group_type: String,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub id: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub group_id: Option<String>,
143    pub name: String,
144}
145
146impl Group {
147    pub fn new(group_type: &str, name: &str) -> Self {
148        Self {
149            group_type: group_type.to_string(),
150            id: None,
151            group_id: None,
152            name: name.to_string(),
153        }
154    }
155
156    pub fn with_id(mut self, id: &str) -> Self {
157        self.id = Some(id.to_string());
158        self
159    }
160
161    pub fn with_group_id(mut self, group_id: &str) -> Self {
162        self.group_id = Some(group_id.to_string());
163        self
164    }
165}
166
167/// Invitation group from API responses
168/// This matches the MemberGroups table structure from the API
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct InvitationGroup {
172    /// Vortex internal UUID
173    pub id: String,
174    /// Vortex account ID
175    pub account_id: String,
176    /// Customer's group ID (the ID they provided to Vortex)
177    pub group_id: String,
178    /// Group type (e.g., "workspace", "team")
179    #[serde(rename = "type")]
180    pub group_type: String,
181    /// Group name
182    pub name: String,
183    /// ISO 8601 timestamp when the group was created
184    pub created_at: String,
185}
186
187/// Invitation target from API responses
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct InvitationTarget {
190    #[serde(rename = "type")]
191    pub target_type: InvitationTargetType,
192    pub value: String,
193    /// Display name of the person being invited
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub name: Option<String>,
196    /// Avatar URL for the person being invited (for display in invitation lists)
197    #[serde(skip_serializing_if = "Option::is_none")]
198    #[serde(rename = "avatarUrl")]
199    pub avatar_url: Option<String>,
200}
201
202impl InvitationTarget {
203    pub fn new(target_type: InvitationTargetType, value: &str) -> Self {
204        Self {
205            target_type,
206            value: value.to_string(),
207            name: None,
208            avatar_url: None,
209        }
210    }
211
212    pub fn email(value: &str) -> Self {
213        Self::new(InvitationTargetType::Email, value)
214    }
215
216    pub fn phone(value: &str) -> Self {
217        Self::new(InvitationTargetType::Phone, value)
218    }
219
220    pub fn with_name(mut self, name: &str) -> Self {
221        self.name = Some(name.to_string());
222        self
223    }
224
225    pub fn with_avatar_url(mut self, avatar_url: &str) -> Self {
226        self.avatar_url = Some(avatar_url.to_string());
227        self
228    }
229}
230
231/// User data for accepting invitations (preferred format)
232///
233/// At least one of email or phone must be provided.
234///
235/// # Example
236///
237/// ```
238/// use vortex_sdk::AcceptUser;
239///
240/// // With email only
241/// let user = AcceptUser::new().with_email("user@example.com");
242///
243/// // With email and name
244/// let user = AcceptUser::new()
245///     .with_email("user@example.com")
246///     .with_name("John Doe");
247///
248/// // With all fields
249/// let user = AcceptUser::new()
250///     .with_email("user@example.com")
251///     .with_phone("+1234567890")
252///     .with_name("John Doe");
253/// ```
254#[derive(Debug, Clone, Serialize, Deserialize, Default)]
255pub struct AcceptUser {
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub email: Option<String>,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub phone: Option<String>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub name: Option<String>,
262}
263
264impl AcceptUser {
265    pub fn new() -> Self {
266        Self::default()
267    }
268
269    pub fn with_email(mut self, email: &str) -> Self {
270        self.email = Some(email.to_string());
271        self
272    }
273
274    pub fn with_phone(mut self, phone: &str) -> Self {
275        self.phone = Some(phone.to_string());
276        self
277    }
278
279    pub fn with_name(mut self, name: &str) -> Self {
280        self.name = Some(name.to_string());
281        self
282    }
283}
284
285/// Invitation acceptance information
286#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct InvitationAcceptance {
289    pub id: Option<String>,
290    pub account_id: Option<String>,
291    pub project_id: Option<String>,
292    pub accepted_at: Option<String>,
293    pub target: Option<InvitationTarget>,
294}
295
296/// Full invitation details
297#[derive(Debug, Clone, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct Invitation {
300    #[serde(default)]
301    pub id: String,
302    #[serde(default)]
303    pub account_id: String,
304    #[serde(default)]
305    pub click_throughs: u32,
306    pub configuration_attributes: Option<HashMap<String, serde_json::Value>>,
307    pub attributes: Option<HashMap<String, serde_json::Value>>,
308    #[serde(default)]
309    pub created_at: String,
310    #[serde(default)]
311    pub deactivated: bool,
312    #[serde(default)]
313    pub delivery_count: u32,
314    #[serde(default)]
315    pub delivery_types: Vec<DeliveryType>,
316    #[serde(default)]
317    pub foreign_creator_id: String,
318    pub invitation_type: InvitationType,
319    pub modified_at: Option<String>,
320    pub status: InvitationStatus,
321    #[serde(default)]
322    pub target: Vec<InvitationTarget>,
323    #[serde(default)]
324    pub views: u32,
325    #[serde(default)]
326    pub widget_configuration_id: String,
327    #[serde(default)]
328    pub project_id: String,
329    #[serde(default)]
330    pub groups: Vec<InvitationGroup>,
331    #[serde(default)]
332    pub accepts: Vec<InvitationAcceptance>,
333    pub expired: bool,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub expires: Option<String>,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub source: Option<String>,
338    /// Customer-defined subtype for analytics segmentation (e.g., "pymk", "find-friends")
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub subtype: Option<String>,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub creator_name: Option<String>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub creator_avatar_url: Option<String>,
345}
346
347/// Response containing multiple invitations
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct InvitationsResponse {
350    pub invitations: Option<Vec<Invitation>>,
351}
352
353/// Accept invitation parameter - supports both new User format and legacy Target format
354#[derive(Debug, Clone)]
355pub enum AcceptInvitationParam {
356    /// New User format (preferred)
357    User(AcceptUser),
358    /// Legacy target format (deprecated)
359    Target(InvitationTarget),
360    /// Legacy multiple targets format (deprecated)
361    Targets(Vec<InvitationTarget>),
362}
363
364impl From<AcceptUser> for AcceptInvitationParam {
365    fn from(user: AcceptUser) -> Self {
366        AcceptInvitationParam::User(user)
367    }
368}
369
370impl From<InvitationTarget> for AcceptInvitationParam {
371    fn from(target: InvitationTarget) -> Self {
372        AcceptInvitationParam::Target(target)
373    }
374}
375
376impl From<Vec<InvitationTarget>> for AcceptInvitationParam {
377    fn from(targets: Vec<InvitationTarget>) -> Self {
378        AcceptInvitationParam::Targets(targets)
379    }
380}
381
382// --- Types for creating invitations via backend API ---
383
384/// Target for creating an invitation
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct CreateInvitationTarget {
387    #[serde(rename = "type")]
388    pub target_type: CreateInvitationTargetType,
389    /// Target value: email address, phone number, or internal user ID
390    pub value: String,
391    /// Display name of the person being invited
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub name: Option<String>,
394    /// Avatar URL for the person being invited (for display in invitation lists)
395    #[serde(skip_serializing_if = "Option::is_none")]
396    #[serde(rename = "avatarUrl")]
397    pub avatar_url: Option<String>,
398}
399
400impl CreateInvitationTarget {
401    pub fn new(target_type: CreateInvitationTargetType, value: &str) -> Self {
402        Self {
403            target_type,
404            value: value.to_string(),
405            name: None,
406            avatar_url: None,
407        }
408    }
409
410    pub fn email(value: &str) -> Self {
411        Self::new(CreateInvitationTargetType::Email, value)
412    }
413
414    pub fn phone(value: &str) -> Self {
415        Self::new(CreateInvitationTargetType::Phone, value)
416    }
417
418    /// Alias for phone (backward compatibility)
419    pub fn sms(value: &str) -> Self {
420        Self::phone(value)
421    }
422
423    pub fn internal(value: &str) -> Self {
424        Self::new(CreateInvitationTargetType::Internal, value)
425    }
426
427    pub fn with_name(mut self, name: &str) -> Self {
428        self.name = Some(name.to_string());
429        self
430    }
431
432    pub fn with_avatar_url(mut self, avatar_url: &str) -> Self {
433        self.avatar_url = Some(avatar_url.to_string());
434        self
435    }
436}
437
438/// Information about the user creating the invitation (the inviter)
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase")]
441pub struct Inviter {
442    /// Required: Your internal user ID for the inviter
443    pub user_id: String,
444    /// Optional: Email of the inviter
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub user_email: Option<String>,
447    /// Optional: Display name of the inviter
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub user_name: Option<String>,
450    /// Optional: Avatar URL of the inviter
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub user_avatar_url: Option<String>,
453}
454
455impl Inviter {
456    pub fn new(user_id: &str) -> Self {
457        Self {
458            user_id: user_id.to_string(),
459            user_email: None,
460            user_name: None,
461            user_avatar_url: None,
462        }
463    }
464
465    pub fn with_email(mut self, email: &str) -> Self {
466        self.user_email = Some(email.to_string());
467        self
468    }
469
470    pub fn with_user_name(mut self, name: &str) -> Self {
471        self.user_name = Some(name.to_string());
472        self
473    }
474
475    pub fn with_user_avatar_url(mut self, url: &str) -> Self {
476        self.user_avatar_url = Some(url.to_string());
477        self
478    }
479}
480
481/// Group information for creating invitations
482#[derive(Debug, Clone, Serialize, Deserialize)]
483#[serde(rename_all = "camelCase")]
484pub struct CreateInvitationGroup {
485    /// Group type (e.g., "team", "organization")
486    #[serde(rename = "type")]
487    pub group_type: String,
488    /// Your internal group ID
489    pub group_id: String,
490    /// Display name of the group
491    pub name: String,
492}
493
494impl CreateInvitationGroup {
495    pub fn new(group_type: &str, group_id: &str, name: &str) -> Self {
496        Self {
497            group_type: group_type.to_string(),
498            group_id: group_id.to_string(),
499            name: name.to_string(),
500        }
501    }
502}
503
504/// Valid Open Graph types for unfurl configuration
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case")]
507pub enum UnfurlOgType {
508    Website,
509    Article,
510    Video,
511    Music,
512    Book,
513    Profile,
514    Product,
515}
516
517/// Configuration for link unfurl (Open Graph) metadata
518/// Controls how the invitation link appears when shared on social platforms or messaging apps
519#[derive(Debug, Clone, Serialize, Deserialize, Default)]
520#[serde(rename_all = "camelCase")]
521pub struct UnfurlConfig {
522    /// The title shown in link previews (og:title)
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub title: Option<String>,
525    /// The description shown in link previews (og:description)
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub description: Option<String>,
528    /// The image URL shown in link previews (og:image) - must be HTTPS
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub image: Option<String>,
531    /// The Open Graph type (og:type)
532    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
533    pub og_type: Option<UnfurlOgType>,
534    /// The site name shown in link previews (og:site_name)
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub site_name: Option<String>,
537}
538
539impl UnfurlConfig {
540    pub fn new() -> Self {
541        Self::default()
542    }
543
544    pub fn with_title(mut self, title: &str) -> Self {
545        self.title = Some(title.to_string());
546        self
547    }
548
549    pub fn with_description(mut self, description: &str) -> Self {
550        self.description = Some(description.to_string());
551        self
552    }
553
554    pub fn with_image(mut self, image: &str) -> Self {
555        self.image = Some(image.to_string());
556        self
557    }
558
559    pub fn with_type(mut self, og_type: UnfurlOgType) -> Self {
560        self.og_type = Some(og_type);
561        self
562    }
563
564    pub fn with_site_name(mut self, site_name: &str) -> Self {
565        self.site_name = Some(site_name.to_string());
566        self
567    }
568}
569
570/// Request body for creating an invitation
571#[derive(Debug, Clone, Serialize, Deserialize)]
572#[serde(rename_all = "camelCase")]
573pub struct CreateInvitationRequest {
574    pub widget_configuration_id: String,
575    pub target: CreateInvitationTarget,
576    pub inviter: Inviter,
577    #[serde(skip_serializing_if = "Option::is_none")]
578    pub groups: Option<Vec<CreateInvitationGroup>>,
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub source: Option<String>,
581    /// Customer-defined subtype for analytics segmentation (e.g., "pymk", "find-friends")
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub subtype: Option<String>,
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub template_variables: Option<HashMap<String, String>>,
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub metadata: Option<HashMap<String, serde_json::Value>>,
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub unfurl_config: Option<UnfurlConfig>,
590}
591
592impl CreateInvitationRequest {
593    pub fn new(
594        widget_configuration_id: &str,
595        target: CreateInvitationTarget,
596        inviter: Inviter,
597    ) -> Self {
598        Self {
599            widget_configuration_id: widget_configuration_id.to_string(),
600            target,
601            inviter,
602            groups: None,
603            source: None,
604            subtype: None,
605            template_variables: None,
606            metadata: None,
607            unfurl_config: None,
608        }
609    }
610
611    pub fn with_groups(mut self, groups: Vec<CreateInvitationGroup>) -> Self {
612        self.groups = Some(groups);
613        self
614    }
615
616    pub fn with_source(mut self, source: &str) -> Self {
617        self.source = Some(source.to_string());
618        self
619    }
620
621    pub fn with_subtype(mut self, subtype: &str) -> Self {
622        self.subtype = Some(subtype.to_string());
623        self
624    }
625
626    pub fn with_template_variables(mut self, vars: HashMap<String, String>) -> Self {
627        self.template_variables = Some(vars);
628        self
629    }
630
631    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
632        self.metadata = Some(metadata);
633        self
634    }
635
636    pub fn with_unfurl_config(mut self, unfurl_config: UnfurlConfig) -> Self {
637        self.unfurl_config = Some(unfurl_config);
638        self
639    }
640}
641
642/// Response from creating an invitation
643#[derive(Debug, Clone, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct CreateInvitationResponse {
646    /// The ID of the created invitation
647    pub id: String,
648    /// The short link for the invitation
649    pub short_link: String,
650    /// The status of the invitation
651    pub status: String,
652    /// When the invitation was created
653    pub created_at: String,
654}
655
656// --- Types for syncing internal invitation actions ---
657
658/// Request body for syncing an internal invitation action
659#[derive(Debug, Clone, Serialize, Deserialize)]
660#[serde(rename_all = "camelCase")]
661pub struct SyncInternalInvitationRequest {
662    /// The inviter's user ID
663    pub creator_id: String,
664    /// The invitee's user ID
665    pub target_value: String,
666    /// The action taken: "accepted" or "declined"
667    pub action: String,
668    /// The widget component UUID
669    pub component_id: String,
670}
671
672impl SyncInternalInvitationRequest {
673    pub fn new(creator_id: &str, target_value: &str, action: &str, component_id: &str) -> Self {
674        Self {
675            creator_id: creator_id.to_string(),
676            target_value: target_value.to_string(),
677            action: action.to_string(),
678            component_id: component_id.to_string(),
679        }
680    }
681}
682
683/// Response from syncing an internal invitation action
684#[derive(Debug, Clone, Serialize, Deserialize)]
685#[serde(rename_all = "camelCase")]
686pub struct SyncInternalInvitationResponse {
687    /// Number of invitations processed
688    pub processed: u32,
689    /// IDs of the invitations that were processed
690    pub invitation_ids: Vec<String>,
691}
692
693// --- Types for autojoin domain management ---
694
695/// Represents an autojoin domain configuration
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct AutojoinDomain {
698    pub id: String,
699    pub domain: String,
700}
701
702/// Response from autojoin API endpoints
703#[derive(Debug, Clone, Serialize, Deserialize)]
704#[serde(rename_all = "camelCase")]
705pub struct AutojoinDomainsResponse {
706    pub autojoin_domains: Vec<AutojoinDomain>,
707    pub invitation: Option<Invitation>,
708}
709
710/// Request body for configuring autojoin domains
711#[derive(Debug, Clone, Serialize, Deserialize)]
712#[serde(rename_all = "camelCase")]
713pub struct ConfigureAutojoinRequest {
714    pub scope: String,
715    pub scope_type: String,
716    #[serde(skip_serializing_if = "Option::is_none")]
717    pub scope_name: Option<String>,
718    pub domains: Vec<String>,
719    pub widget_id: String,
720    #[serde(skip_serializing_if = "Option::is_none")]
721    pub metadata: Option<HashMap<String, serde_json::Value>>,
722}
723
724impl ConfigureAutojoinRequest {
725    pub fn new(scope: &str, scope_type: &str, domains: Vec<String>, widget_id: &str) -> Self {
726        Self {
727            scope: scope.to_string(),
728            scope_type: scope_type.to_string(),
729            scope_name: None,
730            domains,
731            widget_id: widget_id.to_string(),
732            metadata: None,
733        }
734    }
735
736    pub fn with_scope_name(mut self, scope_name: &str) -> Self {
737        self.scope_name = Some(scope_name.to_string());
738        self
739    }
740
741    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
742        self.metadata = Some(metadata);
743        self
744    }
745}