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