Skip to main content

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