tauri_plugin_iap/
models.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Deserialize, Serialize)]
4#[serde(rename_all = "camelCase")]
5pub struct InitializeRequest {}
6
7#[derive(Debug, Clone, Default, Deserialize, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct InitializeResponse {
10    pub success: bool,
11}
12
13#[derive(Debug, Deserialize, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub struct GetProductsRequest {
16    pub product_ids: Vec<String>,
17    #[serde(default = "default_product_type")]
18    pub product_type: String,
19}
20
21fn default_product_type() -> String {
22    "subs".to_string()
23}
24
25#[derive(Debug, Clone, Deserialize, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct PricingPhase {
28    pub formatted_price: String,
29    pub price_currency_code: String,
30    pub price_amount_micros: i64,
31    pub billing_period: String,
32    pub billing_cycle_count: i32,
33    pub recurrence_mode: i32,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct SubscriptionOffer {
39    pub offer_token: String,
40    pub base_plan_id: String,
41    pub offer_id: Option<String>,
42    pub pricing_phases: Vec<PricingPhase>,
43}
44
45#[derive(Debug, Clone, Deserialize, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub struct Product {
48    pub product_id: String,
49    pub title: String,
50    pub description: String,
51    pub product_type: String,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub formatted_price: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub price_currency_code: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub price_amount_micros: Option<i64>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub subscription_offer_details: Option<Vec<SubscriptionOffer>>,
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct GetProductsResponse {
65    pub products: Vec<Product>,
66}
67
68#[derive(Debug, Clone, Deserialize, Serialize)]
69#[serde(rename_all = "camelCase")]
70pub struct PurchaseOptions {
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub offer_token: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub obfuscated_account_id: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub obfuscated_profile_id: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub app_account_token: Option<String>,
79}
80
81#[derive(Debug, Deserialize, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct PurchaseRequest {
84    pub product_id: String,
85    #[serde(default = "default_product_type")]
86    pub product_type: String,
87    #[serde(flatten)]
88    pub options: Option<PurchaseOptions>,
89}
90
91#[derive(Debug, Clone, Deserialize, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct Purchase {
94    pub order_id: Option<String>,
95    pub package_name: String,
96    pub product_id: String,
97    pub purchase_time: i64,
98    pub purchase_token: String,
99    pub purchase_state: PurchaseStateValue,
100    pub is_auto_renewing: bool,
101    pub is_acknowledged: bool,
102    pub original_json: String,
103    pub signature: String,
104    pub original_id: Option<String>,
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct RestorePurchasesRequest {
110    #[serde(default = "default_product_type")]
111    pub product_type: String,
112}
113
114#[derive(Debug, Clone, Deserialize, Serialize)]
115#[serde(rename_all = "camelCase")]
116pub struct RestorePurchasesResponse {
117    pub purchases: Vec<Purchase>,
118}
119
120#[derive(Debug, Clone, Deserialize, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct PurchaseHistoryRecord {
123    pub product_id: String,
124    pub purchase_time: i64,
125    pub purchase_token: String,
126    pub quantity: i32,
127    pub original_json: String,
128    pub signature: String,
129}
130
131#[derive(Debug, Clone, Deserialize, Serialize)]
132#[serde(rename_all = "camelCase")]
133pub struct GetPurchaseHistoryResponse {
134    pub history: Vec<PurchaseHistoryRecord>,
135}
136
137#[derive(Debug, Deserialize, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct AcknowledgePurchaseRequest {
140    pub purchase_token: String,
141}
142
143#[derive(Debug, Clone, Deserialize, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct AcknowledgePurchaseResponse {
146    pub success: bool,
147}
148
149/// Keep in sync with PurchaseState in guest-js/index.ts
150#[derive(Debug, Clone, Copy, PartialEq)]
151pub enum PurchaseStateValue {
152    Purchased = 0,
153    Canceled = 1,
154    Pending = 2,
155}
156
157impl Serialize for PurchaseStateValue {
158    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159    where
160        S: serde::Serializer,
161    {
162        serializer.serialize_i32(*self as i32)
163    }
164}
165
166impl<'de> Deserialize<'de> for PurchaseStateValue {
167    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
168    where
169        D: serde::Deserializer<'de>,
170    {
171        let value = i32::deserialize(deserializer)?;
172        match value {
173            0 => Ok(PurchaseStateValue::Purchased),
174            1 => Ok(PurchaseStateValue::Canceled),
175            2 => Ok(PurchaseStateValue::Pending),
176            _ => Err(serde::de::Error::custom(format!(
177                "Invalid purchase state: {value}"
178            ))),
179        }
180    }
181}
182
183#[derive(Debug, Deserialize, Serialize)]
184#[serde(rename_all = "camelCase")]
185pub struct GetProductStatusRequest {
186    pub product_id: String,
187    #[serde(default = "default_product_type")]
188    pub product_type: String,
189}
190
191#[derive(Debug, Clone, Deserialize, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct ProductStatus {
194    pub product_id: String,
195    pub is_owned: bool,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub purchase_state: Option<PurchaseStateValue>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub purchase_time: Option<i64>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub expiration_time: Option<i64>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub is_auto_renewing: Option<bool>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub is_acknowledged: Option<bool>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub purchase_token: Option<String>,
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_default_product_type() {
216        assert_eq!(default_product_type(), "subs");
217    }
218
219    #[test]
220    fn test_purchase_state_value_serialize() {
221        assert_eq!(
222            serde_json::to_string(&PurchaseStateValue::Purchased)
223                .expect("Failed to serialize Purchased state"),
224            "0"
225        );
226        assert_eq!(
227            serde_json::to_string(&PurchaseStateValue::Canceled)
228                .expect("Failed to serialize Canceled state"),
229            "1"
230        );
231        assert_eq!(
232            serde_json::to_string(&PurchaseStateValue::Pending)
233                .expect("Failed to serialize Pending state"),
234            "2"
235        );
236    }
237
238    #[test]
239    fn test_purchase_state_value_deserialize() {
240        assert_eq!(
241            serde_json::from_str::<PurchaseStateValue>("0")
242                .expect("Failed to deserialize Purchased state"),
243            PurchaseStateValue::Purchased
244        );
245        assert_eq!(
246            serde_json::from_str::<PurchaseStateValue>("1")
247                .expect("Failed to deserialize Canceled state"),
248            PurchaseStateValue::Canceled
249        );
250        assert_eq!(
251            serde_json::from_str::<PurchaseStateValue>("2")
252                .expect("Failed to deserialize Pending state"),
253            PurchaseStateValue::Pending
254        );
255    }
256
257    #[test]
258    fn test_purchase_state_value_deserialize_invalid() {
259        let result = serde_json::from_str::<PurchaseStateValue>("3");
260        assert!(result.is_err());
261        let err = result
262            .expect_err("Expected error for invalid state")
263            .to_string();
264        assert!(err.contains("Invalid purchase state: 3"));
265    }
266
267    #[test]
268    fn test_purchase_state_value_roundtrip() {
269        for state in [
270            PurchaseStateValue::Purchased,
271            PurchaseStateValue::Canceled,
272            PurchaseStateValue::Pending,
273        ] {
274            let serialized =
275                serde_json::to_string(&state).expect("Failed to serialize PurchaseStateValue");
276            let deserialized: PurchaseStateValue = serde_json::from_str(&serialized)
277                .expect("Failed to deserialize PurchaseStateValue");
278            assert_eq!(state, deserialized);
279        }
280    }
281
282    #[test]
283    fn test_initialize_response_default() {
284        let response = InitializeResponse::default();
285        assert!(!response.success);
286    }
287
288    #[test]
289    fn test_initialize_response_serde() {
290        let response = InitializeResponse { success: true };
291        let json =
292            serde_json::to_string(&response).expect("Failed to serialize InitializeResponse");
293        assert_eq!(json, r#"{"success":true}"#);
294
295        let deserialized: InitializeResponse =
296            serde_json::from_str(&json).expect("Failed to deserialize InitializeResponse");
297        assert!(deserialized.success);
298    }
299
300    #[test]
301    fn test_get_products_request_default_product_type() {
302        let json = r#"{"productIds":["product1","product2"]}"#;
303        let request: GetProductsRequest =
304            serde_json::from_str(json).expect("Failed to deserialize GetProductsRequest");
305        assert_eq!(request.product_ids, vec!["product1", "product2"]);
306        assert_eq!(request.product_type, "subs");
307    }
308
309    #[test]
310    fn test_get_products_request_explicit_product_type() {
311        let json = r#"{"productIds":["product1"],"productType":"inapp"}"#;
312        let request: GetProductsRequest =
313            serde_json::from_str(json).expect("Failed to deserialize GetProductsRequest");
314        assert_eq!(request.product_type, "inapp");
315    }
316
317    #[test]
318    fn test_product_optional_fields_skip_serializing() {
319        let product = Product {
320            product_id: "test".to_string(),
321            title: "Test Product".to_string(),
322            description: "A test product".to_string(),
323            product_type: "inapp".to_string(),
324            formatted_price: None,
325            price_currency_code: None,
326            price_amount_micros: None,
327            subscription_offer_details: None,
328        };
329        let json = serde_json::to_string(&product).expect("Failed to serialize Product");
330        assert!(!json.contains("formattedPrice"));
331        assert!(!json.contains("priceCurrencyCode"));
332        assert!(!json.contains("priceAmountMicros"));
333        assert!(!json.contains("subscriptionOfferDetails"));
334    }
335
336    #[test]
337    fn test_product_with_optional_fields() {
338        let product = Product {
339            product_id: "test".to_string(),
340            title: "Test Product".to_string(),
341            description: "A test product".to_string(),
342            product_type: "inapp".to_string(),
343            formatted_price: Some("$9.99".to_string()),
344            price_currency_code: Some("USD".to_string()),
345            price_amount_micros: Some(9990000),
346            subscription_offer_details: None,
347        };
348        let json = serde_json::to_string(&product).expect("Failed to serialize Product");
349        assert!(json.contains(r#""formattedPrice":"$9.99""#));
350        assert!(json.contains(r#""priceCurrencyCode":"USD""#));
351        assert!(json.contains(r#""priceAmountMicros":9990000"#));
352    }
353
354    #[test]
355    fn test_purchase_serde_roundtrip() {
356        let purchase = Purchase {
357            order_id: Some("order123".to_string()),
358            package_name: "com.example.app".to_string(),
359            product_id: "product1".to_string(),
360            purchase_time: 1700000000000,
361            purchase_token: "token123".to_string(),
362            purchase_state: PurchaseStateValue::Purchased,
363            is_auto_renewing: true,
364            is_acknowledged: false,
365            original_json: "{}".to_string(),
366            signature: "sig".to_string(),
367            original_id: None,
368        };
369
370        let json = serde_json::to_string(&purchase).expect("Failed to serialize Purchase");
371        let deserialized: Purchase =
372            serde_json::from_str(&json).expect("Failed to deserialize Purchase");
373
374        assert_eq!(deserialized.order_id, purchase.order_id);
375        assert_eq!(deserialized.product_id, purchase.product_id);
376        assert_eq!(deserialized.purchase_time, purchase.purchase_time);
377        assert_eq!(deserialized.purchase_state, purchase.purchase_state);
378        assert_eq!(deserialized.is_auto_renewing, purchase.is_auto_renewing);
379    }
380
381    #[test]
382    fn test_pricing_phase_serde() {
383        let phase = PricingPhase {
384            formatted_price: "$4.99".to_string(),
385            price_currency_code: "USD".to_string(),
386            price_amount_micros: 4990000,
387            billing_period: "P1M".to_string(),
388            billing_cycle_count: 1,
389            recurrence_mode: 1,
390        };
391
392        let json = serde_json::to_string(&phase).expect("Failed to serialize PricingPhase");
393        assert!(json.contains(r#""formattedPrice":"$4.99""#));
394        assert!(json.contains(r#""billingPeriod":"P1M""#));
395
396        let deserialized: PricingPhase =
397            serde_json::from_str(&json).expect("Failed to deserialize PricingPhase");
398        assert_eq!(deserialized.price_amount_micros, 4990000);
399    }
400
401    #[test]
402    fn test_subscription_offer_serde() {
403        let offer = SubscriptionOffer {
404            offer_token: "token123".to_string(),
405            base_plan_id: "base_plan".to_string(),
406            offer_id: Some("offer1".to_string()),
407            pricing_phases: vec![PricingPhase {
408                formatted_price: "$9.99".to_string(),
409                price_currency_code: "USD".to_string(),
410                price_amount_micros: 9990000,
411                billing_period: "P1M".to_string(),
412                billing_cycle_count: 0,
413                recurrence_mode: 1,
414            }],
415        };
416
417        let json = serde_json::to_string(&offer).expect("Failed to serialize SubscriptionOffer");
418        let deserialized: SubscriptionOffer =
419            serde_json::from_str(&json).expect("Failed to deserialize SubscriptionOffer");
420        assert_eq!(deserialized.offer_token, "token123");
421        assert_eq!(deserialized.pricing_phases.len(), 1);
422    }
423
424    #[test]
425    fn test_purchase_options_flatten() {
426        let json = r#"{"productId":"prod1","offerToken":"token","obfuscatedAccountId":"acc123"}"#;
427        let request: PurchaseRequest =
428            serde_json::from_str(json).expect("Failed to deserialize PurchaseRequest");
429
430        assert_eq!(request.product_id, "prod1");
431        assert_eq!(request.product_type, "subs"); // default
432        let opts = request
433            .options
434            .expect("Expected PurchaseOptions to be present");
435        assert_eq!(opts.offer_token, Some("token".to_string()));
436        assert_eq!(opts.obfuscated_account_id, Some("acc123".to_string()));
437    }
438
439    #[test]
440    fn test_restore_purchases_request_default() {
441        let json = r#"{}"#;
442        let request: RestorePurchasesRequest =
443            serde_json::from_str(json).expect("Failed to deserialize RestorePurchasesRequest");
444        assert_eq!(request.product_type, "subs");
445    }
446
447    #[test]
448    fn test_product_status_optional_fields() {
449        let status = ProductStatus {
450            product_id: "prod1".to_string(),
451            is_owned: false,
452            purchase_state: None,
453            purchase_time: None,
454            expiration_time: None,
455            is_auto_renewing: None,
456            is_acknowledged: None,
457            purchase_token: None,
458        };
459
460        let json = serde_json::to_string(&status).expect("Failed to serialize ProductStatus");
461        // Optional None fields should be skipped
462        assert!(!json.contains("purchaseState"));
463        assert!(!json.contains("purchaseTime"));
464        assert!(!json.contains("expirationTime"));
465    }
466
467    #[test]
468    fn test_product_status_with_values() {
469        let status = ProductStatus {
470            product_id: "prod1".to_string(),
471            is_owned: true,
472            purchase_state: Some(PurchaseStateValue::Purchased),
473            purchase_time: Some(1700000000000),
474            expiration_time: Some(1703000000000),
475            is_auto_renewing: Some(true),
476            is_acknowledged: Some(true),
477            purchase_token: Some("token123".to_string()),
478        };
479
480        let json = serde_json::to_string(&status).expect("Failed to serialize ProductStatus");
481        assert!(json.contains(r#""isOwned":true"#));
482        assert!(json.contains(r#""purchaseState":0"#));
483        assert!(json.contains(r#""isAutoRenewing":true"#));
484    }
485
486    #[test]
487    fn test_acknowledge_purchase_request_serde() {
488        let request = AcknowledgePurchaseRequest {
489            purchase_token: "token123".to_string(),
490        };
491        let json = serde_json::to_string(&request)
492            .expect("Failed to serialize AcknowledgePurchaseRequest");
493        assert_eq!(json, r#"{"purchaseToken":"token123"}"#);
494    }
495
496    #[test]
497    fn test_get_product_status_request_serde() {
498        let json = r#"{"productId":"prod1"}"#;
499        let request: GetProductStatusRequest =
500            serde_json::from_str(json).expect("Failed to deserialize GetProductStatusRequest");
501        assert_eq!(request.product_id, "prod1");
502        assert_eq!(request.product_type, "subs"); // default
503    }
504
505    #[test]
506    fn test_purchase_history_record_serde() {
507        let record = PurchaseHistoryRecord {
508            product_id: "prod1".to_string(),
509            purchase_time: 1700000000000,
510            purchase_token: "token".to_string(),
511            quantity: 1,
512            original_json: "{}".to_string(),
513            signature: "sig".to_string(),
514        };
515
516        let json =
517            serde_json::to_string(&record).expect("Failed to serialize PurchaseHistoryRecord");
518        let deserialized: PurchaseHistoryRecord =
519            serde_json::from_str(&json).expect("Failed to deserialize PurchaseHistoryRecord");
520        assert_eq!(deserialized.quantity, 1);
521    }
522}