1use core::ffi::c_void;
2use core::ptr;
3
4use serde::Deserialize;
5
6use crate::error::StoreKitError;
7use crate::ffi;
8use crate::private::{cstring_from_str, error_from_status, parse_json_ptr, take_string};
9use crate::product::{Product, ProductType, PurchaseOption, PurchaseResult};
10use crate::purchase_option::PurchaseResultPayload;
11use crate::renewal_info::{ExpirationReason, PriceIncreaseStatus};
12use crate::renewal_state::RenewalState;
13use crate::subscription::{
14 SubscriptionOfferType, SubscriptionPaymentMode, SubscriptionPeriod, SubscriptionPeriodUnit,
15};
16use crate::transaction::{OfferType, OwnershipType, RevocationReason};
17use crate::window::NSWindowHandle;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ProductFormatting {
22 pub formatted_price: String,
24 pub formatted_subscription_period: Option<String>,
26 pub formatted_subscription_period_unit: Option<String>,
28}
29
30impl Product {
31 pub fn purchase_in_window(
33 &self,
34 window: &NSWindowHandle,
35 options: &[PurchaseOption],
36 ) -> Result<PurchaseResult, StoreKitError> {
37 let product_id = cstring_from_str(&self.id, "product id")?;
38 let options_json = crate::private::json_cstring(options, "purchase options")?;
39 let mut transaction_handle: *mut c_void = ptr::null_mut();
40 let mut result_json = ptr::null_mut();
41 let mut error_message = ptr::null_mut();
42 let status = unsafe {
43 ffi::sk_product_purchase_in_window(
44 product_id.as_ptr(),
45 window.as_raw(),
46 options_json.as_ptr(),
47 &mut transaction_handle,
48 &mut result_json,
49 &mut error_message,
50 )
51 };
52 if status != ffi::status::OK {
53 return Err(unsafe { error_from_status(status, error_message) });
54 }
55
56 let payload =
57 unsafe { parse_json_ptr::<PurchaseResultPayload>(result_json, "purchase result") };
58 match payload {
59 Ok(payload) => payload.into_purchase_result(transaction_handle),
60 Err(error) => {
61 if !transaction_handle.is_null() {
62 unsafe { ffi::sk_transaction_release(transaction_handle) };
63 }
64 Err(error)
65 }
66 }
67 }
68
69 pub fn formatting(&self) -> Result<ProductFormatting, StoreKitError> {
71 let product_id = cstring_from_str(&self.id, "product id")?;
72 let mut formatting_json = ptr::null_mut();
73 let mut error_message = ptr::null_mut();
74 let status = unsafe {
75 ffi::sk_product_formatting_json(
76 product_id.as_ptr(),
77 &mut formatting_json,
78 &mut error_message,
79 )
80 };
81 if status != ffi::status::OK {
82 return Err(unsafe { error_from_status(status, error_message) });
83 }
84 let payload = unsafe {
85 parse_json_ptr::<ProductFormattingPayload>(formatting_json, "product formatting")
86 }?;
87 Ok(payload.into_product_formatting())
88 }
89
90 pub fn formatted_price(&self) -> Result<String, StoreKitError> {
92 self.formatting()
93 .map(|formatting| formatting.formatted_price)
94 }
95
96 pub fn formatted_subscription_period(&self) -> Result<Option<String>, StoreKitError> {
98 self.formatting()
99 .map(|formatting| formatting.formatted_subscription_period)
100 }
101
102 pub fn formatted_subscription_period_unit(&self) -> Result<Option<String>, StoreKitError> {
104 self.formatting()
105 .map(|formatting| formatting.formatted_subscription_period_unit)
106 }
107}
108
109impl SubscriptionPeriod {
110 pub const fn weekly() -> Self {
112 Self {
113 unit: SubscriptionPeriodUnit::Week,
114 value: 1,
115 }
116 }
117
118 pub const fn monthly() -> Self {
120 Self {
121 unit: SubscriptionPeriodUnit::Month,
122 value: 1,
123 }
124 }
125
126 pub const fn yearly() -> Self {
128 Self {
129 unit: SubscriptionPeriodUnit::Year,
130 value: 1,
131 }
132 }
133
134 pub const fn every_three_days() -> Self {
136 Self {
137 unit: SubscriptionPeriodUnit::Day,
138 value: 3,
139 }
140 }
141
142 pub const fn every_two_weeks() -> Self {
144 Self {
145 unit: SubscriptionPeriodUnit::Week,
146 value: 2,
147 }
148 }
149
150 pub const fn every_two_months() -> Self {
152 Self {
153 unit: SubscriptionPeriodUnit::Month,
154 value: 2,
155 }
156 }
157
158 pub const fn every_three_months() -> Self {
160 Self {
161 unit: SubscriptionPeriodUnit::Month,
162 value: 3,
163 }
164 }
165
166 pub const fn every_six_months() -> Self {
168 Self {
169 unit: SubscriptionPeriodUnit::Month,
170 value: 6,
171 }
172 }
173}
174
175impl ProductType {
176 pub fn localized_description(&self) -> Result<String, StoreKitError> {
178 localized_description("productType", self.as_str())
179 }
180}
181
182impl RenewalState {
183 pub fn localized_description(&self) -> Result<String, StoreKitError> {
185 localized_description("renewalState", self.as_str())
186 }
187}
188
189impl ExpirationReason {
190 pub fn localized_description(&self) -> Result<String, StoreKitError> {
192 localized_description("expirationReason", self.as_str())
193 }
194}
195
196impl PriceIncreaseStatus {
197 pub fn localized_description(&self) -> Result<String, StoreKitError> {
199 localized_description("priceIncreaseStatus", self.as_str())
200 }
201}
202
203impl SubscriptionOfferType {
204 pub fn localized_description(&self) -> Result<String, StoreKitError> {
206 localized_description("subscriptionOfferType", self.as_str())
207 }
208}
209
210impl OfferType {
211 pub fn localized_description(&self) -> Result<String, StoreKitError> {
213 localized_description("transactionOfferType", self.as_str())
214 }
215}
216
217impl SubscriptionPaymentMode {
218 pub fn localized_description(&self) -> Result<String, StoreKitError> {
220 localized_description("subscriptionPaymentMode", self.as_str())
221 }
222}
223
224impl SubscriptionPeriodUnit {
225 pub fn localized_description(&self) -> Result<String, StoreKitError> {
227 localized_description("subscriptionPeriodUnit", self.as_str())
228 }
229}
230
231impl RevocationReason {
232 pub fn localized_description(&self) -> Result<String, StoreKitError> {
234 localized_description("revocationReason", self.as_str())
235 }
236}
237
238impl OwnershipType {
239 pub fn localized_description(&self) -> Result<String, StoreKitError> {
241 let raw = match self {
242 Self::Purchased => "purchased",
243 Self::FamilyShared => "familyShared",
244 Self::Unknown(value) => value.as_str(),
245 };
246 localized_description("ownershipType", raw)
247 }
248}
249
250fn localized_description(kind: &str, raw_value: &str) -> Result<String, StoreKitError> {
251 let kind = cstring_from_str(kind, "localized description kind")?;
252 let raw_value = cstring_from_str(raw_value, "localized description raw value")?;
253 let mut localized = ptr::null_mut();
254 let mut error_message = ptr::null_mut();
255 let status = unsafe {
256 ffi::sk_localized_description(
257 kind.as_ptr(),
258 raw_value.as_ptr(),
259 &mut localized,
260 &mut error_message,
261 )
262 };
263 if status != ffi::status::OK {
264 return Err(unsafe { error_from_status(status, error_message) });
265 }
266 unsafe { take_string(localized) }
267 .ok_or_else(|| StoreKitError::Unknown("missing localized description payload".to_owned()))
268}
269
270#[derive(Debug, Deserialize)]
271struct ProductFormattingPayload {
272 #[serde(rename = "formattedPrice")]
273 price: String,
274 #[serde(rename = "formattedSubscriptionPeriod")]
275 subscription_period: Option<String>,
276 #[serde(rename = "formattedSubscriptionPeriodUnit")]
277 subscription_period_unit: Option<String>,
278}
279
280impl ProductFormattingPayload {
281 fn into_product_formatting(self) -> ProductFormatting {
282 ProductFormatting {
283 formatted_price: self.price,
284 formatted_subscription_period: self.subscription_period,
285 formatted_subscription_period_unit: self.subscription_period_unit,
286 }
287 }
288}