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