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)]
143pub(crate) struct 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 pub(crate) 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}