Skip to main content

storekit/
product_helpers.rs

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}