1use std::{fmt::Display, str::FromStr};
2
3use bytes::Bytes;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_with::base64::Base64;
7use uuid::Uuid;
8
9#[cfg(feature = "uniffi")]
10uniffi::setup_scaffolding!();
11
12#[cfg(feature = "uniffi")]
13uniffi::custom_type!(Bytes, Vec<u8>, {
14 remote,
15 try_lift: |val| Ok(val.into()),
16 lower: |obj| obj.into(),
17});
18
19#[cfg(feature = "uniffi")]
20uniffi::custom_type!(ConnectionId, String, {
21 try_lift: |val| Ok(val.parse()?),
22 lower: |obj| obj.to_string(),
23});
24
25#[cfg(feature = "uniffi")]
26uniffi::custom_type!(Decimal, String, {
27 remote,
28 try_lift: |val| Ok(val.parse()?),
29 lower: |obj| obj.to_string(),
30});
31
32#[derive(Serialize, Eq, PartialEq, Clone, Hash, Debug)]
36pub struct ConnectionId(String);
37
38impl Display for ConnectionId {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 self.0.fmt(f)
41 }
42}
43
44impl<'de> Deserialize<'de> for ConnectionId {
45 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46 where
47 D: serde::Deserializer<'de>,
48 {
49 String::deserialize(deserializer)?
50 .parse()
51 .map_err(serde::de::Error::custom)
52 }
53}
54
55impl From<Uuid> for ConnectionId {
56 fn from(value: Uuid) -> Self {
57 Self(format!("connection-{value}"))
58 }
59}
60
61impl From<&ConnectionId> for Uuid {
62 fn from(value: &ConnectionId) -> Self {
63 (value.0[11..]).parse().unwrap()
64 }
65}
66
67impl FromStr for ConnectionId {
68 type Err = uuid::Error;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 s.trim_start_matches("connection-")
72 .parse::<Uuid>()
73 .map(Into::into)
74 }
75}
76
77#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)]
79#[non_exhaustive]
80#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
81#[serde(rename_all = "camelCase")]
82pub struct CredentialsModel {
83 pub full: bool,
85
86 pub user_id: bool,
91
92 pub none: bool,
94}
95
96#[cfg(feature = "kitx")]
97impl CredentialsModel {
98 pub const FULL: CredentialsModel = CredentialsModel {
99 full: true,
100 user_id: false,
101 none: false,
102 };
103
104 pub const USER_ID: CredentialsModel = CredentialsModel {
105 full: false,
106 user_id: true,
107 none: false,
108 };
109
110 pub const NONE: CredentialsModel = CredentialsModel {
111 full: false,
112 user_id: false,
113 none: true,
114 };
115
116 pub const OPT_USER: CredentialsModel = CredentialsModel {
117 full: false,
118 user_id: true,
119 none: true,
120 };
121
122 pub const OPT_FULL: CredentialsModel = CredentialsModel {
123 full: true,
124 user_id: false,
125 none: true,
126 };
127}
128
129#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
130#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
131#[non_exhaustive]
132pub enum PaymentErrorCode {
133 LimitExceeded,
134 InsufficientFunds,
135}
136
137#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
138#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
139#[non_exhaustive]
140pub enum ProviderErrorCode {
141 Maintenance,
142}
143
144#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
145#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
146#[non_exhaustive]
147pub enum ServiceBlockedCode {
148 MissingSetup,
150 ActionRequired,
152}
153
154#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
155#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
156#[non_exhaustive]
157pub enum UnsupportedProductReason {
158 Limit,
160 Recipient,
162}
163
164#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
165#[serde(rename_all = "camelCase")]
166#[non_exhaustive]
167pub struct Account {
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub iban: Option<String>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub number: Option<String>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub bic: Option<String>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub bank_code: Option<String>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub currency: Option<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub name: Option<String>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub display_name: Option<String>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub owner_name: Option<String>,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub product_name: Option<String>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub status: Option<AccountStatus>,
207
208 #[serde(skip_serializing_if = "Option::is_none")]
209 #[serde(rename = "type")]
210 pub type_: Option<AccountType>,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub capabilities: Option<Vec<Capability>>,
214}
215
216#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
217#[non_exhaustive]
218#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
219pub enum AccountStatus {
220 Available,
221 Terminated,
222 Blocked,
223}
224
225#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
226#[non_exhaustive]
227#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
228pub enum AccountType {
229 Current,
232 Card,
235 Savings,
238 CallMoney,
241 TimeDeposit,
244 Loan,
247 Securities,
248 Insurance,
249 Commerce,
250 Rewards,
251}
252
253#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
254#[serde(rename_all = "camelCase")]
255#[non_exhaustive]
256#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
257pub struct Amount {
258 pub currency: String,
260
261 pub amount: Decimal,
262}
263
264impl Amount {
265 pub fn new(amount: impl Into<Decimal>, currency: impl Into<String>) -> Self {
266 Self {
267 amount: amount.into(),
268 currency: currency.into(),
269 }
270 }
271}
272
273#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
274#[non_exhaustive]
275pub enum Capability {
276 Balances,
277 Documents,
278 Securities,
279 Transactions,
280 SinglePayment,
281 BulkPayment,
282 StandingOrders,
283 ScheduledTransfers,
284}
285
286#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
287#[non_exhaustive]
288#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
289pub enum TransactionStatus {
290 Pending,
292 Booked,
294 Invoiced,
296 Paid,
298 Canceled,
300}
301
302#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
303#[serde(rename_all = "camelCase")]
304#[non_exhaustive]
305#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
306pub struct Fee {
307 pub amount: Amount,
309
310 #[serde(skip_serializing_if = "Option::is_none")]
312 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
313 pub kind: Option<String>,
314
315 #[serde(skip_serializing_if = "Option::is_none")]
317 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
318 pub bic: Option<String>,
319}
320
321impl Fee {
322 pub fn new(amount: impl Into<Amount>) -> Self {
323 Self {
324 amount: amount.into(),
325 kind: None,
326 bic: None,
327 }
328 }
329}
330
331#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
344#[serde(rename_all = "camelCase")]
345#[non_exhaustive]
346pub struct Dialog<ConfirmationContext, InputContext> {
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub context: Option<DialogContext>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub message: Option<String>,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 pub image: Option<Image>,
353 #[serde(bound(
354 serialize = "ConfirmationContext: AsRef<[u8]>, InputContext: AsRef<[u8]>",
355 deserialize = "ConfirmationContext: From<Vec<u8>>, InputContext: From<Vec<u8>>"
356 ))]
357 pub input: DialogInput<ConfirmationContext, InputContext>,
358}
359
360impl<ConCtx, InpCtx> Dialog<ConCtx, InpCtx> {
361 pub fn new(input: DialogInput<ConCtx, InpCtx>) -> Self {
362 Self {
363 context: None,
364 message: None,
365 image: None,
366 input,
367 }
368 }
369}
370
371#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
373#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
374#[non_exhaustive]
375pub enum DialogContext {
376 Sca,
383
384 Accounts,
389
390 Redirect,
394
395 PaymentStatus,
399
400 VopConfirmation,
405
406 VopCheck,
410}
411
412#[serde_with::serde_as]
414#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
415#[serde(rename_all_fields = "camelCase")]
416pub enum DialogInput<ConfirmationContext, InputContext> {
417 Confirmation {
419 #[serde(bound(
421 serialize = "ConfirmationContext: AsRef<[u8]>",
422 deserialize = "ConfirmationContext: From<Vec<u8>>"
423 ))]
424 #[serde_as(as = "Base64")]
425 context: ConfirmationContext,
426
427 #[serde(skip_serializing_if = "Option::is_none")]
429 polling_delay_secs: Option<u32>,
430 },
431
432 Selection {
434 options: Vec<DialogOption>,
438
439 #[serde(bound(
441 serialize = "ConfirmationContext: AsRef<[u8]>",
442 deserialize = "ConfirmationContext: From<Vec<u8>>"
443 ))]
444 #[serde_as(as = "Base64")]
445 context: InputContext,
446 },
447
448 Field {
450 #[serde(rename = "type")]
452 type_: InputType,
453
454 secrecy_level: SecrecyLevel,
456
457 #[serde(skip_serializing_if = "Option::is_none")]
459 min_length: Option<u32>,
460
461 #[serde(skip_serializing_if = "Option::is_none")]
463 max_length: Option<u32>,
464 #[serde(bound(
465 serialize = "InputContext: AsRef<[u8]>",
466 deserialize = "InputContext: From<Vec<u8>>"
467 ))]
468
469 #[serde_as(as = "Base64")]
471 context: InputContext,
472 },
473}
474
475#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
477#[serde(rename_all = "camelCase")]
478#[non_exhaustive]
479#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
480pub struct DialogOption {
481 pub key: String,
482 pub label: String,
483 #[serde(skip_serializing_if = "Option::is_none")]
484 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
485 pub explanation: Option<String>,
486}
487
488impl DialogOption {
489 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
490 Self {
491 key: key.into(),
492 label: label.into(),
493 explanation: None,
494 }
495 }
496}
497
498#[serde_with::serde_as]
500#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
501#[serde(rename_all = "camelCase")]
502#[non_exhaustive]
503#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
504pub struct Image {
505 pub mime_type: String,
506 #[serde_as(as = "Base64")]
508 pub data: Bytes,
509 #[allow(clippy::doc_markdown)]
510 #[serde_as(as = "Option<Base64>")]
521 #[serde(skip_serializing_if = "Option::is_none")]
522 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
523 pub hhd_uc_data: Option<Bytes>,
524}
525
526impl Image {
527 pub fn new(mime_type: impl Into<String>, data: impl Into<Bytes>) -> Self {
528 Self {
529 mime_type: mime_type.into(),
530 data: data.into(),
531 hhd_uc_data: None,
532 }
533 }
534}
535
536#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
538#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
539pub enum SecrecyLevel {
540 Plain,
542 Otp,
545 Password,
547}
548
549#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
551#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
552pub enum InputType {
553 Date,
554 Email,
555 Number,
556 Phone,
557 Text,
558}
559
560#[cfg(test)]
561mod tests {
562 use std::str::FromStr;
563
564 use uuid::Uuid;
565
566 use crate::ConnectionId;
567
568 #[test]
569 fn uuid_conversion() {
570 let uuid = Uuid::new_v4();
571 assert_eq!(uuid, Uuid::from(&ConnectionId::from(uuid)));
572 }
573
574 #[test]
575 fn str_conversion() {
576 let uuid = Uuid::new_v4();
577 let s = format!("connection-{uuid}");
578
579 assert_eq!(s, ConnectionId::from_str(&s).unwrap().to_string());
580 assert_eq!(
581 s,
582 ConnectionId::from_str(&uuid.to_string())
583 .unwrap()
584 .to_string()
585 );
586 }
587
588 #[test]
589 fn deserialize_prefixed_connection_id() {
590 let _ = Uuid::from(
591 &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
592 "connection-00000000-0000-0000-0000-000000000000".to_string(),
593 ))
594 .unwrap(),
595 );
596 }
597
598 #[test]
599 fn deserialize_stripped_connection_id() {
600 let _ = Uuid::from(
601 &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
602 "00000000-0000-0000-0000-000000000000".to_string(),
603 ))
604 .unwrap(),
605 );
606 }
607
608 #[test]
609 fn deserialize_invalid_connection_id() {
610 serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(String::new()))
611 .unwrap_err();
612 }
613
614 #[test]
615 fn deserialize_invalid_prefixed_connection_id() {
616 serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
617 "connection-0000".to_string(),
618 ))
619 .unwrap_err();
620 }
621
622 #[cfg(feature = "uniffi")]
623 fn try_lift(val: impl Into<String>) -> anyhow::Result<ConnectionId> {
624 use uniffi::FfiConverter;
625
626 <ConnectionId as FfiConverter<()>>::try_lift(<String as FfiConverter<()>>::lower(
627 val.into(),
628 ))
629 }
630
631 #[cfg(feature = "uniffi")]
632 #[test]
633 fn convert_uuid_like_connection_id() {
634 let uuid = Uuid::new_v4();
635
636 assert_eq!(
637 try_lift(uuid.to_string()).unwrap(),
638 ConnectionId::from(uuid),
639 );
640 }
641
642 #[cfg(feature = "uniffi")]
643 #[test]
644 fn convert_prefixed_connection_id() {
645 let uuid = Uuid::new_v4();
646
647 assert_eq!(
648 try_lift(format!("connection-{uuid}")).unwrap(),
649 ConnectionId::from(uuid),
650 );
651 }
652
653 #[cfg(feature = "uniffi")]
654 #[test]
655 fn convert_connection_id() {
656 let connection_id = ConnectionId::from(Uuid::new_v4());
657
658 assert_eq!(try_lift(connection_id.to_string()).unwrap(), connection_id);
659 }
660
661 #[cfg(feature = "uniffi")]
662 #[test]
663 fn convert_invalid_string() {
664 assert_eq!(
665 try_lift(String::new()).unwrap_err().to_string(),
666 "invalid length: expected length 32 for simple format, found 0"
667 );
668 }
669}