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)]
26pub enum ProductType {
28 Consumable,
30 NonConsumable,
32 AutoRenewable,
34 NonRenewing,
36 Unknown(String),
38}
39
40impl ProductType {
41 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)]
64pub struct Product {
66 pub id: String,
68 pub display_name: String,
70 pub description: String,
72 pub price: String,
74 pub display_price: String,
76 pub product_type: ProductType,
78 pub is_family_shareable: bool,
80 pub subscription: Option<SubscriptionInfo>,
82 pub currency_code: Option<String>,
84 pub price_locale_identifier: Option<String>,
86 pub json_representation: Vec<u8>,
88}
89
90impl Product {
91 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 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 pub fn latest_transaction(
156 &self,
157 ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
158 Transaction::latest_for(&self.id)
159 }
160
161 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}