tauri_plugin_iap/
models.rs

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