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