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)]
24/// Wraps `StoreKit.Product.ProductType`.
25pub enum ProductType {
26    /// Represents the `Consumable` `StoreKit` case.
27    Consumable,
28    /// Represents the `NonConsumable` `StoreKit` case.
29    NonConsumable,
30    /// Represents the `AutoRenewable` `StoreKit` case.
31    AutoRenewable,
32    /// Represents the `NonRenewing` `StoreKit` case.
33    NonRenewing,
34    /// Preserves an unrecognized `StoreKit` case.
35    Unknown(String),
36}
37
38impl ProductType {
39    /// Returns the raw `StoreKit` string for this product type.
40    pub fn as_str(&self) -> &str {
41        match self {
42            Self::Consumable => "consumable",
43            Self::NonConsumable => "nonConsumable",
44            Self::AutoRenewable => "autoRenewable",
45            Self::NonRenewing => "nonRenewing",
46            Self::Unknown(value) => value.as_str(),
47        }
48    }
49
50    pub(crate) fn from_raw(raw: String) -> Self {
51        match raw.as_str() {
52            "consumable" => Self::Consumable,
53            "nonConsumable" => Self::NonConsumable,
54            "autoRenewable" => Self::AutoRenewable,
55            "nonRenewing" => Self::NonRenewing,
56            _ => Self::Unknown(raw),
57        }
58    }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62/// Wraps `StoreKit.Product`.
63pub struct Product {
64    /// `StoreKit` identifier for this value.
65    pub id: String,
66    /// Display name reported by `StoreKit`.
67    pub display_name: String,
68    /// Description reported by `StoreKit`.
69    pub description: String,
70    /// Price reported by `StoreKit`.
71    pub price: String,
72    /// Localized display price reported by `StoreKit`.
73    pub display_price: String,
74    /// Product type reported by `StoreKit`.
75    pub product_type: ProductType,
76    /// Whether `StoreKit` reports that the product is family shareable.
77    pub is_family_shareable: bool,
78    /// Subscription metadata reported by `StoreKit`.
79    pub subscription: Option<SubscriptionInfo>,
80    /// Currency code reported by `StoreKit`.
81    pub currency_code: Option<String>,
82    /// Locale identifier used by `StoreKit` for price formatting.
83    pub price_locale_identifier: Option<String>,
84    /// Decoded JSON representation returned by `StoreKit`.
85    pub json_representation: Vec<u8>,
86}
87
88impl Product {
89    /// Calls `StoreKit.Product.products(for:)`.
90    pub fn products_for<I, S>(identifiers: I) -> Result<Vec<Self>, StoreKitError>
91    where
92        I: IntoIterator<Item = S>,
93        S: AsRef<str>,
94    {
95        let identifiers: Vec<String> = identifiers
96            .into_iter()
97            .map(|identifier| identifier.as_ref().to_owned())
98            .collect();
99        let identifiers_json = json_cstring(&identifiers, "product identifiers")?;
100        let mut products_json = ptr::null_mut();
101        let mut error_message = ptr::null_mut();
102        let status = unsafe {
103            ffi::sk_products_json(
104                identifiers_json.as_ptr(),
105                &mut products_json,
106                &mut error_message,
107            )
108        };
109        if status != ffi::status::OK {
110            return Err(unsafe { error_from_status(status, error_message) });
111        }
112        let payloads = unsafe { parse_json_ptr::<Vec<ProductPayload>>(products_json, "products") }?;
113        payloads
114            .into_iter()
115            .map(ProductPayload::into_product)
116            .collect::<Result<Vec<_>, _>>()
117    }
118
119    /// Calls `StoreKit.Product.purchase(options:)`.
120    pub fn purchase(&self, options: &[PurchaseOption]) -> Result<PurchaseResult, StoreKitError> {
121        let product_id = cstring_from_str(&self.id, "product id")?;
122        let options_json = json_cstring(options, "purchase options")?;
123        let mut transaction_handle: *mut c_void = ptr::null_mut();
124        let mut result_json = ptr::null_mut();
125        let mut error_message = ptr::null_mut();
126        let status = unsafe {
127            ffi::sk_product_purchase(
128                product_id.as_ptr(),
129                options_json.as_ptr(),
130                &mut transaction_handle,
131                &mut result_json,
132                &mut error_message,
133            )
134        };
135        if status != ffi::status::OK {
136            return Err(unsafe { error_from_status(status, error_message) });
137        }
138
139        let payload =
140            unsafe { parse_json_ptr::<PurchaseResultPayload>(result_json, "purchase result") };
141        match payload {
142            Ok(payload) => payload.into_purchase_result(transaction_handle),
143            Err(error) => {
144                if !transaction_handle.is_null() {
145                    unsafe { ffi::sk_transaction_release(transaction_handle) };
146                }
147                Err(error)
148            }
149        }
150    }
151
152    /// Fetches the latest `StoreKit` transaction for this product.
153    pub fn latest_transaction(
154        &self,
155    ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
156        Transaction::latest_for(&self.id)
157    }
158
159    /// Creates a stream of current `StoreKit` entitlements for this product.
160    pub fn current_entitlements(&self) -> Result<TransactionStream, StoreKitError> {
161        Transaction::current_entitlements_for(&self.id)
162    }
163}
164
165#[derive(Debug, Deserialize)]
166pub(crate) struct ProductPayload {
167    id: String,
168    #[serde(rename = "displayName")]
169    display_name: String,
170    description: String,
171    price: String,
172    #[serde(rename = "displayPrice")]
173    display_price: String,
174    #[serde(rename = "type")]
175    product_type: String,
176    #[serde(rename = "isFamilyShareable")]
177    is_family_shareable: bool,
178    subscription: Option<SubscriptionInfoPayload>,
179    #[serde(rename = "currencyCode")]
180    currency_code: Option<String>,
181    #[serde(rename = "priceLocaleIdentifier")]
182    price_locale_identifier: Option<String>,
183    #[serde(rename = "jsonRepresentationBase64")]
184    json_representation_base64: String,
185}
186
187impl ProductPayload {
188    pub(crate) fn into_product(self) -> Result<Product, StoreKitError> {
189        Ok(Product {
190            id: self.id,
191            display_name: self.display_name,
192            description: self.description,
193            price: self.price,
194            display_price: self.display_price,
195            product_type: ProductType::from_raw(self.product_type),
196            is_family_shareable: self.is_family_shareable,
197            subscription: self
198                .subscription
199                .map(SubscriptionInfoPayload::into_subscription_info),
200            currency_code: self.currency_code,
201            price_locale_identifier: self.price_locale_identifier,
202            json_representation: decode_base64(
203                &self.json_representation_base64,
204                "product JSON representation",
205            )?,
206        })
207    }
208}