Skip to main content

storekit/
product.rs

1use core::ptr;
2use std::ffi::c_void;
3
4use serde::Deserialize;
5
6use crate::error::StoreKitError;
7use crate::ffi;
8use crate::private::{
9    cstring_from_str, decode_base64, error_from_status, json_cstring, parse_json_ptr,
10};
11pub use crate::purchase_option::{PurchaseOption, PurchaseResult};
12pub use crate::subscription::{
13    SubscriptionOffer, SubscriptionOfferType, SubscriptionPaymentMode, SubscriptionPeriod,
14    SubscriptionPeriodUnit,
15};
16pub use crate::subscription_info::SubscriptionInfo;
17
18use crate::purchase_option::PurchaseResultPayload;
19use crate::subscription_info::SubscriptionInfoPayload;
20use crate::transaction::{Transaction, TransactionStream};
21use crate::verification_result::VerificationResult;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ProductType {
25    Consumable,
26    NonConsumable,
27    AutoRenewable,
28    NonRenewing,
29    Unknown(String),
30}
31
32impl ProductType {
33    pub fn as_str(&self) -> &str {
34        match self {
35            Self::Consumable => "consumable",
36            Self::NonConsumable => "nonConsumable",
37            Self::AutoRenewable => "autoRenewable",
38            Self::NonRenewing => "nonRenewing",
39            Self::Unknown(value) => value.as_str(),
40        }
41    }
42
43    pub(crate) fn from_raw(raw: String) -> Self {
44        match raw.as_str() {
45            "consumable" => Self::Consumable,
46            "nonConsumable" => Self::NonConsumable,
47            "autoRenewable" => Self::AutoRenewable,
48            "nonRenewing" => Self::NonRenewing,
49            _ => Self::Unknown(raw),
50        }
51    }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Product {
56    pub id: String,
57    pub display_name: String,
58    pub description: String,
59    pub price: String,
60    pub display_price: String,
61    pub product_type: ProductType,
62    pub is_family_shareable: bool,
63    pub subscription: Option<SubscriptionInfo>,
64    pub currency_code: Option<String>,
65    pub price_locale_identifier: Option<String>,
66    pub json_representation: Vec<u8>,
67}
68
69impl Product {
70    pub fn products_for<I, S>(identifiers: I) -> Result<Vec<Self>, StoreKitError>
71    where
72        I: IntoIterator<Item = S>,
73        S: AsRef<str>,
74    {
75        let identifiers: Vec<String> = identifiers
76            .into_iter()
77            .map(|identifier| identifier.as_ref().to_owned())
78            .collect();
79        let identifiers_json = json_cstring(&identifiers, "product identifiers")?;
80        let mut products_json = ptr::null_mut();
81        let mut error_message = ptr::null_mut();
82        let status = unsafe {
83            ffi::sk_products_json(
84                identifiers_json.as_ptr(),
85                &mut products_json,
86                &mut error_message,
87            )
88        };
89        if status != ffi::status::OK {
90            return Err(unsafe { error_from_status(status, error_message) });
91        }
92        let payloads = unsafe { parse_json_ptr::<Vec<ProductPayload>>(products_json, "products") }?;
93        payloads
94            .into_iter()
95            .map(ProductPayload::into_product)
96            .collect::<Result<Vec<_>, _>>()
97    }
98
99    pub fn purchase(&self, options: &[PurchaseOption]) -> Result<PurchaseResult, StoreKitError> {
100        let product_id = cstring_from_str(&self.id, "product id")?;
101        let options_json = json_cstring(options, "purchase options")?;
102        let mut transaction_handle: *mut c_void = ptr::null_mut();
103        let mut result_json = ptr::null_mut();
104        let mut error_message = ptr::null_mut();
105        let status = unsafe {
106            ffi::sk_product_purchase(
107                product_id.as_ptr(),
108                options_json.as_ptr(),
109                &mut transaction_handle,
110                &mut result_json,
111                &mut error_message,
112            )
113        };
114        if status != ffi::status::OK {
115            return Err(unsafe { error_from_status(status, error_message) });
116        }
117
118        let payload =
119            unsafe { parse_json_ptr::<PurchaseResultPayload>(result_json, "purchase result") };
120        match payload {
121            Ok(payload) => payload.into_purchase_result(transaction_handle),
122            Err(error) => {
123                if !transaction_handle.is_null() {
124                    unsafe { ffi::sk_transaction_release(transaction_handle) };
125                }
126                Err(error)
127            }
128        }
129    }
130
131    pub fn latest_transaction(
132        &self,
133    ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
134        Transaction::latest_for(&self.id)
135    }
136
137    pub fn current_entitlements(&self) -> Result<TransactionStream, StoreKitError> {
138        Transaction::current_entitlements_for(&self.id)
139    }
140}
141
142#[derive(Debug, Deserialize)]
143struct ProductPayload {
144    id: String,
145    #[serde(rename = "displayName")]
146    display_name: String,
147    description: String,
148    price: String,
149    #[serde(rename = "displayPrice")]
150    display_price: String,
151    #[serde(rename = "type")]
152    product_type: String,
153    #[serde(rename = "isFamilyShareable")]
154    is_family_shareable: bool,
155    subscription: Option<SubscriptionInfoPayload>,
156    #[serde(rename = "currencyCode")]
157    currency_code: Option<String>,
158    #[serde(rename = "priceLocaleIdentifier")]
159    price_locale_identifier: Option<String>,
160    #[serde(rename = "jsonRepresentationBase64")]
161    json_representation_base64: String,
162}
163
164impl ProductPayload {
165    fn into_product(self) -> Result<Product, StoreKitError> {
166        Ok(Product {
167            id: self.id,
168            display_name: self.display_name,
169            description: self.description,
170            price: self.price,
171            display_price: self.display_price,
172            product_type: ProductType::from_raw(self.product_type),
173            is_family_shareable: self.is_family_shareable,
174            subscription: self
175                .subscription
176                .map(SubscriptionInfoPayload::into_subscription_info),
177            currency_code: self.currency_code,
178            price_locale_identifier: self.price_locale_identifier,
179            json_representation: decode_base64(
180                &self.json_representation_base64,
181                "product JSON representation",
182            )?,
183        })
184    }
185}