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